From 76a0249eaf2505713d7b1e270c679757feada9b3 Mon Sep 17 00:00:00 2001 From: Coding On Star <447357187@qq.com> Date: Wed, 21 Jan 2026 14:04:33 +0800 Subject: [PATCH 1/4] feat: enhance ProgressBar and UsageInfo for storage mode (#31273) Co-authored-by: CodingOnStar --- .../billing/progress-bar/index.spec.tsx | 63 ++- .../components/billing/progress-bar/index.tsx | 17 +- .../billing/usage-info/index.spec.tsx | 382 +++++++++++++----- .../components/billing/usage-info/index.tsx | 147 ++++++- .../usage-info/vector-space-info.spec.tsx | 305 ++++++++++++++ .../billing/usage-info/vector-space-info.tsx | 24 +- web/app/components/billing/utils/index.ts | 28 +- .../billing/vector-space-full/index.spec.tsx | 14 +- web/eslint-suppressions.json | 5 - web/i18n/en-US/billing.json | 1 + web/i18n/ja-JP/billing.json | 1 + web/i18n/zh-Hans/billing.json | 1 + web/tailwind-common-config.ts | 1 + web/themes/manual-dark.css | 1 + web/themes/manual-light.css | 1 + 15 files changed, 859 insertions(+), 132 deletions(-) create mode 100644 web/app/components/billing/usage-info/vector-space-info.spec.tsx diff --git a/web/app/components/billing/progress-bar/index.spec.tsx b/web/app/components/billing/progress-bar/index.spec.tsx index a9c91468de..4eb66dcf79 100644 --- a/web/app/components/billing/progress-bar/index.spec.tsx +++ b/web/app/components/billing/progress-bar/index.spec.tsx @@ -2,24 +2,61 @@ import { render, screen } from '@testing-library/react' import ProgressBar from './index' describe('ProgressBar', () => { - it('renders with provided percent and color', () => { - render() + describe('Normal Mode (determinate)', () => { + it('renders with provided percent and color', () => { + render() - const bar = screen.getByTestId('billing-progress-bar') - expect(bar).toHaveClass('bg-test-color') - expect(bar.getAttribute('style')).toContain('width: 42%') + const bar = screen.getByTestId('billing-progress-bar') + expect(bar).toHaveClass('bg-test-color') + expect(bar.getAttribute('style')).toContain('width: 42%') + }) + + it('caps width at 100% when percent exceeds max', () => { + render() + + const bar = screen.getByTestId('billing-progress-bar') + expect(bar.getAttribute('style')).toContain('width: 100%') + }) + + it('uses the default color when no color prop is provided', () => { + render() + + const bar = screen.getByTestId('billing-progress-bar') + expect(bar).toHaveClass('bg-components-progress-bar-progress-solid') + expect(bar.getAttribute('style')).toContain('width: 20%') + }) }) - it('caps width at 100% when percent exceeds max', () => { - render() + describe('Indeterminate Mode', () => { + it('should render indeterminate progress bar when indeterminate is true', () => { + render() - const bar = screen.getByTestId('billing-progress-bar') - expect(bar.getAttribute('style')).toContain('width: 100%') - }) + const bar = screen.getByTestId('billing-progress-bar-indeterminate') + expect(bar).toBeInTheDocument() + expect(bar).toHaveClass('bg-progress-bar-indeterminate-stripe') + }) - it('uses the default color when no color prop is provided', () => { - render() + it('should not render normal progress bar when indeterminate is true', () => { + render() - expect(screen.getByTestId('billing-progress-bar')).toHaveClass('#2970FF') + expect(screen.queryByTestId('billing-progress-bar')).not.toBeInTheDocument() + expect(screen.getByTestId('billing-progress-bar-indeterminate')).toBeInTheDocument() + }) + + it('should render with default width (w-[30px]) when indeterminateFull is false', () => { + render() + + const bar = screen.getByTestId('billing-progress-bar-indeterminate') + expect(bar).toHaveClass('w-[30px]') + expect(bar).not.toHaveClass('w-full') + }) + + it('should render with full width (w-full) when indeterminateFull is true', () => { + render() + + const bar = screen.getByTestId('billing-progress-bar-indeterminate') + expect(bar).toHaveClass('w-full') + expect(bar).not.toHaveClass('w-[30px]') + }) }) }) diff --git a/web/app/components/billing/progress-bar/index.tsx b/web/app/components/billing/progress-bar/index.tsx index c41fc53310..f16bd952ea 100644 --- a/web/app/components/billing/progress-bar/index.tsx +++ b/web/app/components/billing/progress-bar/index.tsx @@ -3,12 +3,27 @@ import { cn } from '@/utils/classnames' type ProgressBarProps = { percent: number color: string + indeterminate?: boolean + indeterminateFull?: boolean // For Sandbox users: full width stripe } const ProgressBar = ({ percent = 0, - color = '#2970FF', + color = 'bg-components-progress-bar-progress-solid', + indeterminate = false, + indeterminateFull = false, }: ProgressBarProps) => { + if (indeterminate) { + return ( +
+
+
+ ) + } + return (
describe('UsageInfo', () => { - it('renders the metric with a suffix unit and tooltip text', () => { - render( - , - ) + describe('Default Mode (non-storage)', () => { + it('renders the metric with a suffix unit and tooltip text', () => { + render( + , + ) - expect(screen.getByTestId('usage-icon')).toBeInTheDocument() - expect(screen.getByText('Apps')).toBeInTheDocument() - expect(screen.getByText('30')).toBeInTheDocument() - expect(screen.getByText('100')).toBeInTheDocument() - expect(screen.getByText('GB')).toBeInTheDocument() + expect(screen.getByTestId('usage-icon')).toBeInTheDocument() + expect(screen.getByText('Apps')).toBeInTheDocument() + expect(screen.getByText('30')).toBeInTheDocument() + expect(screen.getByText('100')).toBeInTheDocument() + expect(screen.getByText('GB')).toBeInTheDocument() + }) + + it('renders inline unit when unitPosition is inline', () => { + render( + , + ) + + expect(screen.getByText('100GB')).toBeInTheDocument() + }) + + it('shows reset hint text instead of the unit when resetHint is provided', () => { + const resetHint = 'Resets in 3 days' + render( + , + ) + + expect(screen.getByText(resetHint)).toBeInTheDocument() + expect(screen.queryByText('GB')).not.toBeInTheDocument() + }) + + it('displays unlimited text when total is infinite', () => { + render( + , + ) + + expect(screen.getByText('billing.plansCommon.unlimited')).toBeInTheDocument() + }) + + it('applies warning color when usage is close to the limit', () => { + render( + , + ) + + const progressBar = screen.getByTestId('billing-progress-bar') + expect(progressBar).toHaveClass('bg-components-progress-warning-progress') + }) + + it('applies error color when usage exceeds the limit', () => { + render( + , + ) + + const progressBar = screen.getByTestId('billing-progress-bar') + expect(progressBar).toHaveClass('bg-components-progress-error-progress') + }) + + it('does not render the icon when hideIcon is true', () => { + render( + , + ) + + expect(screen.queryByTestId('usage-icon')).not.toBeInTheDocument() + }) }) - it('renders inline unit when unitPosition is inline', () => { - render( - , - ) + describe('Storage Mode', () => { + describe('Below Threshold', () => { + it('should render indeterminate progress bar when usage is below threshold', () => { + render( + , + ) - expect(screen.getByText('100GB')).toBeInTheDocument() - }) + expect(screen.getByTestId('billing-progress-bar-indeterminate')).toBeInTheDocument() + expect(screen.queryByTestId('billing-progress-bar')).not.toBeInTheDocument() + }) - it('shows reset hint text instead of the unit when resetHint is provided', () => { - const resetHint = 'Resets in 3 days' - render( - , - ) + it('should display "< threshold" format when usage is below threshold (non-sandbox)', () => { + render( + , + ) - expect(screen.getByText(resetHint)).toBeInTheDocument() - expect(screen.queryByText('GB')).not.toBeInTheDocument() - }) + // Text "< 50" is rendered inside a single span + expect(screen.getByText(/< 50/)).toBeInTheDocument() + expect(screen.getByText('5120MB')).toBeInTheDocument() + }) - it('displays unlimited text when total is infinite', () => { - render( - , - ) + it('should display "< threshold unit" format when usage is below threshold (sandbox)', () => { + render( + , + ) - expect(screen.getByText('billing.plansCommon.unlimited')).toBeInTheDocument() - }) + // Text "< 50" is rendered inside a single span + expect(screen.getByText(/< 50/)).toBeInTheDocument() + // Unit "MB" appears in the display + expect(screen.getAllByText('MB').length).toBeGreaterThanOrEqual(1) + }) - it('applies warning color when usage is close to the limit', () => { - render( - , - ) + it('should render full-width indeterminate bar for sandbox users below threshold', () => { + render( + , + ) - const progressBar = screen.getByTestId('billing-progress-bar') - expect(progressBar).toHaveClass('bg-components-progress-warning-progress') - }) + const bar = screen.getByTestId('billing-progress-bar-indeterminate') + expect(bar).toHaveClass('w-full') + }) - it('applies error color when usage exceeds the limit', () => { - render( - , - ) + it('should render narrow indeterminate bar for non-sandbox users below threshold', () => { + render( + , + ) - const progressBar = screen.getByTestId('billing-progress-bar') - expect(progressBar).toHaveClass('bg-components-progress-error-progress') - }) + const bar = screen.getByTestId('billing-progress-bar-indeterminate') + expect(bar).toHaveClass('w-[30px]') + }) + }) - it('does not render the icon when hideIcon is true', () => { - render( - , - ) + describe('Sandbox Full Capacity', () => { + it('should render error color progress bar when sandbox usage >= threshold', () => { + render( + , + ) - expect(screen.queryByTestId('usage-icon')).not.toBeInTheDocument() + const progressBar = screen.getByTestId('billing-progress-bar') + expect(progressBar).toHaveClass('bg-components-progress-error-progress') + }) + + it('should display "threshold / threshold unit" format when sandbox is at full capacity', () => { + render( + , + ) + + // First span: "50", Third span: "50 MB" + expect(screen.getByText('50')).toBeInTheDocument() + expect(screen.getByText(/50 MB/)).toBeInTheDocument() + expect(screen.getByText('/')).toBeInTheDocument() + }) + }) + + describe('Pro/Team Users Above Threshold', () => { + it('should render normal progress bar when usage >= threshold', () => { + render( + , + ) + + expect(screen.getByTestId('billing-progress-bar')).toBeInTheDocument() + expect(screen.queryByTestId('billing-progress-bar-indeterminate')).not.toBeInTheDocument() + }) + + it('should display actual usage when usage >= threshold', () => { + render( + , + ) + + expect(screen.getByText('100')).toBeInTheDocument() + expect(screen.getByText('5120MB')).toBeInTheDocument() + }) + }) + + describe('Storage Tooltip', () => { + it('should render tooltip wrapper when storageTooltip is provided', () => { + const { container } = render( + , + ) + + // Tooltip wrapper should contain cursor-default class + const tooltipWrapper = container.querySelector('.cursor-default') + expect(tooltipWrapper).toBeInTheDocument() + }) + }) }) }) diff --git a/web/app/components/billing/usage-info/index.tsx b/web/app/components/billing/usage-info/index.tsx index 8f0c1bcbcc..f820b85eab 100644 --- a/web/app/components/billing/usage-info/index.tsx +++ b/web/app/components/billing/usage-info/index.tsx @@ -1,5 +1,5 @@ 'use client' -import type { FC } from 'react' +import type { ComponentType, FC } from 'react' import * as React from 'react' import { useTranslation } from 'react-i18next' import Tooltip from '@/app/components/base/tooltip' @@ -9,7 +9,7 @@ import ProgressBar from '../progress-bar' type Props = { className?: string - Icon: any + Icon: ComponentType<{ className?: string }> name: string tooltip?: string usage: number @@ -19,6 +19,11 @@ type Props = { resetHint?: string resetInDays?: number hideIcon?: boolean + // Props for the 50MB threshold display logic + storageMode?: boolean + storageThreshold?: number + storageTooltip?: string + isSandboxPlan?: boolean } const WARNING_THRESHOLD = 80 @@ -35,30 +40,141 @@ const UsageInfo: FC = ({ resetHint, resetInDays, hideIcon = false, + storageMode = false, + storageThreshold = 50, + storageTooltip, + isSandboxPlan = false, }) => { const { t } = useTranslation() + // Special display logic for usage below threshold (only in storage mode) + const isBelowThreshold = storageMode && usage < storageThreshold + // Sandbox at full capacity (usage >= threshold and it's sandbox plan) + const isSandboxFull = storageMode && isSandboxPlan && usage >= storageThreshold + const percent = usage / total * 100 - const color = percent >= 100 - ? 'bg-components-progress-error-progress' - : (percent >= WARNING_THRESHOLD ? 'bg-components-progress-warning-progress' : 'bg-components-progress-bar-progress-solid') + const getProgressColor = () => { + if (percent >= 100) + return 'bg-components-progress-error-progress' + if (percent >= WARNING_THRESHOLD) + return 'bg-components-progress-warning-progress' + return 'bg-components-progress-bar-progress-solid' + } + const color = getProgressColor() const isUnlimited = total === NUM_INFINITE let totalDisplay: string | number = isUnlimited ? t('plansCommon.unlimited', { ns: 'billing' }) : total if (!isUnlimited && unit && unitPosition === 'inline') totalDisplay = `${total}${unit}` const showUnit = !!unit && !isUnlimited && unitPosition === 'suffix' const resetText = resetHint ?? (typeof resetInDays === 'number' ? t('usagePage.resetsIn', { ns: 'billing', count: resetInDays }) : undefined) - const rightInfo = resetText - ? ( + + const renderRightInfo = () => { + if (resetText) { + return (
{resetText}
) - : (showUnit && ( + } + if (showUnit) { + return (
{unit}
- )) + ) + } + return null + } + + // Render usage display + const renderUsageDisplay = () => { + // Storage mode: special display logic + if (storageMode) { + // Sandbox user at full capacity + if (isSandboxFull) { + return ( +
+ + {storageThreshold} + + / + + {storageThreshold} + {' '} + {unit} + +
+ ) + } + // Usage below threshold - show "< 50 MB" or "< 50 / 5GB" + if (isBelowThreshold) { + return ( +
+ + < + {' '} + {storageThreshold} + + {!isSandboxPlan && ( + <> + / + {totalDisplay} + + )} + {isSandboxPlan && {unit}} +
+ ) + } + // Pro/Team users with usage >= threshold - show actual usage + return ( +
+ {usage} + / + {totalDisplay} +
+ ) + } + + // Default display (storageMode = false) + return ( +
+ {usage} + / + {totalDisplay} +
+ ) + } + + const renderWithTooltip = (children: React.ReactNode) => { + if (storageMode && storageTooltip) { + return ( + {storageTooltip}
} + asChild={false} + > +
{children}
+ + ) + } + return children + } + + // Render progress bar with optional tooltip wrapper + const renderProgressBar = () => { + const progressBar = ( + + ) + return renderWithTooltip(progressBar) + } + + const renderUsageWithTooltip = () => { + return renderWithTooltip(renderUsageDisplay()) + } return (
@@ -78,17 +194,10 @@ const UsageInfo: FC = ({ )}
-
- {usage} -
/
-
{totalDisplay}
-
- {rightInfo} + {renderUsageWithTooltip()} + {renderRightInfo()}
- + {renderProgressBar()}
) } diff --git a/web/app/components/billing/usage-info/vector-space-info.spec.tsx b/web/app/components/billing/usage-info/vector-space-info.spec.tsx new file mode 100644 index 0000000000..a811cc9a09 --- /dev/null +++ b/web/app/components/billing/usage-info/vector-space-info.spec.tsx @@ -0,0 +1,305 @@ +import { render, screen } from '@testing-library/react' +import { defaultPlan } from '../config' +import { Plan } from '../type' +import VectorSpaceInfo from './vector-space-info' + +// Mock provider context with configurable plan +let mockPlanType = Plan.sandbox +let mockVectorSpaceUsage = 30 +let mockVectorSpaceTotal = 5120 + +vi.mock('@/context/provider-context', () => ({ + useProviderContext: () => ({ + plan: { + ...defaultPlan, + type: mockPlanType, + usage: { + ...defaultPlan.usage, + vectorSpace: mockVectorSpaceUsage, + }, + total: { + ...defaultPlan.total, + vectorSpace: mockVectorSpaceTotal, + }, + }, + }), +})) + +describe('VectorSpaceInfo', () => { + beforeEach(() => { + vi.clearAllMocks() + // Reset to default values + mockPlanType = Plan.sandbox + mockVectorSpaceUsage = 30 + mockVectorSpaceTotal = 5120 + }) + + describe('Rendering', () => { + it('should render vector space info component', () => { + render() + + expect(screen.getByText('billing.usagePage.vectorSpace')).toBeInTheDocument() + }) + + it('should apply custom className', () => { + render() + + const container = screen.getByText('billing.usagePage.vectorSpace').closest('.custom-class') + expect(container).toBeInTheDocument() + }) + }) + + describe('Sandbox Plan', () => { + beforeEach(() => { + mockPlanType = Plan.sandbox + mockVectorSpaceUsage = 30 + }) + + it('should render indeterminate progress bar when usage is below threshold', () => { + render() + + expect(screen.getByTestId('billing-progress-bar-indeterminate')).toBeInTheDocument() + }) + + it('should render full-width indeterminate bar for sandbox users', () => { + render() + + const bar = screen.getByTestId('billing-progress-bar-indeterminate') + expect(bar).toHaveClass('w-full') + }) + + it('should display "< 50" format for sandbox below threshold', () => { + render() + + expect(screen.getByText(/< 50/)).toBeInTheDocument() + }) + }) + + describe('Sandbox Plan at Full Capacity', () => { + beforeEach(() => { + mockPlanType = Plan.sandbox + mockVectorSpaceUsage = 50 + }) + + it('should render error color progress bar when at full capacity', () => { + render() + + const progressBar = screen.getByTestId('billing-progress-bar') + expect(progressBar).toHaveClass('bg-components-progress-error-progress') + }) + + it('should display "50 / 50 MB" format when at full capacity', () => { + render() + + expect(screen.getByText('50')).toBeInTheDocument() + expect(screen.getByText(/50 MB/)).toBeInTheDocument() + }) + }) + + describe('Professional Plan', () => { + beforeEach(() => { + mockPlanType = Plan.professional + mockVectorSpaceUsage = 30 + }) + + it('should render indeterminate progress bar when usage is below threshold', () => { + render() + + expect(screen.getByTestId('billing-progress-bar-indeterminate')).toBeInTheDocument() + }) + + it('should render narrow indeterminate bar (not full width)', () => { + render() + + const bar = screen.getByTestId('billing-progress-bar-indeterminate') + expect(bar).toHaveClass('w-[30px]') + expect(bar).not.toHaveClass('w-full') + }) + + it('should display "< 50 / total" format when below threshold', () => { + render() + + expect(screen.getByText(/< 50/)).toBeInTheDocument() + // 5 GB = 5120 MB + expect(screen.getByText('5120MB')).toBeInTheDocument() + }) + }) + + describe('Professional Plan Above Threshold', () => { + beforeEach(() => { + mockPlanType = Plan.professional + mockVectorSpaceUsage = 100 + }) + + it('should render normal progress bar when usage >= threshold', () => { + render() + + expect(screen.getByTestId('billing-progress-bar')).toBeInTheDocument() + expect(screen.queryByTestId('billing-progress-bar-indeterminate')).not.toBeInTheDocument() + }) + + it('should display actual usage when above threshold', () => { + render() + + expect(screen.getByText('100')).toBeInTheDocument() + expect(screen.getByText('5120MB')).toBeInTheDocument() + }) + }) + + describe('Team Plan', () => { + beforeEach(() => { + mockPlanType = Plan.team + mockVectorSpaceUsage = 30 + }) + + it('should render indeterminate progress bar when usage is below threshold', () => { + render() + + expect(screen.getByTestId('billing-progress-bar-indeterminate')).toBeInTheDocument() + }) + + it('should render narrow indeterminate bar (not full width)', () => { + render() + + const bar = screen.getByTestId('billing-progress-bar-indeterminate') + expect(bar).toHaveClass('w-[30px]') + expect(bar).not.toHaveClass('w-full') + }) + + it('should display "< 50 / total" format when below threshold', () => { + render() + + expect(screen.getByText(/< 50/)).toBeInTheDocument() + // 20 GB = 20480 MB + expect(screen.getByText('20480MB')).toBeInTheDocument() + }) + }) + + describe('Team Plan Above Threshold', () => { + beforeEach(() => { + mockPlanType = Plan.team + mockVectorSpaceUsage = 100 + }) + + it('should render normal progress bar when usage >= threshold', () => { + render() + + expect(screen.getByTestId('billing-progress-bar')).toBeInTheDocument() + expect(screen.queryByTestId('billing-progress-bar-indeterminate')).not.toBeInTheDocument() + }) + + it('should display actual usage when above threshold', () => { + render() + + expect(screen.getByText('100')).toBeInTheDocument() + expect(screen.getByText('20480MB')).toBeInTheDocument() + }) + }) + + describe('Pro/Team Plan Warning State', () => { + it('should show warning color when Professional plan usage approaches limit (80%+)', () => { + mockPlanType = Plan.professional + // 5120 MB * 80% = 4096 MB + mockVectorSpaceUsage = 4100 + + render() + + const progressBar = screen.getByTestId('billing-progress-bar') + expect(progressBar).toHaveClass('bg-components-progress-warning-progress') + }) + + it('should show warning color when Team plan usage approaches limit (80%+)', () => { + mockPlanType = Plan.team + // 20480 MB * 80% = 16384 MB + mockVectorSpaceUsage = 16500 + + render() + + const progressBar = screen.getByTestId('billing-progress-bar') + expect(progressBar).toHaveClass('bg-components-progress-warning-progress') + }) + }) + + describe('Pro/Team Plan Error State', () => { + it('should show error color when Professional plan usage exceeds limit', () => { + mockPlanType = Plan.professional + // Exceeds 5120 MB + mockVectorSpaceUsage = 5200 + + render() + + const progressBar = screen.getByTestId('billing-progress-bar') + expect(progressBar).toHaveClass('bg-components-progress-error-progress') + }) + + it('should show error color when Team plan usage exceeds limit', () => { + mockPlanType = Plan.team + // Exceeds 20480 MB + mockVectorSpaceUsage = 21000 + + render() + + const progressBar = screen.getByTestId('billing-progress-bar') + expect(progressBar).toHaveClass('bg-components-progress-error-progress') + }) + }) + + describe('Enterprise Plan (default case)', () => { + beforeEach(() => { + mockPlanType = Plan.enterprise + mockVectorSpaceUsage = 30 + // Enterprise plan uses total.vectorSpace from context + mockVectorSpaceTotal = 102400 // 100 GB = 102400 MB + }) + + it('should use total.vectorSpace from context for enterprise plan', () => { + render() + + // Enterprise plan should use the mockVectorSpaceTotal value (102400MB) + expect(screen.getByText('102400MB')).toBeInTheDocument() + }) + + it('should render indeterminate progress bar when usage is below threshold', () => { + render() + + expect(screen.getByTestId('billing-progress-bar-indeterminate')).toBeInTheDocument() + }) + + it('should render narrow indeterminate bar (not full width) for enterprise', () => { + render() + + const bar = screen.getByTestId('billing-progress-bar-indeterminate') + expect(bar).toHaveClass('w-[30px]') + expect(bar).not.toHaveClass('w-full') + }) + + it('should display "< 50 / total" format when below threshold', () => { + render() + + expect(screen.getByText(/< 50/)).toBeInTheDocument() + expect(screen.getByText('102400MB')).toBeInTheDocument() + }) + }) + + describe('Enterprise Plan Above Threshold', () => { + beforeEach(() => { + mockPlanType = Plan.enterprise + mockVectorSpaceUsage = 100 + mockVectorSpaceTotal = 102400 // 100 GB + }) + + it('should render normal progress bar when usage >= threshold', () => { + render() + + expect(screen.getByTestId('billing-progress-bar')).toBeInTheDocument() + expect(screen.queryByTestId('billing-progress-bar-indeterminate')).not.toBeInTheDocument() + }) + + it('should display actual usage when above threshold', () => { + render() + + expect(screen.getByText('100')).toBeInTheDocument() + expect(screen.getByText('102400MB')).toBeInTheDocument() + }) + }) +}) diff --git a/web/app/components/billing/usage-info/vector-space-info.tsx b/web/app/components/billing/usage-info/vector-space-info.tsx index 11e3a6a1ae..e384ef4d9a 100644 --- a/web/app/components/billing/usage-info/vector-space-info.tsx +++ b/web/app/components/billing/usage-info/vector-space-info.tsx @@ -1,26 +1,44 @@ 'use client' import type { FC } from 'react' +import type { BasicPlan } from '../type' import { RiHardDrive3Line, } from '@remixicon/react' import * as React from 'react' import { useTranslation } from 'react-i18next' import { useProviderContext } from '@/context/provider-context' +import { Plan } from '../type' import UsageInfo from '../usage-info' +import { getPlanVectorSpaceLimitMB } from '../utils' type Props = { className?: string } +// Storage threshold in MB - usage below this shows as "< 50 MB" +const STORAGE_THRESHOLD_MB = getPlanVectorSpaceLimitMB(Plan.sandbox) + const VectorSpaceInfo: FC = ({ className, }) => { const { t } = useTranslation() const { plan } = useProviderContext() const { + type, usage, total, } = plan + + // Determine total based on plan type (in MB), derived from ALL_PLANS config + const getTotalInMB = () => { + const planLimit = getPlanVectorSpaceLimitMB(type as BasicPlan) + // For known plans, use the config value; otherwise fall back to API response + return planLimit > 0 ? planLimit : total.vectorSpace + } + + const totalInMB = getTotalInMB() + const isSandbox = type === Plan.sandbox + return ( = ({ name={t('usagePage.vectorSpace', { ns: 'billing' })} tooltip={t('usagePage.vectorSpaceTooltip', { ns: 'billing' }) as string} usage={usage.vectorSpace} - total={total.vectorSpace} + total={totalInMB} unit="MB" unitPosition="inline" + storageMode + storageThreshold={STORAGE_THRESHOLD_MB} + storageTooltip={t('usagePage.storageThresholdTooltip', { ns: 'billing' }) as string} + isSandboxPlan={isSandbox} /> ) } diff --git a/web/app/components/billing/utils/index.ts b/web/app/components/billing/utils/index.ts index e7192ec351..39fc0cd7b5 100644 --- a/web/app/components/billing/utils/index.ts +++ b/web/app/components/billing/utils/index.ts @@ -1,7 +1,33 @@ -import type { BillingQuota, CurrentPlanInfoBackend } from '../type' +import type { BasicPlan, BillingQuota, CurrentPlanInfoBackend } from '../type' import dayjs from 'dayjs' import { ALL_PLANS, NUM_INFINITE } from '@/app/components/billing/config' +/** + * Parse vectorSpace string from ALL_PLANS config and convert to MB + * @example "50MB" -> 50, "5GB" -> 5120, "20GB" -> 20480 + */ +export const parseVectorSpaceToMB = (vectorSpace: string): number => { + const match = vectorSpace.match(/^(\d+)(MB|GB)$/i) + if (!match) + return 0 + + const value = Number.parseInt(match[1], 10) + const unit = match[2].toUpperCase() + + return unit === 'GB' ? value * 1024 : value +} + +/** + * Get the vector space limit in MB for a given plan type from ALL_PLANS config + */ +export const getPlanVectorSpaceLimitMB = (planType: BasicPlan): number => { + const planInfo = ALL_PLANS[planType] + if (!planInfo) + return 0 + + return parseVectorSpaceToMB(planInfo.vectorSpace) +} + const parseLimit = (limit: number) => { if (limit === 0) return NUM_INFINITE diff --git a/web/app/components/billing/vector-space-full/index.spec.tsx b/web/app/components/billing/vector-space-full/index.spec.tsx index 0382ec0872..375ac54c22 100644 --- a/web/app/components/billing/vector-space-full/index.spec.tsx +++ b/web/app/components/billing/vector-space-full/index.spec.tsx @@ -21,6 +21,18 @@ vi.mock('../upgrade-btn', () => ({ default: () => , })) +// Mock utils to control threshold and plan limits +vi.mock('../utils', () => ({ + getPlanVectorSpaceLimitMB: (planType: string) => { + // Return 5 for sandbox (threshold) and 100 for team + if (planType === 'sandbox') + return 5 + if (planType === 'team') + return 100 + return 0 + }, +})) + describe('VectorSpaceFull', () => { const planMock = { type: 'team', @@ -52,6 +64,6 @@ describe('VectorSpaceFull', () => { render() expect(screen.getByText('8')).toBeInTheDocument() - expect(screen.getByText('10MB')).toBeInTheDocument() + expect(screen.getByText('100MB')).toBeInTheDocument() }) }) diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index 4f274115c8..e430ea6739 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -1559,11 +1559,6 @@ "count": 3 } }, - "app/components/billing/usage-info/index.tsx": { - "ts/no-explicit-any": { - "count": 1 - } - }, "app/components/custom/custom-web-app-brand/index.spec.tsx": { "ts/no-explicit-any": { "count": 7 diff --git a/web/i18n/en-US/billing.json b/web/i18n/en-US/billing.json index 3242aa8e78..bfd82e1d67 100644 --- a/web/i18n/en-US/billing.json +++ b/web/i18n/en-US/billing.json @@ -172,6 +172,7 @@ "usagePage.documentsUploadQuota": "Documents Upload Quota", "usagePage.perMonth": "per month", "usagePage.resetsIn": "Resets in {{count,number}} days", + "usagePage.storageThresholdTooltip": "Detailed usage is shown once storage exceeds 50 MB.", "usagePage.teamMembers": "Team Members", "usagePage.triggerEvents": "Trigger Events", "usagePage.vectorSpace": "Knowledge Data Storage", diff --git a/web/i18n/ja-JP/billing.json b/web/i18n/ja-JP/billing.json index bf2f496428..fe38244f75 100644 --- a/web/i18n/ja-JP/billing.json +++ b/web/i18n/ja-JP/billing.json @@ -172,6 +172,7 @@ "usagePage.documentsUploadQuota": "ドキュメント・アップロード・クォータ", "usagePage.perMonth": "月あたり", "usagePage.resetsIn": "{{count,number}}日後にリセット", + "usagePage.storageThresholdTooltip": "ストレージ使用量が 50 MB を超えると、詳細な使用状況が表示されます。", "usagePage.teamMembers": "チームメンバー", "usagePage.triggerEvents": "トリガーイベント数", "usagePage.vectorSpace": "ナレッジベースのデータストレージ", diff --git a/web/i18n/zh-Hans/billing.json b/web/i18n/zh-Hans/billing.json index 6f976f620b..b3d06febc8 100644 --- a/web/i18n/zh-Hans/billing.json +++ b/web/i18n/zh-Hans/billing.json @@ -172,6 +172,7 @@ "usagePage.documentsUploadQuota": "文档上传配额", "usagePage.perMonth": "每月", "usagePage.resetsIn": "{{count,number}} 天后重置", + "usagePage.storageThresholdTooltip": "存储空间超过 50 MB 后,将显示详细使用情况。", "usagePage.teamMembers": "团队成员", "usagePage.triggerEvents": "触发器事件数", "usagePage.vectorSpace": "知识库数据存储空间", diff --git a/web/tailwind-common-config.ts b/web/tailwind-common-config.ts index 304be919fa..05aabfc2f1 100644 --- a/web/tailwind-common-config.ts +++ b/web/tailwind-common-config.ts @@ -139,6 +139,7 @@ const config = { 'billing-plan-card-premium-bg': 'var(--color-billing-plan-card-premium-bg)', 'billing-plan-card-enterprise-bg': 'var(--color-billing-plan-card-enterprise-bg)', 'knowledge-pipeline-creation-footer-bg': 'var(--color-knowledge-pipeline-creation-footer-bg)', + 'progress-bar-indeterminate-stripe': 'var(--color-progress-bar-indeterminate-stripe)', }, animation: { 'spin-slow': 'spin 2s linear infinite', diff --git a/web/themes/manual-dark.css b/web/themes/manual-dark.css index 867e2fe01d..eb33f93030 100644 --- a/web/themes/manual-dark.css +++ b/web/themes/manual-dark.css @@ -74,4 +74,5 @@ html[data-theme="dark"] { --color-billing-plan-card-premium-bg: linear-gradient(180deg, #F90 0%, rgba(255, 153, 0, 0.00) 100%); --color-billing-plan-card-enterprise-bg: linear-gradient(180deg, #03F 0%, rgba(0, 51, 255, 0.00) 100%); --color-knowledge-pipeline-creation-footer-bg: linear-gradient(90deg, rgba(34, 34, 37, 1) 4.89%, rgba(0, 0, 0, 0) 100%); + --color-progress-bar-indeterminate-stripe: repeating-linear-gradient(-55deg, #3A3A40, #3A3A40 2px, transparent 2px, transparent 5px); } diff --git a/web/themes/manual-light.css b/web/themes/manual-light.css index 3487153246..62ff4b2178 100644 --- a/web/themes/manual-light.css +++ b/web/themes/manual-light.css @@ -74,4 +74,5 @@ html[data-theme="light"] { --color-billing-plan-card-premium-bg: linear-gradient(180deg, #F90 0%, rgba(255, 153, 0, 0.00) 100%); --color-billing-plan-card-enterprise-bg: linear-gradient(180deg, #03F 0%, rgba(0, 51, 255, 0.00) 100%); --color-knowledge-pipeline-creation-footer-bg: linear-gradient(90deg, #FCFCFD 4.89%, rgba(255, 255, 255, 0.00) 100%); + --color-progress-bar-indeterminate-stripe: repeating-linear-gradient(-55deg, #D0D5DD, #D0D5DD 2px, transparent 2px, transparent 5px); } From e80d76af15c1be25213139cf3b2bf9289e180ac4 Mon Sep 17 00:00:00 2001 From: hj24 Date: Wed, 21 Jan 2026 14:06:35 +0800 Subject: [PATCH 2/4] feat: add lock for retention jobs (#31320) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/.env.example | 1 + api/configs/feature/__init__.py | 4 ++ api/schedule/clean_messages.py | 28 ++++++++--- api/schedule/clean_workflow_runs_task.py | 64 ++++++++++++++++++------ docker/.env.example | 1 + docker/docker-compose.yaml | 1 + 6 files changed, 79 insertions(+), 20 deletions(-) diff --git a/api/.env.example b/api/.env.example index 15981c14b8..c3b1474549 100644 --- a/api/.env.example +++ b/api/.env.example @@ -715,4 +715,5 @@ ANNOTATION_IMPORT_MAX_CONCURRENT=5 SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD=21 SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_SIZE=1000 SANDBOX_EXPIRED_RECORDS_RETENTION_DAYS=30 +SANDBOX_EXPIRED_RECORDS_CLEAN_TASK_LOCK_TTL=90000 diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index cf71a33fa8..03aff7e6b5 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -1298,6 +1298,10 @@ class SandboxExpiredRecordsCleanConfig(BaseSettings): description="Retention days for sandbox expired workflow_run records and message records", default=30, ) + SANDBOX_EXPIRED_RECORDS_CLEAN_TASK_LOCK_TTL: PositiveInt = Field( + description="Lock TTL for sandbox expired records clean task in seconds", + default=90000, + ) class FeatureConfig( diff --git a/api/schedule/clean_messages.py b/api/schedule/clean_messages.py index e85bba8823..be5f483b95 100644 --- a/api/schedule/clean_messages.py +++ b/api/schedule/clean_messages.py @@ -2,9 +2,11 @@ import logging import time import click +from redis.exceptions import LockError import app from configs import dify_config +from extensions.ext_redis import redis_client from services.retention.conversation.messages_clean_policy import create_message_clean_policy from services.retention.conversation.messages_clean_service import MessagesCleanService @@ -31,12 +33,16 @@ def clean_messages(): ) # Create and run the cleanup service - service = MessagesCleanService.from_days( - policy=policy, - days=dify_config.SANDBOX_EXPIRED_RECORDS_RETENTION_DAYS, - batch_size=dify_config.SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_SIZE, - ) - stats = service.run() + # lock the task to avoid concurrent execution in case of the future data volume growth + with redis_client.lock( + "retention:clean_messages", timeout=dify_config.SANDBOX_EXPIRED_RECORDS_CLEAN_TASK_LOCK_TTL, blocking=False + ): + service = MessagesCleanService.from_days( + policy=policy, + days=dify_config.SANDBOX_EXPIRED_RECORDS_RETENTION_DAYS, + batch_size=dify_config.SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_SIZE, + ) + stats = service.run() end_at = time.perf_counter() click.echo( @@ -50,6 +56,16 @@ def clean_messages(): fg="green", ) ) + except LockError: + end_at = time.perf_counter() + logger.exception("clean_messages: acquire task lock failed, skip current execution") + click.echo( + click.style( + f"clean_messages: skipped (lock already held) - latency: {end_at - start_at:.2f}s", + fg="yellow", + ) + ) + raise except Exception as e: end_at = time.perf_counter() logger.exception("clean_messages failed") diff --git a/api/schedule/clean_workflow_runs_task.py b/api/schedule/clean_workflow_runs_task.py index 9f5bf8e150..ff45a3ddf2 100644 --- a/api/schedule/clean_workflow_runs_task.py +++ b/api/schedule/clean_workflow_runs_task.py @@ -1,11 +1,16 @@ +import logging from datetime import UTC, datetime import click +from redis.exceptions import LockError import app from configs import dify_config +from extensions.ext_redis import redis_client from services.retention.workflow_run.clear_free_plan_expired_workflow_run_logs import WorkflowRunCleanup +logger = logging.getLogger(__name__) + @app.celery.task(queue="retention") def clean_workflow_runs_task() -> None: @@ -25,19 +30,50 @@ def clean_workflow_runs_task() -> None: start_time = datetime.now(UTC) - WorkflowRunCleanup( - days=dify_config.SANDBOX_EXPIRED_RECORDS_RETENTION_DAYS, - batch_size=dify_config.SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_SIZE, - start_from=None, - end_before=None, - ).run() + try: + # lock the task to avoid concurrent execution in case of the future data volume growth + with redis_client.lock( + "retention:clean_workflow_runs_task", + timeout=dify_config.SANDBOX_EXPIRED_RECORDS_CLEAN_TASK_LOCK_TTL, + blocking=False, + ): + WorkflowRunCleanup( + days=dify_config.SANDBOX_EXPIRED_RECORDS_RETENTION_DAYS, + batch_size=dify_config.SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_SIZE, + start_from=None, + end_before=None, + ).run() - end_time = datetime.now(UTC) - elapsed = end_time - start_time - click.echo( - click.style( - f"Scheduled workflow run cleanup finished. start={start_time.isoformat()} " - f"end={end_time.isoformat()} duration={elapsed}", - fg="green", + end_time = datetime.now(UTC) + elapsed = end_time - start_time + click.echo( + click.style( + f"Scheduled workflow run cleanup finished. start={start_time.isoformat()} " + f"end={end_time.isoformat()} duration={elapsed}", + fg="green", + ) ) - ) + except LockError: + end_time = datetime.now(UTC) + elapsed = end_time - start_time + logger.exception("clean_workflow_runs_task: acquire task lock failed, skip current execution") + click.echo( + click.style( + f"Scheduled workflow run cleanup skipped (lock already held). " + f"start={start_time.isoformat()} end={end_time.isoformat()} duration={elapsed}", + fg="yellow", + ) + ) + raise + except Exception as e: + end_time = datetime.now(UTC) + elapsed = end_time - start_time + logger.exception("clean_workflow_runs_task failed") + click.echo( + click.style( + f"Scheduled workflow run cleanup failed. start={start_time.isoformat()} " + f"end={end_time.isoformat()} duration={elapsed} - {str(e)}", + fg="red", + ) + ) + raise diff --git a/docker/.env.example b/docker/.env.example index 627a3a23da..c7246ae11f 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -1518,3 +1518,4 @@ AMPLITUDE_API_KEY= SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD=21 SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_SIZE=1000 SANDBOX_EXPIRED_RECORDS_RETENTION_DAYS=30 +SANDBOX_EXPIRED_RECORDS_CLEAN_TASK_LOCK_TTL=90000 diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 429667e75f..902ca3103c 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -682,6 +682,7 @@ x-shared-env: &shared-api-worker-env SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD: ${SANDBOX_EXPIRED_RECORDS_CLEAN_GRACEFUL_PERIOD:-21} SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_SIZE: ${SANDBOX_EXPIRED_RECORDS_CLEAN_BATCH_SIZE:-1000} SANDBOX_EXPIRED_RECORDS_RETENTION_DAYS: ${SANDBOX_EXPIRED_RECORDS_RETENTION_DAYS:-30} + SANDBOX_EXPIRED_RECORDS_CLEAN_TASK_LOCK_TTL: ${SANDBOX_EXPIRED_RECORDS_CLEAN_TASK_LOCK_TTL:-90000} services: # Init container to fix permissions From 34436fc89c8150d72ee0bb85a2235d39182b8714 Mon Sep 17 00:00:00 2001 From: wangxiaolei Date: Wed, 21 Jan 2026 14:31:47 +0800 Subject: [PATCH 3/4] feat: workflow support register context and read context (#31265) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Maries --- api/context/flask_app_context.py | 2 +- api/core/workflow/context/__init__.py | 12 +++ .../workflow/context/execution_context.py | 76 +++++++++++++++++-- api/core/workflow/context/models.py | 13 ++++ .../context/test_execution_context.py | 31 ++++++++ 5 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 api/core/workflow/context/models.py diff --git a/api/context/flask_app_context.py b/api/context/flask_app_context.py index 4b693cd91f..360be16beb 100644 --- a/api/context/flask_app_context.py +++ b/api/context/flask_app_context.py @@ -9,7 +9,7 @@ from typing import Any, final from flask import Flask, current_app, g -from context import register_context_capturer +from core.workflow.context import register_context_capturer from core.workflow.context.execution_context import ( AppContext, IExecutionContext, diff --git a/api/core/workflow/context/__init__.py b/api/core/workflow/context/__init__.py index 31e1f2c8d9..1237d6a017 100644 --- a/api/core/workflow/context/__init__.py +++ b/api/core/workflow/context/__init__.py @@ -7,16 +7,28 @@ execution in multi-threaded environments. from core.workflow.context.execution_context import ( AppContext, + ContextProviderNotFoundError, ExecutionContext, IExecutionContext, NullAppContext, capture_current_context, + read_context, + register_context, + register_context_capturer, + reset_context_provider, ) +from core.workflow.context.models import SandboxContext __all__ = [ "AppContext", + "ContextProviderNotFoundError", "ExecutionContext", "IExecutionContext", "NullAppContext", + "SandboxContext", "capture_current_context", + "read_context", + "register_context", + "register_context_capturer", + "reset_context_provider", ] diff --git a/api/core/workflow/context/execution_context.py b/api/core/workflow/context/execution_context.py index 5a4203be93..d951c95d68 100644 --- a/api/core/workflow/context/execution_context.py +++ b/api/core/workflow/context/execution_context.py @@ -4,9 +4,11 @@ Execution Context - Abstracted context management for workflow execution. import contextvars from abc import ABC, abstractmethod -from collections.abc import Generator +from collections.abc import Callable, Generator from contextlib import AbstractContextManager, contextmanager -from typing import Any, Protocol, final, runtime_checkable +from typing import Any, Protocol, TypeVar, final, runtime_checkable + +from pydantic import BaseModel class AppContext(ABC): @@ -204,13 +206,75 @@ class ExecutionContextBuilder: ) +_capturer: Callable[[], IExecutionContext] | None = None + +# Tenant-scoped providers using tuple keys for clarity and constant-time lookup. +# Key mapping: +# (name, tenant_id) -> provider +# - name: namespaced identifier (recommend prefixing, e.g. "workflow.sandbox") +# - tenant_id: tenant identifier string +# Value: +# provider: Callable[[], BaseModel] returning the typed context value +# Type-safety note: +# - This registry cannot enforce that all providers for a given name return the same BaseModel type. +# - Implementors SHOULD provide typed wrappers around register/read (like Go's context best practice), +# e.g. def register_sandbox_ctx(tenant_id: str, p: Callable[[], SandboxContext]) and +# def read_sandbox_ctx(tenant_id: str) -> SandboxContext. +_tenant_context_providers: dict[tuple[str, str], Callable[[], BaseModel]] = {} + +T = TypeVar("T", bound=BaseModel) + + +class ContextProviderNotFoundError(KeyError): + """Raised when a tenant-scoped context provider is missing for a given (name, tenant_id).""" + + pass + + +def register_context_capturer(capturer: Callable[[], IExecutionContext]) -> None: + """Register a single enterable execution context capturer (e.g., Flask).""" + global _capturer + _capturer = capturer + + +def register_context(name: str, tenant_id: str, provider: Callable[[], BaseModel]) -> None: + """Register a tenant-specific provider for a named context. + + Tip: use a namespaced "name" (e.g., "workflow.sandbox") to avoid key collisions. + Consider adding a typed wrapper for this registration in your feature module. + """ + _tenant_context_providers[(name, tenant_id)] = provider + + +def read_context(name: str, *, tenant_id: str) -> BaseModel: + """ + Read a context value for a specific tenant. + + Raises KeyError if the provider for (name, tenant_id) is not registered. + """ + prov = _tenant_context_providers.get((name, tenant_id)) + if prov is None: + raise ContextProviderNotFoundError(f"Context provider '{name}' not registered for tenant '{tenant_id}'") + return prov() + + def capture_current_context() -> IExecutionContext: """ Capture current execution context from the calling environment. - Returns: - IExecutionContext with captured context + If a capturer is registered (e.g., Flask), use it. Otherwise, return a minimal + context with NullAppContext + copy of current contextvars. """ - from context import capture_current_context + if _capturer is None: + return ExecutionContext( + app_context=NullAppContext(), + context_vars=contextvars.copy_context(), + ) + return _capturer() - return capture_current_context() + +def reset_context_provider() -> None: + """Reset the capturer and all tenant-scoped context providers (primarily for tests).""" + global _capturer + _capturer = None + _tenant_context_providers.clear() diff --git a/api/core/workflow/context/models.py b/api/core/workflow/context/models.py new file mode 100644 index 0000000000..af5a4b2614 --- /dev/null +++ b/api/core/workflow/context/models.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from pydantic import AnyHttpUrl, BaseModel + + +class SandboxContext(BaseModel): + """Typed context for sandbox integration. All fields optional by design.""" + + sandbox_url: AnyHttpUrl | None = None + sandbox_token: str | None = None # optional, if later needed for auth + + +__all__ = ["SandboxContext"] diff --git a/api/tests/unit_tests/core/workflow/context/test_execution_context.py b/api/tests/unit_tests/core/workflow/context/test_execution_context.py index 217c39385c..63466cfb5e 100644 --- a/api/tests/unit_tests/core/workflow/context/test_execution_context.py +++ b/api/tests/unit_tests/core/workflow/context/test_execution_context.py @@ -5,6 +5,7 @@ from typing import Any from unittest.mock import MagicMock import pytest +from pydantic import BaseModel from core.workflow.context.execution_context import ( AppContext, @@ -12,6 +13,8 @@ from core.workflow.context.execution_context import ( ExecutionContextBuilder, IExecutionContext, NullAppContext, + read_context, + register_context, ) @@ -256,3 +259,31 @@ class TestCaptureCurrentContext: # Context variables should be captured assert result.context_vars is not None + + +class TestTenantScopedContextRegistry: + def setup_method(self): + from core.workflow.context import reset_context_provider + + reset_context_provider() + + def teardown_method(self): + from core.workflow.context import reset_context_provider + + reset_context_provider() + + def test_tenant_provider_read_ok(self): + class SandboxContext(BaseModel): + base_url: str | None = None + + register_context("workflow.sandbox", "t1", lambda: SandboxContext(base_url="http://t1")) + register_context("workflow.sandbox", "t2", lambda: SandboxContext(base_url="http://t2")) + + assert read_context("workflow.sandbox", tenant_id="t1").base_url == "http://t1" + assert read_context("workflow.sandbox", tenant_id="t2").base_url == "http://t2" + + def test_missing_provider_raises_keyerror(self): + from core.workflow.context import ContextProviderNotFoundError + + with pytest.raises(ContextProviderNotFoundError): + read_context("missing", tenant_id="unknown") From 4b068022e1c746f87a7f3867fb6e8126458b0fee Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:48:58 +0800 Subject: [PATCH 4/4] chore: reorganize agent skills and add web design skills for all agents (#31334) --- .agent/skills | 1 - .agent/skills/component-refactoring | 1 + .agent/skills/frontend-code-review | 1 + .agent/skills/frontend-testing | 1 + .agent/skills/orpc-contract-first | 1 + .agent/skills/skill-creator | 1 + .agent/skills/vercel-react-best-practices | 1 + .agent/skills/web-design-guidelines | 1 + .../skills/component-refactoring/SKILL.md | 0 .../references/complexity-patterns.md | 0 .../references/component-splitting.md | 0 .../references/hook-extraction.md | 0 .../skills/frontend-code-review/SKILL.md | 0 .../references/business-logic.md | 0 .../references/code-quality.md | 0 .../references/performance.md | 0 .../skills/frontend-testing/SKILL.md | 0 .../assets/component-test.template.tsx | 0 .../assets/hook-test.template.ts | 0 .../assets/utility-test.template.ts | 0 .../references/async-testing.md | 0 .../frontend-testing/references/checklist.md | 0 .../references/common-patterns.md | 0 .../references/domain-components.md | 0 .../frontend-testing/references/mocking.md | 0 .../frontend-testing/references/workflow.md | 0 .../skills/orpc-contract-first/SKILL.md | 0 .../skills/skill-creator/SKILL.md | 0 .../references/output-patterns.md | 0 .../skill-creator/references/workflows.md | 0 .../skill-creator/scripts/init_skill.py | 0 .../skill-creator/scripts/package_skill.py | 0 .../skill-creator/scripts/quick_validate.py | 0 .../vercel-react-best-practices/AGENTS.md | 0 .../vercel-react-best-practices/SKILL.md | 0 .../rules/advanced-event-handler-refs.md | 0 .../rules/advanced-use-latest.md | 0 .../rules/async-api-routes.md | 0 .../rules/async-defer-await.md | 0 .../rules/async-dependencies.md | 0 .../rules/async-parallel.md | 0 .../rules/async-suspense-boundaries.md | 0 .../rules/bundle-barrel-imports.md | 0 .../rules/bundle-conditional.md | 0 .../rules/bundle-defer-third-party.md | 0 .../rules/bundle-dynamic-imports.md | 0 .../rules/bundle-preload.md | 0 .../rules/client-event-listeners.md | 0 .../rules/client-localstorage-schema.md | 0 .../rules/client-passive-event-listeners.md | 0 .../rules/client-swr-dedup.md | 0 .../rules/js-batch-dom-css.md | 0 .../rules/js-cache-function-results.md | 0 .../rules/js-cache-property-access.md | 0 .../rules/js-cache-storage.md | 0 .../rules/js-combine-iterations.md | 0 .../rules/js-early-exit.md | 0 .../rules/js-hoist-regexp.md | 0 .../rules/js-index-maps.md | 0 .../rules/js-length-check-first.md | 0 .../rules/js-min-max-loop.md | 0 .../rules/js-set-map-lookups.md | 0 .../rules/js-tosorted-immutable.md | 0 .../rules/rendering-activity.md | 0 .../rules/rendering-animate-svg-wrapper.md | 0 .../rules/rendering-conditional-render.md | 0 .../rules/rendering-content-visibility.md | 0 .../rules/rendering-hoist-jsx.md | 0 .../rules/rendering-hydration-no-flicker.md | 0 .../rules/rendering-svg-precision.md | 0 .../rules/rerender-defer-reads.md | 0 .../rules/rerender-dependencies.md | 0 .../rules/rerender-derived-state.md | 0 .../rules/rerender-functional-setstate.md | 0 .../rules/rerender-lazy-state-init.md | 0 .../rules/rerender-memo.md | 0 .../rules/rerender-transitions.md | 0 .../rules/server-after-nonblocking.md | 0 .../rules/server-cache-lru.md | 0 .../rules/server-cache-react.md | 0 .../rules/server-parallel-fetching.md | 0 .../rules/server-serialization.md | 0 .agents/skills/web-design-guidelines/SKILL.md | 39 ++++++++ .claude/skills/component-refactoring | 1 + .claude/skills/frontend-code-review | 1 + .claude/skills/frontend-testing | 1 + .claude/skills/orpc-contract-first | 1 + .claude/skills/skill-creator | 1 + .claude/skills/vercel-react-best-practices | 1 + .claude/skills/web-design-guidelines | 1 + .codex/skills | 1 - .codex/skills/component-refactoring | 1 + .codex/skills/frontend-code-review | 1 + .codex/skills/frontend-testing | 1 + .codex/skills/orpc-contract-first | 1 + .codex/skills/skill-creator | 1 + .codex/skills/vercel-react-best-practices | 1 + .codex/skills/web-design-guidelines | 1 + .cursor/skills/component-refactoring | 1 + .cursor/skills/frontend-code-review | 1 + .cursor/skills/frontend-testing | 1 + .cursor/skills/orpc-contract-first | 1 + .cursor/skills/skill-creator | 1 + .cursor/skills/vercel-react-best-practices | 1 + .cursor/skills/web-design-guidelines | 1 + .gemini/skills/component-refactoring | 1 + .gemini/skills/frontend-code-review | 1 + .gemini/skills/frontend-testing | 1 + .gemini/skills/orpc-contract-first | 1 + .gemini/skills/skill-creator | 1 + .gemini/skills/vercel-react-best-practices | 1 + .gemini/skills/web-design-guidelines | 1 + .github/skills/component-refactoring | 1 + .github/skills/frontend-code-review | 1 + .github/skills/frontend-testing | 1 + .github/skills/orpc-contract-first | 1 + .github/skills/skill-creator | 1 + .github/skills/vercel-react-best-practices | 1 + .github/skills/web-design-guidelines | 1 + .github/workflows/autofix.yml | 2 +- .../console/datasets/datasets_document.py.md | 52 ---------- .../services/dataset_service.py.md | 18 ---- api/agent-notes/services/file_service.py.md | 35 ------- .../test_datasets_document_download.py.md | 28 ------ .../test_file_service_zip_and_lookup.py.md | 18 ---- api/agent_skills/infra.md | 96 ------------------- api/agent_skills/plugin.md | 1 - api/agent_skills/plugin_oauth.md | 1 - api/agent_skills/trigger.md | 53 ---------- 129 files changed, 82 insertions(+), 305 deletions(-) delete mode 120000 .agent/skills create mode 120000 .agent/skills/component-refactoring create mode 120000 .agent/skills/frontend-code-review create mode 120000 .agent/skills/frontend-testing create mode 120000 .agent/skills/orpc-contract-first create mode 120000 .agent/skills/skill-creator create mode 120000 .agent/skills/vercel-react-best-practices create mode 120000 .agent/skills/web-design-guidelines rename {.claude => .agents}/skills/component-refactoring/SKILL.md (100%) rename {.claude => .agents}/skills/component-refactoring/references/complexity-patterns.md (100%) rename {.claude => .agents}/skills/component-refactoring/references/component-splitting.md (100%) rename {.claude => .agents}/skills/component-refactoring/references/hook-extraction.md (100%) rename {.claude => .agents}/skills/frontend-code-review/SKILL.md (100%) rename {.claude => .agents}/skills/frontend-code-review/references/business-logic.md (100%) rename {.claude => .agents}/skills/frontend-code-review/references/code-quality.md (100%) rename {.claude => .agents}/skills/frontend-code-review/references/performance.md (100%) rename {.claude => .agents}/skills/frontend-testing/SKILL.md (100%) rename {.claude => .agents}/skills/frontend-testing/assets/component-test.template.tsx (100%) rename {.claude => .agents}/skills/frontend-testing/assets/hook-test.template.ts (100%) rename {.claude => .agents}/skills/frontend-testing/assets/utility-test.template.ts (100%) rename {.claude => .agents}/skills/frontend-testing/references/async-testing.md (100%) rename {.claude => .agents}/skills/frontend-testing/references/checklist.md (100%) rename {.claude => .agents}/skills/frontend-testing/references/common-patterns.md (100%) rename {.claude => .agents}/skills/frontend-testing/references/domain-components.md (100%) rename {.claude => .agents}/skills/frontend-testing/references/mocking.md (100%) rename {.claude => .agents}/skills/frontend-testing/references/workflow.md (100%) rename {.claude => .agents}/skills/orpc-contract-first/SKILL.md (100%) rename {.claude => .agents}/skills/skill-creator/SKILL.md (100%) rename {.claude => .agents}/skills/skill-creator/references/output-patterns.md (100%) rename {.claude => .agents}/skills/skill-creator/references/workflows.md (100%) rename {.claude => .agents}/skills/skill-creator/scripts/init_skill.py (100%) rename {.claude => .agents}/skills/skill-creator/scripts/package_skill.py (100%) rename {.claude => .agents}/skills/skill-creator/scripts/quick_validate.py (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/AGENTS.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/SKILL.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/advanced-use-latest.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/async-api-routes.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/async-defer-await.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/async-dependencies.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/async-parallel.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/bundle-conditional.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/bundle-preload.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/client-event-listeners.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/client-localstorage-schema.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/client-swr-dedup.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/js-batch-dom-css.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/js-cache-function-results.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/js-cache-property-access.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/js-cache-storage.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/js-combine-iterations.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/js-early-exit.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/js-hoist-regexp.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/js-index-maps.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/js-length-check-first.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/js-min-max-loop.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/js-set-map-lookups.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/rendering-activity.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/rendering-conditional-render.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/rendering-content-visibility.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/rendering-svg-precision.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/rerender-defer-reads.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/rerender-dependencies.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/rerender-derived-state.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/rerender-memo.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/rerender-transitions.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/server-after-nonblocking.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/server-cache-lru.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/server-cache-react.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/server-parallel-fetching.md (100%) rename {.claude => .agents}/skills/vercel-react-best-practices/rules/server-serialization.md (100%) create mode 100644 .agents/skills/web-design-guidelines/SKILL.md create mode 120000 .claude/skills/component-refactoring create mode 120000 .claude/skills/frontend-code-review create mode 120000 .claude/skills/frontend-testing create mode 120000 .claude/skills/orpc-contract-first create mode 120000 .claude/skills/skill-creator create mode 120000 .claude/skills/vercel-react-best-practices create mode 120000 .claude/skills/web-design-guidelines delete mode 120000 .codex/skills create mode 120000 .codex/skills/component-refactoring create mode 120000 .codex/skills/frontend-code-review create mode 120000 .codex/skills/frontend-testing create mode 120000 .codex/skills/orpc-contract-first create mode 120000 .codex/skills/skill-creator create mode 120000 .codex/skills/vercel-react-best-practices create mode 120000 .codex/skills/web-design-guidelines create mode 120000 .cursor/skills/component-refactoring create mode 120000 .cursor/skills/frontend-code-review create mode 120000 .cursor/skills/frontend-testing create mode 120000 .cursor/skills/orpc-contract-first create mode 120000 .cursor/skills/skill-creator create mode 120000 .cursor/skills/vercel-react-best-practices create mode 120000 .cursor/skills/web-design-guidelines create mode 120000 .gemini/skills/component-refactoring create mode 120000 .gemini/skills/frontend-code-review create mode 120000 .gemini/skills/frontend-testing create mode 120000 .gemini/skills/orpc-contract-first create mode 120000 .gemini/skills/skill-creator create mode 120000 .gemini/skills/vercel-react-best-practices create mode 120000 .gemini/skills/web-design-guidelines create mode 120000 .github/skills/component-refactoring create mode 120000 .github/skills/frontend-code-review create mode 120000 .github/skills/frontend-testing create mode 120000 .github/skills/orpc-contract-first create mode 120000 .github/skills/skill-creator create mode 120000 .github/skills/vercel-react-best-practices create mode 120000 .github/skills/web-design-guidelines delete mode 100644 api/agent-notes/controllers/console/datasets/datasets_document.py.md delete mode 100644 api/agent-notes/services/dataset_service.py.md delete mode 100644 api/agent-notes/services/file_service.py.md delete mode 100644 api/agent-notes/tests/unit_tests/controllers/console/datasets/test_datasets_document_download.py.md delete mode 100644 api/agent-notes/tests/unit_tests/services/test_file_service_zip_and_lookup.py.md delete mode 100644 api/agent_skills/infra.md delete mode 100644 api/agent_skills/plugin.md delete mode 100644 api/agent_skills/plugin_oauth.md delete mode 100644 api/agent_skills/trigger.md diff --git a/.agent/skills b/.agent/skills deleted file mode 120000 index 454b8427cd..0000000000 --- a/.agent/skills +++ /dev/null @@ -1 +0,0 @@ -../.claude/skills \ No newline at end of file diff --git a/.agent/skills/component-refactoring b/.agent/skills/component-refactoring new file mode 120000 index 0000000000..53ae67e2f2 --- /dev/null +++ b/.agent/skills/component-refactoring @@ -0,0 +1 @@ +../../.agents/skills/component-refactoring \ No newline at end of file diff --git a/.agent/skills/frontend-code-review b/.agent/skills/frontend-code-review new file mode 120000 index 0000000000..55654ffbd7 --- /dev/null +++ b/.agent/skills/frontend-code-review @@ -0,0 +1 @@ +../../.agents/skills/frontend-code-review \ No newline at end of file diff --git a/.agent/skills/frontend-testing b/.agent/skills/frontend-testing new file mode 120000 index 0000000000..092cec7745 --- /dev/null +++ b/.agent/skills/frontend-testing @@ -0,0 +1 @@ +../../.agents/skills/frontend-testing \ No newline at end of file diff --git a/.agent/skills/orpc-contract-first b/.agent/skills/orpc-contract-first new file mode 120000 index 0000000000..da47b335c7 --- /dev/null +++ b/.agent/skills/orpc-contract-first @@ -0,0 +1 @@ +../../.agents/skills/orpc-contract-first \ No newline at end of file diff --git a/.agent/skills/skill-creator b/.agent/skills/skill-creator new file mode 120000 index 0000000000..b87455490f --- /dev/null +++ b/.agent/skills/skill-creator @@ -0,0 +1 @@ +../../.agents/skills/skill-creator \ No newline at end of file diff --git a/.agent/skills/vercel-react-best-practices b/.agent/skills/vercel-react-best-practices new file mode 120000 index 0000000000..e567923b32 --- /dev/null +++ b/.agent/skills/vercel-react-best-practices @@ -0,0 +1 @@ +../../.agents/skills/vercel-react-best-practices \ No newline at end of file diff --git a/.agent/skills/web-design-guidelines b/.agent/skills/web-design-guidelines new file mode 120000 index 0000000000..886b26ded7 --- /dev/null +++ b/.agent/skills/web-design-guidelines @@ -0,0 +1 @@ +../../.agents/skills/web-design-guidelines \ No newline at end of file diff --git a/.claude/skills/component-refactoring/SKILL.md b/.agents/skills/component-refactoring/SKILL.md similarity index 100% rename from .claude/skills/component-refactoring/SKILL.md rename to .agents/skills/component-refactoring/SKILL.md diff --git a/.claude/skills/component-refactoring/references/complexity-patterns.md b/.agents/skills/component-refactoring/references/complexity-patterns.md similarity index 100% rename from .claude/skills/component-refactoring/references/complexity-patterns.md rename to .agents/skills/component-refactoring/references/complexity-patterns.md diff --git a/.claude/skills/component-refactoring/references/component-splitting.md b/.agents/skills/component-refactoring/references/component-splitting.md similarity index 100% rename from .claude/skills/component-refactoring/references/component-splitting.md rename to .agents/skills/component-refactoring/references/component-splitting.md diff --git a/.claude/skills/component-refactoring/references/hook-extraction.md b/.agents/skills/component-refactoring/references/hook-extraction.md similarity index 100% rename from .claude/skills/component-refactoring/references/hook-extraction.md rename to .agents/skills/component-refactoring/references/hook-extraction.md diff --git a/.claude/skills/frontend-code-review/SKILL.md b/.agents/skills/frontend-code-review/SKILL.md similarity index 100% rename from .claude/skills/frontend-code-review/SKILL.md rename to .agents/skills/frontend-code-review/SKILL.md diff --git a/.claude/skills/frontend-code-review/references/business-logic.md b/.agents/skills/frontend-code-review/references/business-logic.md similarity index 100% rename from .claude/skills/frontend-code-review/references/business-logic.md rename to .agents/skills/frontend-code-review/references/business-logic.md diff --git a/.claude/skills/frontend-code-review/references/code-quality.md b/.agents/skills/frontend-code-review/references/code-quality.md similarity index 100% rename from .claude/skills/frontend-code-review/references/code-quality.md rename to .agents/skills/frontend-code-review/references/code-quality.md diff --git a/.claude/skills/frontend-code-review/references/performance.md b/.agents/skills/frontend-code-review/references/performance.md similarity index 100% rename from .claude/skills/frontend-code-review/references/performance.md rename to .agents/skills/frontend-code-review/references/performance.md diff --git a/.claude/skills/frontend-testing/SKILL.md b/.agents/skills/frontend-testing/SKILL.md similarity index 100% rename from .claude/skills/frontend-testing/SKILL.md rename to .agents/skills/frontend-testing/SKILL.md diff --git a/.claude/skills/frontend-testing/assets/component-test.template.tsx b/.agents/skills/frontend-testing/assets/component-test.template.tsx similarity index 100% rename from .claude/skills/frontend-testing/assets/component-test.template.tsx rename to .agents/skills/frontend-testing/assets/component-test.template.tsx diff --git a/.claude/skills/frontend-testing/assets/hook-test.template.ts b/.agents/skills/frontend-testing/assets/hook-test.template.ts similarity index 100% rename from .claude/skills/frontend-testing/assets/hook-test.template.ts rename to .agents/skills/frontend-testing/assets/hook-test.template.ts diff --git a/.claude/skills/frontend-testing/assets/utility-test.template.ts b/.agents/skills/frontend-testing/assets/utility-test.template.ts similarity index 100% rename from .claude/skills/frontend-testing/assets/utility-test.template.ts rename to .agents/skills/frontend-testing/assets/utility-test.template.ts diff --git a/.claude/skills/frontend-testing/references/async-testing.md b/.agents/skills/frontend-testing/references/async-testing.md similarity index 100% rename from .claude/skills/frontend-testing/references/async-testing.md rename to .agents/skills/frontend-testing/references/async-testing.md diff --git a/.claude/skills/frontend-testing/references/checklist.md b/.agents/skills/frontend-testing/references/checklist.md similarity index 100% rename from .claude/skills/frontend-testing/references/checklist.md rename to .agents/skills/frontend-testing/references/checklist.md diff --git a/.claude/skills/frontend-testing/references/common-patterns.md b/.agents/skills/frontend-testing/references/common-patterns.md similarity index 100% rename from .claude/skills/frontend-testing/references/common-patterns.md rename to .agents/skills/frontend-testing/references/common-patterns.md diff --git a/.claude/skills/frontend-testing/references/domain-components.md b/.agents/skills/frontend-testing/references/domain-components.md similarity index 100% rename from .claude/skills/frontend-testing/references/domain-components.md rename to .agents/skills/frontend-testing/references/domain-components.md diff --git a/.claude/skills/frontend-testing/references/mocking.md b/.agents/skills/frontend-testing/references/mocking.md similarity index 100% rename from .claude/skills/frontend-testing/references/mocking.md rename to .agents/skills/frontend-testing/references/mocking.md diff --git a/.claude/skills/frontend-testing/references/workflow.md b/.agents/skills/frontend-testing/references/workflow.md similarity index 100% rename from .claude/skills/frontend-testing/references/workflow.md rename to .agents/skills/frontend-testing/references/workflow.md diff --git a/.claude/skills/orpc-contract-first/SKILL.md b/.agents/skills/orpc-contract-first/SKILL.md similarity index 100% rename from .claude/skills/orpc-contract-first/SKILL.md rename to .agents/skills/orpc-contract-first/SKILL.md diff --git a/.claude/skills/skill-creator/SKILL.md b/.agents/skills/skill-creator/SKILL.md similarity index 100% rename from .claude/skills/skill-creator/SKILL.md rename to .agents/skills/skill-creator/SKILL.md diff --git a/.claude/skills/skill-creator/references/output-patterns.md b/.agents/skills/skill-creator/references/output-patterns.md similarity index 100% rename from .claude/skills/skill-creator/references/output-patterns.md rename to .agents/skills/skill-creator/references/output-patterns.md diff --git a/.claude/skills/skill-creator/references/workflows.md b/.agents/skills/skill-creator/references/workflows.md similarity index 100% rename from .claude/skills/skill-creator/references/workflows.md rename to .agents/skills/skill-creator/references/workflows.md diff --git a/.claude/skills/skill-creator/scripts/init_skill.py b/.agents/skills/skill-creator/scripts/init_skill.py similarity index 100% rename from .claude/skills/skill-creator/scripts/init_skill.py rename to .agents/skills/skill-creator/scripts/init_skill.py diff --git a/.claude/skills/skill-creator/scripts/package_skill.py b/.agents/skills/skill-creator/scripts/package_skill.py similarity index 100% rename from .claude/skills/skill-creator/scripts/package_skill.py rename to .agents/skills/skill-creator/scripts/package_skill.py diff --git a/.claude/skills/skill-creator/scripts/quick_validate.py b/.agents/skills/skill-creator/scripts/quick_validate.py similarity index 100% rename from .claude/skills/skill-creator/scripts/quick_validate.py rename to .agents/skills/skill-creator/scripts/quick_validate.py diff --git a/.claude/skills/vercel-react-best-practices/AGENTS.md b/.agents/skills/vercel-react-best-practices/AGENTS.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/AGENTS.md rename to .agents/skills/vercel-react-best-practices/AGENTS.md diff --git a/.claude/skills/vercel-react-best-practices/SKILL.md b/.agents/skills/vercel-react-best-practices/SKILL.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/SKILL.md rename to .agents/skills/vercel-react-best-practices/SKILL.md diff --git a/.claude/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md b/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md rename to .agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md diff --git a/.claude/skills/vercel-react-best-practices/rules/advanced-use-latest.md b/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/advanced-use-latest.md rename to .agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md diff --git a/.claude/skills/vercel-react-best-practices/rules/async-api-routes.md b/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/async-api-routes.md rename to .agents/skills/vercel-react-best-practices/rules/async-api-routes.md diff --git a/.claude/skills/vercel-react-best-practices/rules/async-defer-await.md b/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/async-defer-await.md rename to .agents/skills/vercel-react-best-practices/rules/async-defer-await.md diff --git a/.claude/skills/vercel-react-best-practices/rules/async-dependencies.md b/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/async-dependencies.md rename to .agents/skills/vercel-react-best-practices/rules/async-dependencies.md diff --git a/.claude/skills/vercel-react-best-practices/rules/async-parallel.md b/.agents/skills/vercel-react-best-practices/rules/async-parallel.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/async-parallel.md rename to .agents/skills/vercel-react-best-practices/rules/async-parallel.md diff --git a/.claude/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md b/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md rename to .agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md diff --git a/.claude/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md b/.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md rename to .agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md diff --git a/.claude/skills/vercel-react-best-practices/rules/bundle-conditional.md b/.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/bundle-conditional.md rename to .agents/skills/vercel-react-best-practices/rules/bundle-conditional.md diff --git a/.claude/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md b/.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md rename to .agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md diff --git a/.claude/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md b/.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md rename to .agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md diff --git a/.claude/skills/vercel-react-best-practices/rules/bundle-preload.md b/.agents/skills/vercel-react-best-practices/rules/bundle-preload.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/bundle-preload.md rename to .agents/skills/vercel-react-best-practices/rules/bundle-preload.md diff --git a/.claude/skills/vercel-react-best-practices/rules/client-event-listeners.md b/.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/client-event-listeners.md rename to .agents/skills/vercel-react-best-practices/rules/client-event-listeners.md diff --git a/.claude/skills/vercel-react-best-practices/rules/client-localstorage-schema.md b/.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/client-localstorage-schema.md rename to .agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md diff --git a/.claude/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md b/.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md rename to .agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md diff --git a/.claude/skills/vercel-react-best-practices/rules/client-swr-dedup.md b/.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/client-swr-dedup.md rename to .agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md diff --git a/.claude/skills/vercel-react-best-practices/rules/js-batch-dom-css.md b/.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/js-batch-dom-css.md rename to .agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md diff --git a/.claude/skills/vercel-react-best-practices/rules/js-cache-function-results.md b/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/js-cache-function-results.md rename to .agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md diff --git a/.claude/skills/vercel-react-best-practices/rules/js-cache-property-access.md b/.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/js-cache-property-access.md rename to .agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md diff --git a/.claude/skills/vercel-react-best-practices/rules/js-cache-storage.md b/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/js-cache-storage.md rename to .agents/skills/vercel-react-best-practices/rules/js-cache-storage.md diff --git a/.claude/skills/vercel-react-best-practices/rules/js-combine-iterations.md b/.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/js-combine-iterations.md rename to .agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md diff --git a/.claude/skills/vercel-react-best-practices/rules/js-early-exit.md b/.agents/skills/vercel-react-best-practices/rules/js-early-exit.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/js-early-exit.md rename to .agents/skills/vercel-react-best-practices/rules/js-early-exit.md diff --git a/.claude/skills/vercel-react-best-practices/rules/js-hoist-regexp.md b/.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/js-hoist-regexp.md rename to .agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md diff --git a/.claude/skills/vercel-react-best-practices/rules/js-index-maps.md b/.agents/skills/vercel-react-best-practices/rules/js-index-maps.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/js-index-maps.md rename to .agents/skills/vercel-react-best-practices/rules/js-index-maps.md diff --git a/.claude/skills/vercel-react-best-practices/rules/js-length-check-first.md b/.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/js-length-check-first.md rename to .agents/skills/vercel-react-best-practices/rules/js-length-check-first.md diff --git a/.claude/skills/vercel-react-best-practices/rules/js-min-max-loop.md b/.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/js-min-max-loop.md rename to .agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md diff --git a/.claude/skills/vercel-react-best-practices/rules/js-set-map-lookups.md b/.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/js-set-map-lookups.md rename to .agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md diff --git a/.claude/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md b/.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md rename to .agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md diff --git a/.claude/skills/vercel-react-best-practices/rules/rendering-activity.md b/.agents/skills/vercel-react-best-practices/rules/rendering-activity.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/rendering-activity.md rename to .agents/skills/vercel-react-best-practices/rules/rendering-activity.md diff --git a/.claude/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md b/.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md rename to .agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md diff --git a/.claude/skills/vercel-react-best-practices/rules/rendering-conditional-render.md b/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/rendering-conditional-render.md rename to .agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md diff --git a/.claude/skills/vercel-react-best-practices/rules/rendering-content-visibility.md b/.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/rendering-content-visibility.md rename to .agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md diff --git a/.claude/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md b/.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md rename to .agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md diff --git a/.claude/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md b/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md rename to .agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md diff --git a/.claude/skills/vercel-react-best-practices/rules/rendering-svg-precision.md b/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/rendering-svg-precision.md rename to .agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md diff --git a/.claude/skills/vercel-react-best-practices/rules/rerender-defer-reads.md b/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/rerender-defer-reads.md rename to .agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md diff --git a/.claude/skills/vercel-react-best-practices/rules/rerender-dependencies.md b/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/rerender-dependencies.md rename to .agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md diff --git a/.claude/skills/vercel-react-best-practices/rules/rerender-derived-state.md b/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/rerender-derived-state.md rename to .agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md diff --git a/.claude/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md b/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md rename to .agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md diff --git a/.claude/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md b/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md rename to .agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md diff --git a/.claude/skills/vercel-react-best-practices/rules/rerender-memo.md b/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/rerender-memo.md rename to .agents/skills/vercel-react-best-practices/rules/rerender-memo.md diff --git a/.claude/skills/vercel-react-best-practices/rules/rerender-transitions.md b/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/rerender-transitions.md rename to .agents/skills/vercel-react-best-practices/rules/rerender-transitions.md diff --git a/.claude/skills/vercel-react-best-practices/rules/server-after-nonblocking.md b/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/server-after-nonblocking.md rename to .agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md diff --git a/.claude/skills/vercel-react-best-practices/rules/server-cache-lru.md b/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/server-cache-lru.md rename to .agents/skills/vercel-react-best-practices/rules/server-cache-lru.md diff --git a/.claude/skills/vercel-react-best-practices/rules/server-cache-react.md b/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/server-cache-react.md rename to .agents/skills/vercel-react-best-practices/rules/server-cache-react.md diff --git a/.claude/skills/vercel-react-best-practices/rules/server-parallel-fetching.md b/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/server-parallel-fetching.md rename to .agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md diff --git a/.claude/skills/vercel-react-best-practices/rules/server-serialization.md b/.agents/skills/vercel-react-best-practices/rules/server-serialization.md similarity index 100% rename from .claude/skills/vercel-react-best-practices/rules/server-serialization.md rename to .agents/skills/vercel-react-best-practices/rules/server-serialization.md diff --git a/.agents/skills/web-design-guidelines/SKILL.md b/.agents/skills/web-design-guidelines/SKILL.md new file mode 100644 index 0000000000..ceae92ab31 --- /dev/null +++ b/.agents/skills/web-design-guidelines/SKILL.md @@ -0,0 +1,39 @@ +--- +name: web-design-guidelines +description: Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices". +metadata: + author: vercel + version: "1.0.0" + argument-hint: +--- + +# Web Interface Guidelines + +Review files for compliance with Web Interface Guidelines. + +## How It Works + +1. Fetch the latest guidelines from the source URL below +2. Read the specified files (or prompt user for files/pattern) +3. Check against all rules in the fetched guidelines +4. Output findings in the terse `file:line` format + +## Guidelines Source + +Fetch fresh guidelines before each review: + +``` +https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/main/command.md +``` + +Use WebFetch to retrieve the latest rules. The fetched content contains all the rules and output format instructions. + +## Usage + +When a user provides a file or pattern argument: +1. Fetch guidelines from the source URL above +2. Read the specified files +3. Apply all rules from the fetched guidelines +4. Output findings using the format specified in the guidelines + +If no files specified, ask the user which files to review. diff --git a/.claude/skills/component-refactoring b/.claude/skills/component-refactoring new file mode 120000 index 0000000000..53ae67e2f2 --- /dev/null +++ b/.claude/skills/component-refactoring @@ -0,0 +1 @@ +../../.agents/skills/component-refactoring \ No newline at end of file diff --git a/.claude/skills/frontend-code-review b/.claude/skills/frontend-code-review new file mode 120000 index 0000000000..55654ffbd7 --- /dev/null +++ b/.claude/skills/frontend-code-review @@ -0,0 +1 @@ +../../.agents/skills/frontend-code-review \ No newline at end of file diff --git a/.claude/skills/frontend-testing b/.claude/skills/frontend-testing new file mode 120000 index 0000000000..092cec7745 --- /dev/null +++ b/.claude/skills/frontend-testing @@ -0,0 +1 @@ +../../.agents/skills/frontend-testing \ No newline at end of file diff --git a/.claude/skills/orpc-contract-first b/.claude/skills/orpc-contract-first new file mode 120000 index 0000000000..da47b335c7 --- /dev/null +++ b/.claude/skills/orpc-contract-first @@ -0,0 +1 @@ +../../.agents/skills/orpc-contract-first \ No newline at end of file diff --git a/.claude/skills/skill-creator b/.claude/skills/skill-creator new file mode 120000 index 0000000000..b87455490f --- /dev/null +++ b/.claude/skills/skill-creator @@ -0,0 +1 @@ +../../.agents/skills/skill-creator \ No newline at end of file diff --git a/.claude/skills/vercel-react-best-practices b/.claude/skills/vercel-react-best-practices new file mode 120000 index 0000000000..e567923b32 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices @@ -0,0 +1 @@ +../../.agents/skills/vercel-react-best-practices \ No newline at end of file diff --git a/.claude/skills/web-design-guidelines b/.claude/skills/web-design-guidelines new file mode 120000 index 0000000000..886b26ded7 --- /dev/null +++ b/.claude/skills/web-design-guidelines @@ -0,0 +1 @@ +../../.agents/skills/web-design-guidelines \ No newline at end of file diff --git a/.codex/skills b/.codex/skills deleted file mode 120000 index 454b8427cd..0000000000 --- a/.codex/skills +++ /dev/null @@ -1 +0,0 @@ -../.claude/skills \ No newline at end of file diff --git a/.codex/skills/component-refactoring b/.codex/skills/component-refactoring new file mode 120000 index 0000000000..53ae67e2f2 --- /dev/null +++ b/.codex/skills/component-refactoring @@ -0,0 +1 @@ +../../.agents/skills/component-refactoring \ No newline at end of file diff --git a/.codex/skills/frontend-code-review b/.codex/skills/frontend-code-review new file mode 120000 index 0000000000..55654ffbd7 --- /dev/null +++ b/.codex/skills/frontend-code-review @@ -0,0 +1 @@ +../../.agents/skills/frontend-code-review \ No newline at end of file diff --git a/.codex/skills/frontend-testing b/.codex/skills/frontend-testing new file mode 120000 index 0000000000..092cec7745 --- /dev/null +++ b/.codex/skills/frontend-testing @@ -0,0 +1 @@ +../../.agents/skills/frontend-testing \ No newline at end of file diff --git a/.codex/skills/orpc-contract-first b/.codex/skills/orpc-contract-first new file mode 120000 index 0000000000..da47b335c7 --- /dev/null +++ b/.codex/skills/orpc-contract-first @@ -0,0 +1 @@ +../../.agents/skills/orpc-contract-first \ No newline at end of file diff --git a/.codex/skills/skill-creator b/.codex/skills/skill-creator new file mode 120000 index 0000000000..b87455490f --- /dev/null +++ b/.codex/skills/skill-creator @@ -0,0 +1 @@ +../../.agents/skills/skill-creator \ No newline at end of file diff --git a/.codex/skills/vercel-react-best-practices b/.codex/skills/vercel-react-best-practices new file mode 120000 index 0000000000..e567923b32 --- /dev/null +++ b/.codex/skills/vercel-react-best-practices @@ -0,0 +1 @@ +../../.agents/skills/vercel-react-best-practices \ No newline at end of file diff --git a/.codex/skills/web-design-guidelines b/.codex/skills/web-design-guidelines new file mode 120000 index 0000000000..886b26ded7 --- /dev/null +++ b/.codex/skills/web-design-guidelines @@ -0,0 +1 @@ +../../.agents/skills/web-design-guidelines \ No newline at end of file diff --git a/.cursor/skills/component-refactoring b/.cursor/skills/component-refactoring new file mode 120000 index 0000000000..53ae67e2f2 --- /dev/null +++ b/.cursor/skills/component-refactoring @@ -0,0 +1 @@ +../../.agents/skills/component-refactoring \ No newline at end of file diff --git a/.cursor/skills/frontend-code-review b/.cursor/skills/frontend-code-review new file mode 120000 index 0000000000..55654ffbd7 --- /dev/null +++ b/.cursor/skills/frontend-code-review @@ -0,0 +1 @@ +../../.agents/skills/frontend-code-review \ No newline at end of file diff --git a/.cursor/skills/frontend-testing b/.cursor/skills/frontend-testing new file mode 120000 index 0000000000..092cec7745 --- /dev/null +++ b/.cursor/skills/frontend-testing @@ -0,0 +1 @@ +../../.agents/skills/frontend-testing \ No newline at end of file diff --git a/.cursor/skills/orpc-contract-first b/.cursor/skills/orpc-contract-first new file mode 120000 index 0000000000..da47b335c7 --- /dev/null +++ b/.cursor/skills/orpc-contract-first @@ -0,0 +1 @@ +../../.agents/skills/orpc-contract-first \ No newline at end of file diff --git a/.cursor/skills/skill-creator b/.cursor/skills/skill-creator new file mode 120000 index 0000000000..b87455490f --- /dev/null +++ b/.cursor/skills/skill-creator @@ -0,0 +1 @@ +../../.agents/skills/skill-creator \ No newline at end of file diff --git a/.cursor/skills/vercel-react-best-practices b/.cursor/skills/vercel-react-best-practices new file mode 120000 index 0000000000..e567923b32 --- /dev/null +++ b/.cursor/skills/vercel-react-best-practices @@ -0,0 +1 @@ +../../.agents/skills/vercel-react-best-practices \ No newline at end of file diff --git a/.cursor/skills/web-design-guidelines b/.cursor/skills/web-design-guidelines new file mode 120000 index 0000000000..886b26ded7 --- /dev/null +++ b/.cursor/skills/web-design-guidelines @@ -0,0 +1 @@ +../../.agents/skills/web-design-guidelines \ No newline at end of file diff --git a/.gemini/skills/component-refactoring b/.gemini/skills/component-refactoring new file mode 120000 index 0000000000..53ae67e2f2 --- /dev/null +++ b/.gemini/skills/component-refactoring @@ -0,0 +1 @@ +../../.agents/skills/component-refactoring \ No newline at end of file diff --git a/.gemini/skills/frontend-code-review b/.gemini/skills/frontend-code-review new file mode 120000 index 0000000000..55654ffbd7 --- /dev/null +++ b/.gemini/skills/frontend-code-review @@ -0,0 +1 @@ +../../.agents/skills/frontend-code-review \ No newline at end of file diff --git a/.gemini/skills/frontend-testing b/.gemini/skills/frontend-testing new file mode 120000 index 0000000000..092cec7745 --- /dev/null +++ b/.gemini/skills/frontend-testing @@ -0,0 +1 @@ +../../.agents/skills/frontend-testing \ No newline at end of file diff --git a/.gemini/skills/orpc-contract-first b/.gemini/skills/orpc-contract-first new file mode 120000 index 0000000000..da47b335c7 --- /dev/null +++ b/.gemini/skills/orpc-contract-first @@ -0,0 +1 @@ +../../.agents/skills/orpc-contract-first \ No newline at end of file diff --git a/.gemini/skills/skill-creator b/.gemini/skills/skill-creator new file mode 120000 index 0000000000..b87455490f --- /dev/null +++ b/.gemini/skills/skill-creator @@ -0,0 +1 @@ +../../.agents/skills/skill-creator \ No newline at end of file diff --git a/.gemini/skills/vercel-react-best-practices b/.gemini/skills/vercel-react-best-practices new file mode 120000 index 0000000000..e567923b32 --- /dev/null +++ b/.gemini/skills/vercel-react-best-practices @@ -0,0 +1 @@ +../../.agents/skills/vercel-react-best-practices \ No newline at end of file diff --git a/.gemini/skills/web-design-guidelines b/.gemini/skills/web-design-guidelines new file mode 120000 index 0000000000..886b26ded7 --- /dev/null +++ b/.gemini/skills/web-design-guidelines @@ -0,0 +1 @@ +../../.agents/skills/web-design-guidelines \ No newline at end of file diff --git a/.github/skills/component-refactoring b/.github/skills/component-refactoring new file mode 120000 index 0000000000..53ae67e2f2 --- /dev/null +++ b/.github/skills/component-refactoring @@ -0,0 +1 @@ +../../.agents/skills/component-refactoring \ No newline at end of file diff --git a/.github/skills/frontend-code-review b/.github/skills/frontend-code-review new file mode 120000 index 0000000000..55654ffbd7 --- /dev/null +++ b/.github/skills/frontend-code-review @@ -0,0 +1 @@ +../../.agents/skills/frontend-code-review \ No newline at end of file diff --git a/.github/skills/frontend-testing b/.github/skills/frontend-testing new file mode 120000 index 0000000000..092cec7745 --- /dev/null +++ b/.github/skills/frontend-testing @@ -0,0 +1 @@ +../../.agents/skills/frontend-testing \ No newline at end of file diff --git a/.github/skills/orpc-contract-first b/.github/skills/orpc-contract-first new file mode 120000 index 0000000000..da47b335c7 --- /dev/null +++ b/.github/skills/orpc-contract-first @@ -0,0 +1 @@ +../../.agents/skills/orpc-contract-first \ No newline at end of file diff --git a/.github/skills/skill-creator b/.github/skills/skill-creator new file mode 120000 index 0000000000..b87455490f --- /dev/null +++ b/.github/skills/skill-creator @@ -0,0 +1 @@ +../../.agents/skills/skill-creator \ No newline at end of file diff --git a/.github/skills/vercel-react-best-practices b/.github/skills/vercel-react-best-practices new file mode 120000 index 0000000000..e567923b32 --- /dev/null +++ b/.github/skills/vercel-react-best-practices @@ -0,0 +1 @@ +../../.agents/skills/vercel-react-best-practices \ No newline at end of file diff --git a/.github/skills/web-design-guidelines b/.github/skills/web-design-guidelines new file mode 120000 index 0000000000..886b26ded7 --- /dev/null +++ b/.github/skills/web-design-guidelines @@ -0,0 +1 @@ +../../.agents/skills/web-design-guidelines \ No newline at end of file diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index ff006324bb..4571fd1cd1 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -82,6 +82,6 @@ jobs: # mdformat breaks YAML front matter in markdown files. Add --exclude for directories containing YAML front matter. - name: mdformat run: | - uvx --python 3.13 mdformat . --exclude ".claude/skills/**" + uvx --python 3.13 mdformat . --exclude ".agents/skills/**" - uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 diff --git a/api/agent-notes/controllers/console/datasets/datasets_document.py.md b/api/agent-notes/controllers/console/datasets/datasets_document.py.md deleted file mode 100644 index b100249981..0000000000 --- a/api/agent-notes/controllers/console/datasets/datasets_document.py.md +++ /dev/null @@ -1,52 +0,0 @@ -## Purpose - -`api/controllers/console/datasets/datasets_document.py` contains the console (authenticated) APIs for managing dataset documents (list/create/update/delete, processing controls, estimates, etc.). - -## Storage model (uploaded files) - -- For local file uploads into a knowledge base, the binary is stored via `extensions.ext_storage.storage` under the key: - - `upload_files//.` -- File metadata is stored in the `upload_files` table (`UploadFile` model), keyed by `UploadFile.id`. -- Dataset `Document` records reference the uploaded file via: - - `Document.data_source_info.upload_file_id` - -## Download endpoint - -- `GET /datasets//documents//download` - - - Only supported when `Document.data_source_type == "upload_file"`. - - Performs dataset permission + tenant checks via `DocumentResource.get_document(...)`. - - Delegates `Document -> UploadFile` validation and signed URL generation to `DocumentService.get_document_download_url(...)`. - - Applies `cloud_edition_billing_rate_limit_check("knowledge")` to match other KB operations. - - Response body is **only**: `{ "url": "" }`. - -- `POST /datasets//documents/download-zip` - - - Accepts `{ "document_ids": ["..."] }` (upload-file only). - - Returns `application/zip` as a single attachment download. - - Rationale: browsers often block multiple automatic downloads; a ZIP avoids that limitation. - - Applies `cloud_edition_billing_rate_limit_check("knowledge")`. - - Delegates dataset permission checks, document/upload-file validation, and download-name generation to - `DocumentService.prepare_document_batch_download_zip(...)` before streaming the ZIP. - -## Verification plan - -- Upload a document from a local file into a dataset. -- Call the download endpoint and confirm it returns a signed URL. -- Open the URL and confirm: - - Response headers force download (`Content-Disposition`), and - - Downloaded bytes match the uploaded file. -- Select multiple uploaded-file documents and download as ZIP; confirm all selected files exist in the archive. - -## Shared helper - -- `DocumentService.get_document_download_url(document)` resolves the `UploadFile` and signs a download URL. -- `DocumentService.prepare_document_batch_download_zip(...)` performs dataset permission checks, batches - document + upload file lookups, preserves request order, and generates the client-visible ZIP filename. -- Internal helpers now live in `DocumentService` (`_get_upload_file_id_for_upload_file_document(...)`, - `_get_upload_file_for_upload_file_document(...)`, `_get_upload_files_by_document_id_for_zip_download(...)`). -- ZIP packing is handled by `FileService.build_upload_files_zip_tempfile(...)`, which also: - - sanitizes entry names to avoid path traversal, and - - deduplicates names while preserving extensions (e.g., `doc.txt` → `doc (1).txt`). - Streaming the response and deferring cleanup is handled by the route via `send_file(path, ...)` + `ExitStack` + - `response.call_on_close(...)` (the file is deleted when the response is closed). diff --git a/api/agent-notes/services/dataset_service.py.md b/api/agent-notes/services/dataset_service.py.md deleted file mode 100644 index b68ef345f5..0000000000 --- a/api/agent-notes/services/dataset_service.py.md +++ /dev/null @@ -1,18 +0,0 @@ -## Purpose - -`api/services/dataset_service.py` hosts dataset/document service logic used by console and API controllers. - -## Batch document operations - -- Batch document workflows should avoid N+1 database queries by using set-based lookups. -- Tenant checks must be enforced consistently across dataset/document operations. -- `DocumentService.get_documents_by_ids(...)` fetches documents for a dataset using `id.in_(...)`. -- `FileService.get_upload_files_by_ids(...)` performs tenant-scoped batch lookup for `UploadFile` (dedupes ids with `set(...)`). -- `DocumentService.get_document_download_url(...)` and `prepare_document_batch_download_zip(...)` handle - dataset/document permission checks plus `Document -> UploadFile` validation for download endpoints. - -## Verification plan - -- Exercise document list and download endpoints that use the service helpers. -- Confirm batch download uses constant query count for documents + upload files. -- Request a ZIP with a missing document id and confirm a 404 is returned. diff --git a/api/agent-notes/services/file_service.py.md b/api/agent-notes/services/file_service.py.md deleted file mode 100644 index cf394a1c05..0000000000 --- a/api/agent-notes/services/file_service.py.md +++ /dev/null @@ -1,35 +0,0 @@ -## Purpose - -`api/services/file_service.py` owns business logic around `UploadFile` objects: upload validation, storage persistence, -previews/generators, and deletion. - -## Key invariants - -- All storage I/O goes through `extensions.ext_storage.storage`. -- Uploaded file keys follow: `upload_files//.`. -- Upload validation is enforced in `FileService.upload_file(...)` (blocked extensions, size limits, dataset-only types). - -## Batch lookup helpers - -- `FileService.get_upload_files_by_ids(tenant_id, upload_file_ids)` is the canonical tenant-scoped batch loader for - `UploadFile`. - -## Dataset document download helpers - -The dataset document download/ZIP endpoints now delegate “Document → UploadFile” validation and permission checks to -`DocumentService` (`api/services/dataset_service.py`). `FileService` stays focused on generic `UploadFile` operations -(uploading, previews, deletion), plus generic ZIP serving. - -### ZIP serving - -- `FileService.build_upload_files_zip_tempfile(...)` builds a ZIP from `UploadFile` objects and yields a seeked - tempfile **path** so callers can stream it (e.g., `send_file(path, ...)`) without hitting "read of closed file" - issues from file-handle lifecycle during streamed responses. -- Flask `send_file(...)` and the `ExitStack`/`call_on_close(...)` cleanup pattern are handled in the route layer. - -## Verification plan - -- Unit: `api/tests/unit_tests/controllers/console/datasets/test_datasets_document_download.py` - - Verify signed URL generation for upload-file documents and ZIP download behavior for multiple documents. -- Unit: `api/tests/unit_tests/services/test_file_service_zip_and_lookup.py` - - Verify ZIP packing produces a valid, openable archive and preserves file content. diff --git a/api/agent-notes/tests/unit_tests/controllers/console/datasets/test_datasets_document_download.py.md b/api/agent-notes/tests/unit_tests/controllers/console/datasets/test_datasets_document_download.py.md deleted file mode 100644 index 8f78dacde8..0000000000 --- a/api/agent-notes/tests/unit_tests/controllers/console/datasets/test_datasets_document_download.py.md +++ /dev/null @@ -1,28 +0,0 @@ -## Purpose - -Unit tests for the console dataset document download endpoint: - -- `GET /datasets//documents//download` - -## Testing approach - -- Uses `Flask.test_request_context()` and calls the `Resource.get(...)` method directly. -- Monkeypatches console decorators (`login_required`, `setup_required`, rate limit) to no-ops to keep the test focused. -- Mocks: - - `DatasetService.get_dataset` / `check_dataset_permission` - - `DocumentService.get_document` for single-file download tests - - `DocumentService.get_documents_by_ids` + `FileService.get_upload_files_by_ids` for ZIP download tests - - `FileService.get_upload_files_by_ids` for `UploadFile` lookups in single-file tests - - `services.dataset_service.file_helpers.get_signed_file_url` to return a deterministic URL -- Document mocks include `id` fields so batch lookups can map documents by id. - -## Covered cases - -- Success returns `{ "url": "" }` for upload-file documents. -- 404 when document is not `upload_file`. -- 404 when `upload_file_id` is missing. -- 404 when referenced `UploadFile` row does not exist. -- 403 when document tenant does not match current tenant. -- Batch ZIP download returns `application/zip` for upload-file documents. -- Batch ZIP download rejects non-upload-file documents. -- Batch ZIP download uses a random `.zip` attachment name (`download_name`), so tests only assert the suffix. diff --git a/api/agent-notes/tests/unit_tests/services/test_file_service_zip_and_lookup.py.md b/api/agent-notes/tests/unit_tests/services/test_file_service_zip_and_lookup.py.md deleted file mode 100644 index dbcdf26f10..0000000000 --- a/api/agent-notes/tests/unit_tests/services/test_file_service_zip_and_lookup.py.md +++ /dev/null @@ -1,18 +0,0 @@ -## Purpose - -Unit tests for `api/services/file_service.py` helper methods that are not covered by higher-level controller tests. - -## What’s covered - -- `FileService.build_upload_files_zip_tempfile(...)` - - ZIP entry name sanitization (no directory components / traversal) - - name deduplication while preserving extensions - - writing streamed bytes from `storage.load(...)` into ZIP entries - - yields a tempfile path so callers can open/stream the ZIP without holding a live file handle -- `FileService.get_upload_files_by_ids(...)` - - returns `{}` for empty id lists - - returns an id-keyed mapping for non-empty lists - -## Notes - -- These tests intentionally stub `storage.load` and `db.session.scalars(...).all()` to avoid needing a real DB/storage. diff --git a/api/agent_skills/infra.md b/api/agent_skills/infra.md deleted file mode 100644 index bc36c7bf64..0000000000 --- a/api/agent_skills/infra.md +++ /dev/null @@ -1,96 +0,0 @@ -## Configuration - -- Import `configs.dify_config` for every runtime toggle. Do not read environment variables directly. -- Add new settings to the proper mixin inside `configs/` (deployment, feature, middleware, etc.) so they load through `DifyConfig`. -- Remote overrides come from the optional providers in `configs/remote_settings_sources`; keep defaults in code safe when the value is missing. -- Example: logging pulls targets from `extensions/ext_logging.py`, and model provider URLs are assembled in `services/entities/model_provider_entities.py`. - -## Dependencies - -- Runtime dependencies live in `[project].dependencies` inside `pyproject.toml`. Optional clients go into the `storage`, `tools`, or `vdb` groups under `[dependency-groups]`. -- Always pin versions and keep the list alphabetised. Shared tooling (lint, typing, pytest) belongs in the `dev` group. -- When code needs a new package, explain why in the PR and run `uv lock` so the lockfile stays current. - -## Storage & Files - -- Use `extensions.ext_storage.storage` for all blob IO; it already respects the configured backend. -- Convert files for workflows with helpers in `core/file/file_manager.py`; they handle signed URLs and multimodal payloads. -- When writing controller logic, delegate upload quotas and metadata to `services/file_service.py` instead of touching storage directly. -- All outbound HTTP fetches (webhooks, remote files) must go through the SSRF-safe client in `core/helper/ssrf_proxy.py`; it wraps `httpx` with the allow/deny rules configured for the platform. - -## Redis & Shared State - -- Access Redis through `extensions.ext_redis.redis_client`. For locking, reuse `redis_client.lock`. -- Prefer higher-level helpers when available: rate limits use `libs.helper.RateLimiter`, provider metadata uses caches in `core/helper/provider_cache.py`. - -## Models - -- SQLAlchemy models sit in `models/` and inherit from the shared declarative `Base` defined in `models/base.py` (metadata configured via `models/engine.py`). -- `models/__init__.py` exposes grouped aggregates: account/tenant models, app and conversation tables, datasets, providers, workflow runs, triggers, etc. Import from there to avoid deep path churn. -- Follow the DDD boundary: persistence objects live in `models/`, repositories under `repositories/` translate them into domain entities, and services consume those repositories. -- When adding a table, create the model class, register it in `models/__init__.py`, wire a repository if needed, and generate an Alembic migration as described below. - -## Vector Stores - -- Vector client implementations live in `core/rag/datasource/vdb/`, with a common factory in `core/rag/datasource/vdb/vector_factory.py` and enums in `core/rag/datasource/vdb/vector_type.py`. -- Retrieval pipelines call these providers through `core/rag/datasource/retrieval_service.py` and dataset ingestion flows in `services/dataset_service.py`. -- The CLI helper `flask vdb-migrate` orchestrates bulk migrations using routines in `commands.py`; reuse that pattern when adding new backend transitions. -- To add another store, mirror the provider layout, register it with the factory, and include any schema changes in Alembic migrations. - -## Observability & OTEL - -- OpenTelemetry settings live under the observability mixin in `configs/observability`. Toggle exporters and sampling via `dify_config`, not ad-hoc env reads. -- HTTP, Celery, Redis, SQLAlchemy, and httpx instrumentation is initialised in `extensions/ext_app_metrics.py` and `extensions/ext_request_logging.py`; reuse these hooks when adding new workers or entrypoints. -- When creating background tasks or external calls, propagate tracing context with helpers in the existing instrumented clients (e.g. use the shared `httpx` session from `core/helper/http_client_pooling.py`). -- If you add a new external integration, ensure spans and metrics are emitted by wiring the appropriate OTEL instrumentation package in `pyproject.toml` and configuring it in `extensions/`. - -## Ops Integrations - -- Langfuse support and other tracing bridges live under `core/ops/opik_trace`. Config toggles sit in `configs/observability`, while exporters are initialised in the OTEL extensions mentioned above. -- External monitoring services should follow this pattern: keep client code in `core/ops`, expose switches via `dify_config`, and hook initialisation in `extensions/ext_app_metrics.py` or sibling modules. -- Before instrumenting new code paths, check whether existing context helpers (e.g. `extensions/ext_request_logging.py`) already capture the necessary metadata. - -## Controllers, Services, Core - -- Controllers only parse HTTP input and call a service method. Keep business rules in `services/`. -- Services enforce tenant rules, quotas, and orchestration, then call into `core/` engines (workflow execution, tools, LLMs). -- When adding a new endpoint, search for an existing service to extend before introducing a new layer. Example: workflow APIs pipe through `services/workflow_service.py` into `core/workflow`. - -## Plugins, Tools, Providers - -- In Dify a plugin is a tenant-installable bundle that declares one or more providers (tool, model, datasource, trigger, endpoint, agent strategy) plus its resource needs and version metadata. The manifest (`core/plugin/entities/plugin.py`) mirrors what you see in the marketplace documentation. -- Installation, upgrades, and migrations are orchestrated by `services/plugin/plugin_service.py` together with helpers such as `services/plugin/plugin_migration.py`. -- Runtime loading happens through the implementations under `core/plugin/impl/*` (tool/model/datasource/trigger/endpoint/agent). These modules normalise plugin providers so that downstream systems (`core/tools/tool_manager.py`, `services/model_provider_service.py`, `services/trigger/*`) can treat builtin and plugin capabilities the same way. -- For remote execution, plugin daemons (`core/plugin/entities/plugin_daemon.py`, `core/plugin/impl/plugin.py`) manage lifecycle hooks, credential forwarding, and background workers that keep plugin processes in sync with the main application. -- Acquire tool implementations through `core/tools/tool_manager.py`; it resolves builtin, plugin, and workflow-as-tool providers uniformly, injecting the right context (tenant, credentials, runtime config). -- To add a new plugin capability, extend the relevant `core/plugin/entities` schema and register the implementation in the matching `core/plugin/impl` module rather than importing the provider directly. - -## Async Workloads - -see `agent_skills/trigger.md` for more detailed documentation. - -- Enqueue background work through `services/async_workflow_service.py`. It routes jobs to the tiered Celery queues defined in `tasks/`. -- Workers boot from `celery_entrypoint.py` and execute functions in `tasks/workflow_execution_tasks.py`, `tasks/trigger_processing_tasks.py`, etc. -- Scheduled workflows poll from `schedule/workflow_schedule_tasks.py`. Follow the same pattern if you need new periodic jobs. - -## Database & Migrations - -- SQLAlchemy models live under `models/` and map directly to migration files in `migrations/versions`. -- Generate migrations with `uv run --project api flask db revision --autogenerate -m ""`, then review the diff; never hand-edit the database outside Alembic. -- Apply migrations locally using `uv run --project api flask db upgrade`; production deploys expect the same history. -- If you add tenant-scoped data, confirm the upgrade includes tenant filters or defaults consistent with the service logic touching those tables. - -## CLI Commands - -- Maintenance commands from `commands.py` are registered on the Flask CLI. Run them via `uv run --project api flask `. -- Use the built-in `db` commands from Flask-Migrate for schema operations (`flask db upgrade`, `flask db stamp`, etc.). Only fall back to custom helpers if you need their extra behaviour. -- Custom entries such as `flask reset-password`, `flask reset-email`, and `flask vdb-migrate` handle self-hosted account recovery and vector database migrations. -- Before adding a new command, check whether an existing service can be reused and ensure the command guards edition-specific behaviour (many enforce `SELF_HOSTED`). Document any additions in the PR. -- Ruff helpers are run directly with `uv`: `uv run --project api --dev ruff format ./api` for formatting and `uv run --project api --dev ruff check ./api` (add `--fix` if you want automatic fixes). - -## When You Add Features - -- Check for an existing helper or service before writing a new util. -- Uphold tenancy: every service method should receive the tenant ID from controller wrappers such as `controllers/console/wraps.py`. -- Update or create tests alongside behaviour changes (`tests/unit_tests` for fast coverage, `tests/integration_tests` when touching orchestrations). -- Run `uv run --project api --dev ruff check ./api`, `uv run --directory api --dev basedpyright`, and `uv run --project api --dev dev/pytest/pytest_unit_tests.sh` before submitting changes. diff --git a/api/agent_skills/plugin.md b/api/agent_skills/plugin.md deleted file mode 100644 index 954ddd236b..0000000000 --- a/api/agent_skills/plugin.md +++ /dev/null @@ -1 +0,0 @@ -// TBD diff --git a/api/agent_skills/plugin_oauth.md b/api/agent_skills/plugin_oauth.md deleted file mode 100644 index 954ddd236b..0000000000 --- a/api/agent_skills/plugin_oauth.md +++ /dev/null @@ -1 +0,0 @@ -// TBD diff --git a/api/agent_skills/trigger.md b/api/agent_skills/trigger.md deleted file mode 100644 index f4b076332c..0000000000 --- a/api/agent_skills/trigger.md +++ /dev/null @@ -1,53 +0,0 @@ -## Overview - -Trigger is a collection of nodes that we called `Start` nodes, also, the concept of `Start` is the same as `RootNode` in the workflow engine `core/workflow/graph_engine`, On the other hand, `Start` node is the entry point of workflows, every workflow run always starts from a `Start` node. - -## Trigger nodes - -- `UserInput` -- `Trigger Webhook` -- `Trigger Schedule` -- `Trigger Plugin` - -### UserInput - -Before `Trigger` concept is introduced, it's what we called `Start` node, but now, to avoid confusion, it was renamed to `UserInput` node, has a strong relation with `ServiceAPI` in `controllers/service_api/app` - -1. `UserInput` node introduces a list of arguments that need to be provided by the user, finally it will be converted into variables in the workflow variable pool. -1. `ServiceAPI` accept those arguments, and pass through them into `UserInput` node. -1. For its detailed implementation, please refer to `core/workflow/nodes/start` - -### Trigger Webhook - -Inside Webhook Node, Dify provided a UI panel that allows user define a HTTP manifest `core/workflow/nodes/trigger_webhook/entities.py`.`WebhookData`, also, Dify generates a random webhook id for each `Trigger Webhook` node, the implementation was implemented in `core/trigger/utils/endpoint.py`, as you can see, `webhook-debug` is a debug mode for webhook, you may find it in `controllers/trigger/webhook.py`. - -Finally, requests to `webhook` endpoint will be converted into variables in workflow variable pool during workflow execution. - -### Trigger Schedule - -`Trigger Schedule` node is a node that allows user define a schedule to trigger the workflow, detailed manifest is here `core/workflow/nodes/trigger_schedule/entities.py`, we have a poller and executor to handle millions of schedules, see `docker/entrypoint.sh` / `schedule/workflow_schedule_task.py` for help. - -To Achieve this, a `WorkflowSchedulePlan` model was introduced in `models/trigger.py`, and a `events/event_handlers/sync_workflow_schedule_when_app_published.py` was used to sync workflow schedule plans when app is published. - -### Trigger Plugin - -`Trigger Plugin` node allows user define there own distributed trigger plugin, whenever a request was received, Dify forwards it to the plugin and wait for parsed variables from it. - -1. Requests were saved in storage by `services/trigger/trigger_request_service.py`, referenced by `services/trigger/trigger_service.py`.`TriggerService`.`process_endpoint` -1. Plugins accept those requests and parse variables from it, see `core/plugin/impl/trigger.py` for details. - -A `subscription` concept was out here by Dify, it means an endpoint address from Dify was bound to thirdparty webhook service like `Github` `Slack` `Linear` `GoogleDrive` `Gmail` etc. Once a subscription was created, Dify continually receives requests from the platforms and handle them one by one. - -## Worker Pool / Async Task - -All the events that triggered a new workflow run is always in async mode, a unified entrypoint can be found here `services/async_workflow_service.py`.`AsyncWorkflowService`.`trigger_workflow_async`. - -The infrastructure we used is `celery`, we've already configured it in `docker/entrypoint.sh`, and the consumers are in `tasks/async_workflow_tasks.py`, 3 queues were used to handle different tiers of users, `PROFESSIONAL_QUEUE` `TEAM_QUEUE` `SANDBOX_QUEUE`. - -## Debug Strategy - -Dify divided users into 2 groups: builders / end users. - -Builders are the users who create workflows, in this stage, debugging a workflow becomes a critical part of the workflow development process, as the start node in workflows, trigger nodes can `listen` to the events from `WebhookDebug` `Schedule` `Plugin`, debugging process was created in `controllers/console/app/workflow.py`.`DraftWorkflowTriggerNodeApi`. - -A polling process can be considered as combine of few single `poll` operations, each `poll` operation fetches events cached in `Redis`, returns `None` if no event was found, more detailed implemented: `core/trigger/debug/event_bus.py` was used to handle the polling process, and `core/trigger/debug/event_selectors.py` was used to select the event poller based on the trigger type.