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 01/16] 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 02/16] 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 03/16] 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 04/16] 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. From d4f5a113ed0f2db298ca2d701036d30ff4dbba03 Mon Sep 17 00:00:00 2001 From: Bowen Liang Date: Wed, 21 Jan 2026 15:07:32 +0800 Subject: [PATCH 05/16] chore(web): refactor next.config.js to next.config.ts (#31331) --- web/{next.config.js => next.config.ts} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename web/{next.config.js => next.config.ts} (88%) diff --git a/web/next.config.js b/web/next.config.ts similarity index 88% rename from web/next.config.js rename to web/next.config.ts index 1457d638c4..fc4dee3289 100644 --- a/web/next.config.js +++ b/web/next.config.ts @@ -1,3 +1,4 @@ +import type { NextConfig } from 'next' import process from 'node:process' import withBundleAnalyzerInit from '@next/bundle-analyzer' import createMDX from '@next/mdx' @@ -24,10 +25,9 @@ const withBundleAnalyzer = withBundleAnalyzerInit({ const hasSetWebPrefix = process.env.NEXT_PUBLIC_WEB_PREFIX const port = process.env.PORT || 3000 const locImageURLs = !hasSetWebPrefix ? [new URL(`http://localhost:${port}/**`), new URL(`http://127.0.0.1:${port}/**`)] : [] -const remoteImageURLs = [hasSetWebPrefix ? new URL(`${process.env.NEXT_PUBLIC_WEB_PREFIX}/**`) : '', ...locImageURLs].filter(item => !!item) +const remoteImageURLs = ([hasSetWebPrefix ? new URL(`${process.env.NEXT_PUBLIC_WEB_PREFIX}/**`) : '', ...locImageURLs].filter(item => !!item)) as URL[] -/** @type {import('next').NextConfig} */ -const nextConfig = { +const nextConfig: NextConfig = { basePath: process.env.NEXT_PUBLIC_BASE_PATH || '', serverExternalPackages: ['esbuild-wasm'], transpilePackages: ['echarts', 'zrender'], @@ -42,7 +42,7 @@ const nextConfig = { // https://nextjs.org/docs/messages/next-image-unconfigured-host images: { remotePatterns: remoteImageURLs.map(remoteImageURL => ({ - protocol: remoteImageURL.protocol.replace(':', ''), + protocol: remoteImageURL.protocol.replace(':', '') as 'http' | 'https', hostname: remoteImageURL.hostname, port: remoteImageURL.port, pathname: remoteImageURL.pathname, From ed0e068a47f90c1e5ce8118f1a43307eafa7c253 Mon Sep 17 00:00:00 2001 From: Coding On Star <447357187@qq.com> Date: Wed, 21 Jan 2026 15:47:49 +0800 Subject: [PATCH 06/16] fix(i18n): update model provider tip to only mention OpenAI in English, Japanese, and Simplified Chinese translations (#31339) Co-authored-by: CodingOnStar --- web/i18n/en-US/common.json | 2 +- web/i18n/ja-JP/common.json | 2 +- web/i18n/zh-Hans/common.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/i18n/en-US/common.json b/web/i18n/en-US/common.json index d2e5281282..20e5400e56 100644 --- a/web/i18n/en-US/common.json +++ b/web/i18n/en-US/common.json @@ -350,7 +350,7 @@ "modelProvider.card.quota": "QUOTA", "modelProvider.card.quotaExhausted": "Quota exhausted", "modelProvider.card.removeKey": "Remove API Key", - "modelProvider.card.tip": "Message Credits supports models from OpenAI, Anthropic, Gemini, xAI, DeepSeek and Tongyi. Priority will be given to the paid quota. The free quota will be used after the paid quota is exhausted.", + "modelProvider.card.tip": "Message Credits supports models from OpenAI. Priority will be given to the paid quota. The free quota will be used after the paid quota is exhausted.", "modelProvider.card.tokens": "Tokens", "modelProvider.collapse": "Collapse", "modelProvider.config": "Config", diff --git a/web/i18n/ja-JP/common.json b/web/i18n/ja-JP/common.json index ffc2d0bd31..8a76021759 100644 --- a/web/i18n/ja-JP/common.json +++ b/web/i18n/ja-JP/common.json @@ -350,7 +350,7 @@ "modelProvider.card.quota": "クォータ", "modelProvider.card.quotaExhausted": "クォータが使い果たされました", "modelProvider.card.removeKey": "API キーを削除", - "modelProvider.card.tip": "メッセージ枠はOpenAI、Anthropic、Gemini、xAI、DeepSeek、Tongyiのモデルを使用することをサポートしています。無料枠は有料枠が使い果たされた後に消費されます。", + "modelProvider.card.tip": "メッセージ枠はOpenAIのモデルを使用することをサポートしています。無料枠は有料枠が使い果たされた後に消費されます。", "modelProvider.card.tokens": "トークン", "modelProvider.collapse": "折り畳み", "modelProvider.config": "設定", diff --git a/web/i18n/zh-Hans/common.json b/web/i18n/zh-Hans/common.json index b5eabfeecc..6f62b53e2d 100644 --- a/web/i18n/zh-Hans/common.json +++ b/web/i18n/zh-Hans/common.json @@ -350,7 +350,7 @@ "modelProvider.card.quota": "额度", "modelProvider.card.quotaExhausted": "配额已用完", "modelProvider.card.removeKey": "删除 API 密钥", - "modelProvider.card.tip": "消息额度支持使用 OpenAI、Anthropic、Gemini、xAI、深度求索、通义 的模型;免费额度会在付费额度用尽后才会消耗。", + "modelProvider.card.tip": "消息额度支持使用 OpenAI 的模型;免费额度会在付费额度用尽后才会消耗。", "modelProvider.card.tokens": "Tokens", "modelProvider.collapse": "收起", "modelProvider.config": "配置", From 146ee4d3e9a9d000e993d05bb0dba1eaa00c9b36 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 16:15:58 +0800 Subject: [PATCH 07/16] chore(i18n): sync translations with en-US (#31332) Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> --- web/i18n/ar-TN/billing.json | 1 + web/i18n/de-DE/billing.json | 1 + web/i18n/es-ES/billing.json | 1 + web/i18n/fa-IR/billing.json | 1 + web/i18n/fr-FR/billing.json | 1 + web/i18n/hi-IN/billing.json | 1 + web/i18n/id-ID/billing.json | 1 + web/i18n/it-IT/billing.json | 1 + web/i18n/ko-KR/billing.json | 1 + web/i18n/pl-PL/billing.json | 1 + web/i18n/pt-BR/billing.json | 1 + web/i18n/ro-RO/billing.json | 1 + web/i18n/ru-RU/billing.json | 1 + web/i18n/sl-SI/billing.json | 1 + web/i18n/th-TH/billing.json | 1 + web/i18n/tr-TR/billing.json | 1 + web/i18n/uk-UA/billing.json | 1 + web/i18n/vi-VN/billing.json | 1 + web/i18n/zh-Hant/billing.json | 1 + 19 files changed, 19 insertions(+) diff --git a/web/i18n/ar-TN/billing.json b/web/i18n/ar-TN/billing.json index a67f8216a3..24bc5d2d58 100644 --- a/web/i18n/ar-TN/billing.json +++ b/web/i18n/ar-TN/billing.json @@ -172,6 +172,7 @@ "usagePage.documentsUploadQuota": "حصة رفع المستندات", "usagePage.perMonth": "شهريًا", "usagePage.resetsIn": "يتم إعادة التعيين في {{count,number}} أيام", + "usagePage.storageThresholdTooltip": "يتم عرض الاستخدام التفصيلي بمجرد أن تتجاوز مساحة التخزين 50 ميجابايت.", "usagePage.teamMembers": "أعضاء الفريق", "usagePage.triggerEvents": "أحداث المشغل", "usagePage.vectorSpace": "تخزين بيانات المعرفة", diff --git a/web/i18n/de-DE/billing.json b/web/i18n/de-DE/billing.json index 31d9150135..f44605984d 100644 --- a/web/i18n/de-DE/billing.json +++ b/web/i18n/de-DE/billing.json @@ -172,6 +172,7 @@ "usagePage.documentsUploadQuota": "Dokumenten-Upload-Quota", "usagePage.perMonth": "pro Monat", "usagePage.resetsIn": "Setzt in {{count,number}} Tagen zurück", + "usagePage.storageThresholdTooltip": "Die detaillierte Nutzung wird angezeigt, sobald der Speicher 50 MB überschreitet.", "usagePage.teamMembers": "Teammitglieder", "usagePage.triggerEvents": "Auslöser-Ereignisse", "usagePage.vectorSpace": "Wissensdatenbank", diff --git a/web/i18n/es-ES/billing.json b/web/i18n/es-ES/billing.json index 7e5c4ed1de..04150901fc 100644 --- a/web/i18n/es-ES/billing.json +++ b/web/i18n/es-ES/billing.json @@ -172,6 +172,7 @@ "usagePage.documentsUploadQuota": "Cuota de carga de documentos", "usagePage.perMonth": "por mes", "usagePage.resetsIn": "Se reinicia en {{count,number}} días", + "usagePage.storageThresholdTooltip": "El uso detallado se muestra una vez que el almacenamiento supera los 50 MB.", "usagePage.teamMembers": "Miembros del equipo", "usagePage.triggerEvents": "Eventos desencadenantes", "usagePage.vectorSpace": "Almacenamiento de Datos de Conocimiento", diff --git a/web/i18n/fa-IR/billing.json b/web/i18n/fa-IR/billing.json index 0cd2e28106..c7790411af 100644 --- a/web/i18n/fa-IR/billing.json +++ b/web/i18n/fa-IR/billing.json @@ -172,6 +172,7 @@ "usagePage.documentsUploadQuota": "حجم بارگذاری اسناد", "usagePage.perMonth": "در ماه", "usagePage.resetsIn": "در {{count,number}} روز بازنشانی می‌شود", + "usagePage.storageThresholdTooltip": "جزئیات استفاده زمانی نمایش داده می‌شود که فضای ذخیره‌سازی از 50 مگابایت بیشتر شود.", "usagePage.teamMembers": "اعضای تیم", "usagePage.triggerEvents": "رویدادهای محرک", "usagePage.vectorSpace": "ذخیره‌سازی داده‌های دانش", diff --git a/web/i18n/fr-FR/billing.json b/web/i18n/fr-FR/billing.json index 0c67b010d8..34db09bb32 100644 --- a/web/i18n/fr-FR/billing.json +++ b/web/i18n/fr-FR/billing.json @@ -172,6 +172,7 @@ "usagePage.documentsUploadQuota": "Quota de téléchargement de documents", "usagePage.perMonth": "par mois", "usagePage.resetsIn": "Réinitialisations dans {{count,number}} jours", + "usagePage.storageThresholdTooltip": "L'utilisation détaillée est affichée lorsque le stockage dépasse 50 Mo.", "usagePage.teamMembers": "Membres de l'équipe", "usagePage.triggerEvents": "Événements déclencheurs", "usagePage.vectorSpace": "Stockage de données de connaissance", diff --git a/web/i18n/hi-IN/billing.json b/web/i18n/hi-IN/billing.json index 37c6555640..5ab7130ad0 100644 --- a/web/i18n/hi-IN/billing.json +++ b/web/i18n/hi-IN/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/id-ID/billing.json b/web/i18n/id-ID/billing.json index f912cf1960..920836d467 100644 --- a/web/i18n/id-ID/billing.json +++ b/web/i18n/id-ID/billing.json @@ -172,6 +172,7 @@ "usagePage.documentsUploadQuota": "Kuota Unggah Dokumen", "usagePage.perMonth": "per bulan", "usagePage.resetsIn": "Diatur ulang dalam {{count,number}} hari", + "usagePage.storageThresholdTooltip": "Penggunaan terperinci ditampilkan setelah penyimpanan melebihi 50 MB.", "usagePage.teamMembers": "Anggota Tim", "usagePage.triggerEvents": "Pemicu Acara", "usagePage.vectorSpace": "Penyimpanan Data Pengetahuan", diff --git a/web/i18n/it-IT/billing.json b/web/i18n/it-IT/billing.json index fdf2547374..7626232646 100644 --- a/web/i18n/it-IT/billing.json +++ b/web/i18n/it-IT/billing.json @@ -172,6 +172,7 @@ "usagePage.documentsUploadQuota": "Quota di Caricamento Documenti", "usagePage.perMonth": "al mese", "usagePage.resetsIn": "Si resetta tra {{count,number}} giorni", + "usagePage.storageThresholdTooltip": "L'utilizzo dettagliato viene visualizzato quando lo spazio di archiviazione supera i 50 MB.", "usagePage.teamMembers": "Membri del team", "usagePage.triggerEvents": "Eventi di attivazione", "usagePage.vectorSpace": "Archiviazione dei dati conoscitivi", diff --git a/web/i18n/ko-KR/billing.json b/web/i18n/ko-KR/billing.json index 318435d63d..602f5c7407 100644 --- a/web/i18n/ko-KR/billing.json +++ b/web/i18n/ko-KR/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/pl-PL/billing.json b/web/i18n/pl-PL/billing.json index 913778e91d..62381ab9ba 100644 --- a/web/i18n/pl-PL/billing.json +++ b/web/i18n/pl-PL/billing.json @@ -172,6 +172,7 @@ "usagePage.documentsUploadQuota": "Limit przesyłania dokumentów", "usagePage.perMonth": "miesięcznie", "usagePage.resetsIn": "Resetuje się za {{count,number}} dni", + "usagePage.storageThresholdTooltip": "Szczegółowe użycie jest wyświetlane, gdy przestrzeń dyskowa przekracza 50 MB.", "usagePage.teamMembers": "Członkowie zespołu", "usagePage.triggerEvents": "Wydarzenia wyzwalające", "usagePage.vectorSpace": "Magazynowanie danych wiedzy", diff --git a/web/i18n/pt-BR/billing.json b/web/i18n/pt-BR/billing.json index 8e447d0c17..6a4a0b0eb1 100644 --- a/web/i18n/pt-BR/billing.json +++ b/web/i18n/pt-BR/billing.json @@ -172,6 +172,7 @@ "usagePage.documentsUploadQuota": "Cota de Upload de Documentos", "usagePage.perMonth": "por mês", "usagePage.resetsIn": "Reinicia em {{count,number}} dias", + "usagePage.storageThresholdTooltip": "O uso detalhado é exibido quando o armazenamento excede 50 MB.", "usagePage.teamMembers": "Membros da equipe", "usagePage.triggerEvents": "Eventos de Gatilho", "usagePage.vectorSpace": "Armazenamento de Dados do Conhecimento", diff --git a/web/i18n/ro-RO/billing.json b/web/i18n/ro-RO/billing.json index 99fcb93a4e..41bb429905 100644 --- a/web/i18n/ro-RO/billing.json +++ b/web/i18n/ro-RO/billing.json @@ -172,6 +172,7 @@ "usagePage.documentsUploadQuota": "Cota de încărcare a documentelor", "usagePage.perMonth": "pe lună", "usagePage.resetsIn": "Se resetează în {{count,number}} zile", + "usagePage.storageThresholdTooltip": "Utilizarea detaliată este afișată odată ce spațiul de stocare depășește 50 MB.", "usagePage.teamMembers": "Membrii echipei", "usagePage.triggerEvents": "Evenimente declanșatoare", "usagePage.vectorSpace": "Stocarea datelor de cunoștințe", diff --git a/web/i18n/ru-RU/billing.json b/web/i18n/ru-RU/billing.json index 722953747e..c5a526418a 100644 --- a/web/i18n/ru-RU/billing.json +++ b/web/i18n/ru-RU/billing.json @@ -172,6 +172,7 @@ "usagePage.documentsUploadQuota": "Квота на загрузку документов", "usagePage.perMonth": "в месяц", "usagePage.resetsIn": "Сброс через {{count,number}} дней", + "usagePage.storageThresholdTooltip": "Подробные данные об использовании отображаются после превышения 50 МБ хранилища.", "usagePage.teamMembers": "Члены команды", "usagePage.triggerEvents": "Триггерные события", "usagePage.vectorSpace": "Хранилище данных знаний", diff --git a/web/i18n/sl-SI/billing.json b/web/i18n/sl-SI/billing.json index c9bbbf8043..6409a7aedb 100644 --- a/web/i18n/sl-SI/billing.json +++ b/web/i18n/sl-SI/billing.json @@ -172,6 +172,7 @@ "usagePage.documentsUploadQuota": "Kvota za nalaganje dokumentov", "usagePage.perMonth": "na mesec", "usagePage.resetsIn": "Ponastavitve čez {{count,number}} dni", + "usagePage.storageThresholdTooltip": "Podrobna uporaba se prikaže, ko shramba preseže 50 MB.", "usagePage.teamMembers": "Člani ekipe", "usagePage.triggerEvents": "Sprožilni dogodki", "usagePage.vectorSpace": "Shranjevanje podatkov znanja", diff --git a/web/i18n/th-TH/billing.json b/web/i18n/th-TH/billing.json index ec1cbf501f..b0d6eadafd 100644 --- a/web/i18n/th-TH/billing.json +++ b/web/i18n/th-TH/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/tr-TR/billing.json b/web/i18n/tr-TR/billing.json index 036f3e98c3..b780045768 100644 --- a/web/i18n/tr-TR/billing.json +++ b/web/i18n/tr-TR/billing.json @@ -172,6 +172,7 @@ "usagePage.documentsUploadQuota": "Belgeler Yükleme Kotası", "usagePage.perMonth": "ayda", "usagePage.resetsIn": "{{count,number}} gün içinde sıfırlanır", + "usagePage.storageThresholdTooltip": "Depolama alanı 50 MB'yi aştığında ayrıntılı kullanım gösterilir.", "usagePage.teamMembers": "Ekip Üyeleri", "usagePage.triggerEvents": "Tetikleyici Olaylar", "usagePage.vectorSpace": "Bilgi Veri Depolama", diff --git a/web/i18n/uk-UA/billing.json b/web/i18n/uk-UA/billing.json index 7fe974c96e..223eccb699 100644 --- a/web/i18n/uk-UA/billing.json +++ b/web/i18n/uk-UA/billing.json @@ -172,6 +172,7 @@ "usagePage.documentsUploadQuota": "Квота на завантаження документів", "usagePage.perMonth": "на місяць", "usagePage.resetsIn": "Скидання через {{count,number}} днів", + "usagePage.storageThresholdTooltip": "Детальне використання відображається після перевищення 50 МБ сховища.", "usagePage.teamMembers": "Члени команди", "usagePage.triggerEvents": "Тригерні події", "usagePage.vectorSpace": "Сховище даних знань", diff --git a/web/i18n/vi-VN/billing.json b/web/i18n/vi-VN/billing.json index ca792318fa..e111c94082 100644 --- a/web/i18n/vi-VN/billing.json +++ b/web/i18n/vi-VN/billing.json @@ -172,6 +172,7 @@ "usagePage.documentsUploadQuota": "Hạn ngạch tải lên tài liệu", "usagePage.perMonth": "mỗi tháng", "usagePage.resetsIn": "Đặt lại sau {{count,number}} ngày", + "usagePage.storageThresholdTooltip": "Thông tin sử dụng chi tiết sẽ được hiển thị khi dung lượng lưu trữ vượt quá 50 MB.", "usagePage.teamMembers": "Các thành viên trong nhóm", "usagePage.triggerEvents": "Các sự kiện kích hoạt", "usagePage.vectorSpace": "Lưu trữ dữ liệu kiến thức", diff --git a/web/i18n/zh-Hant/billing.json b/web/i18n/zh-Hant/billing.json index 1b343d814a..20277ca50c 100644 --- a/web/i18n/zh-Hant/billing.json +++ b/web/i18n/zh-Hant/billing.json @@ -172,6 +172,7 @@ "usagePage.documentsUploadQuota": "文件上傳配額", "usagePage.perMonth": "每月", "usagePage.resetsIn": "{{count,number}} 天後重置", + "usagePage.storageThresholdTooltip": "儲存空間超過 50 MB 後,將顯示詳細使用情況。", "usagePage.teamMembers": "團隊成員", "usagePage.triggerEvents": "觸發事件", "usagePage.vectorSpace": "知識數據儲存", From 061feebd87099f0bc0129d7e61d720cf88a6f96e Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Wed, 21 Jan 2026 16:31:48 +0800 Subject: [PATCH 08/16] fix: check and update doc links (#30849) Co-authored-by: Riskey <36894937+RiskeyL@users.noreply.github.com> --- web/README.md | 2 +- .../[appId]/overview/card-view.tsx | 17 +- .../conversation-history/history-panel.tsx | 11 - .../settings-modal/retrieval-section.spec.tsx | 9 +- .../settings-modal/retrieval-section.tsx | 5 +- .../tools/external-data-tool-modal.tsx | 2 +- .../components/app/create-app-modal/index.tsx | 18 - web/app/components/app/overview/app-card.tsx | 2 +- .../app/overview/customize/index.tsx | 2 +- .../app/overview/settings/index.tsx | 12 - .../components/app/overview/trigger-card.tsx | 2 +- .../base/features/new-feature-panel/index.tsx | 11 - .../moderation/moderation-setting-modal.tsx | 2 +- .../datasets/create/step-three/index.spec.tsx | 2 +- .../datasets/create/step-three/index.tsx | 2 +- .../components/indexing-mode-section.tsx | 2 +- .../documents/components/documents-header.tsx | 2 +- .../data-source/online-documents/index.tsx | 2 +- .../data-source/online-drive/index.spec.tsx | 2 +- .../data-source/online-drive/index.tsx | 2 +- .../data-source/website-crawl/index.tsx | 2 +- .../processing/index.spec.tsx | 2 +- .../create-from-pipeline/processing/index.tsx | 2 +- .../external-api/external-api-modal/Form.tsx | 2 +- .../external-api-panel/index.spec.tsx | 2 +- .../external-api/external-api-panel/index.tsx | 2 +- .../create/InfoPanel.tsx | 4 +- .../create/index.spec.tsx | 2 +- .../external-knowledge-base/create/index.tsx | 2 +- .../hit-testing/modify-retrieval-modal.tsx | 5 +- .../datasets/no-linked-apps-panel.tsx | 2 +- .../datasets/settings/form/index.tsx | 7 +- .../header/account-dropdown/index.tsx | 2 +- .../api-based-extension-page/empty.tsx | 2 +- .../api-based-extension-page/modal.tsx | 2 +- .../plugin-detail-panel/endpoint-list.tsx | 2 +- .../plugins/plugin-page/debug-info.tsx | 7 +- .../components/plugins/plugin-page/index.tsx | 7 +- web/app/components/plugins/utils.ts | 13 - .../rag-pipeline-header/publisher/popup.tsx | 4 +- .../hooks/use-available-nodes-meta-data.ts | 15 +- web/app/components/tools/mcp/create-card.tsx | 14 +- .../components/tools/mcp/mcp-service-card.tsx | 2 +- .../tools/provider/custom-create-card.tsx | 22 +- .../workflow-onboarding-modal/index.tsx | 11 - .../hooks/use-available-nodes-meta-data.ts | 3 +- .../nodes/_base/components/agent-strategy.tsx | 5 +- .../components/error-handle/default-value.tsx | 11 - .../error-handle/fail-branch-card.tsx | 2 +- .../variable/var-reference-popup.tsx | 14 +- .../chunk-structure/instruction/index.tsx | 2 +- .../components/retrieval-setting/index.tsx | 4 +- .../json-schema-config.tsx | 14 +- .../panel/chat-variable-panel/index.tsx | 13 - web/app/components/workflow/run/node.tsx | 2 +- web/app/components/workflow/run/status.tsx | 2 +- .../workflow/variable-inspect/empty.tsx | 4 +- .../education-apply/education-apply-page.tsx | 2 +- .../education-apply/expire-notice-modal.tsx | 2 +- .../education-apply/verify-state-modal.tsx | 2 +- web/app/install/installForm.tsx | 5 +- web/app/signin/invite-settings/page.tsx | 5 +- web/app/signin/one-more-step.tsx | 6 +- web/constants/link.ts | 1 + web/context/i18n.ts | 19 +- web/eslint.config.mjs | 2 +- web/hooks/use-api-access-url.ts | 15 +- web/i18n-config/language.ts | 9 +- web/package.json | 1 + web/scripts/gen-doc-paths.ts | 433 ++++++++++++++++++ web/types/doc-paths.ts | 316 +++++++++++++ 71 files changed, 858 insertions(+), 282 deletions(-) create mode 100644 web/constants/link.ts create mode 100644 web/scripts/gen-doc-paths.ts create mode 100644 web/types/doc-paths.ts diff --git a/web/README.md b/web/README.md index 13780eec6c..9c731a081a 100644 --- a/web/README.md +++ b/web/README.md @@ -138,7 +138,7 @@ This will help you determine the testing strategy. See [web/testing/testing.md]( ## Documentation -Visit to view the full documentation. +Visit to view the full documentation. ## Community diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx index 81b4f2474e..f07b2932c9 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx @@ -5,7 +5,6 @@ import type { BlockEnum } from '@/app/components/workflow/types' import type { UpdateAppSiteCodeResponse } from '@/models/app' import type { App } from '@/types/app' import type { I18nKeysByPrefix } from '@/types/i18n' -import * as React from 'react' import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' @@ -17,7 +16,6 @@ import { ToastContext } from '@/app/components/base/toast' import MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card' import { isTriggerNode } from '@/app/components/workflow/types' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' -import { useDocLink } from '@/context/i18n' import { fetchAppDetail, updateAppSiteAccessToken, @@ -36,7 +34,6 @@ export type ICardViewProps = { const CardView: FC = ({ appId, isInPanel, className }) => { const { t } = useTranslation() - const docLink = useDocLink() const { notify } = useContext(ToastContext) const appDetail = useAppStore(state => state.appDetail) const setAppDetail = useAppStore(state => state.setAppDetail) @@ -59,25 +56,13 @@ const CardView: FC = ({ appId, isInPanel, className }) => { const shouldRenderAppCards = !isWorkflowApp || hasTriggerNode === false const disableAppCards = !shouldRenderAppCards - const triggerDocUrl = docLink('/guides/workflow/node/start') const buildTriggerModeMessage = useCallback((featureName: string) => (
{t('overview.disableTooltip.triggerMode', { ns: 'appOverview', feature: featureName })}
- { - event.stopPropagation() - }} - > - {t('overview.appInfo.enableTooltip.learnMore', { ns: 'appOverview' })} -
- ), [t, triggerDocUrl]) + ), [t]) const disableWebAppTooltip = disableAppCards ? buildTriggerModeMessage(t('overview.appInfo.title', { ns: 'appOverview' })) diff --git a/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.tsx b/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.tsx index b0b042b2a5..44c4fc8f46 100644 --- a/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.tsx +++ b/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.tsx @@ -5,7 +5,6 @@ import { useTranslation } from 'react-i18next' import Panel from '@/app/components/app/configuration/base/feature-panel' import OperationBtn from '@/app/components/app/configuration/base/operation-btn' import { MessageClockCircle } from '@/app/components/base/icons/src/vender/solid/general' -import { useDocLink } from '@/context/i18n' type Props = { showWarning: boolean @@ -17,8 +16,6 @@ const HistoryPanel: FC = ({ onShowEditModal, }) => { const { t } = useTranslation() - const docLink = useDocLink() - return ( = ({
{t('feature.conversationHistory.tip', { ns: 'appDebug' })} - - {t('feature.conversationHistory.learnMore', { ns: 'appDebug' })} -
)} diff --git a/web/app/components/app/configuration/dataset-config/settings-modal/retrieval-section.spec.tsx b/web/app/components/app/configuration/dataset-config/settings-modal/retrieval-section.spec.tsx index 0d7b705d9e..2140afe1dd 100644 --- a/web/app/components/app/configuration/dataset-config/settings-modal/retrieval-section.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/settings-modal/retrieval-section.spec.tsx @@ -1,5 +1,6 @@ import type { DataSet } from '@/models/datasets' import type { RetrievalConfig } from '@/types/app' +import type { DocPathWithoutLang } from '@/types/doc-paths' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { IndexingType } from '@/app/components/datasets/create/step-two' @@ -237,15 +238,15 @@ describe('RetrievalSection', () => { retrievalConfig={retrievalConfig} showMultiModalTip onRetrievalConfigChange={vi.fn()} - docLink={docLink} + docLink={docLink as unknown as (path?: DocPathWithoutLang) => string} />, ) // Assert expect(screen.getByText('dataset.retrieval.semantic_search.title')).toBeInTheDocument() const learnMoreLink = screen.getByRole('link', { name: 'datasetSettings.form.retrievalSetting.learnMore' }) - expect(learnMoreLink).toHaveAttribute('href', 'https://docs.example/guides/knowledge-base/create-knowledge-and-upload-documents/setting-indexing-methods#setting-the-retrieval-setting') - expect(docLink).toHaveBeenCalledWith('/guides/knowledge-base/create-knowledge-and-upload-documents/setting-indexing-methods#setting-the-retrieval-setting') + expect(learnMoreLink).toHaveAttribute('href', 'https://docs.example/use-dify/knowledge/create-knowledge/setting-indexing-methods') + expect(docLink).toHaveBeenCalledWith('/use-dify/knowledge/create-knowledge/setting-indexing-methods') }) it('propagates retrieval config changes for economical indexing', async () => { @@ -263,7 +264,7 @@ describe('RetrievalSection', () => { retrievalConfig={createRetrievalConfig()} showMultiModalTip={false} onRetrievalConfigChange={handleRetrievalChange} - docLink={path => path} + docLink={path => path || ''} />, ) const [topKIncrement] = screen.getAllByLabelText('increment') diff --git a/web/app/components/app/configuration/dataset-config/settings-modal/retrieval-section.tsx b/web/app/components/app/configuration/dataset-config/settings-modal/retrieval-section.tsx index 6c9bd14d1e..6d478de908 100644 --- a/web/app/components/app/configuration/dataset-config/settings-modal/retrieval-section.tsx +++ b/web/app/components/app/configuration/dataset-config/settings-modal/retrieval-section.tsx @@ -1,6 +1,7 @@ import type { FC } from 'react' import type { DataSet } from '@/models/datasets' import type { RetrievalConfig } from '@/types/app' +import type { DocPathWithoutLang } from '@/types/doc-paths' import { RiCloseLine } from '@remixicon/react' import Divider from '@/app/components/base/divider' import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' @@ -84,7 +85,7 @@ type InternalRetrievalSectionProps = CommonSectionProps & { retrievalConfig: RetrievalConfig showMultiModalTip: boolean onRetrievalConfigChange: (value: RetrievalConfig) => void - docLink: (path: string) => string + docLink: (path?: DocPathWithoutLang) => string } const InternalRetrievalSection: FC = ({ @@ -102,7 +103,7 @@ const InternalRetrievalSection: FC = ({
{t('form.retrievalSetting.title', { ns: 'datasetSettings' })}
diff --git a/web/app/components/app/configuration/tools/external-data-tool-modal.tsx b/web/app/components/app/configuration/tools/external-data-tool-modal.tsx index 71827c4e0d..fece5598e1 100644 --- a/web/app/components/app/configuration/tools/external-data-tool-modal.tsx +++ b/web/app/components/app/configuration/tools/external-data-tool-modal.tsx @@ -240,7 +240,7 @@ const ExternalDataToolModal: FC = ({ ) diff --git a/web/app/components/app/overview/app-card.tsx b/web/app/components/app/overview/app-card.tsx index c1a662df5d..9975c81b3e 100644 --- a/web/app/components/app/overview/app-card.tsx +++ b/web/app/components/app/overview/app-card.tsx @@ -245,7 +245,7 @@ function AppCard({
window.open(docLink('/guides/workflow/node/user-input'), '_blank')} + onClick={() => window.open(docLink('/use-dify/nodes/user-input'), '_blank')} > {t('overview.appInfo.enableTooltip.learnMore', { ns: 'appOverview' })}
diff --git a/web/app/components/app/overview/customize/index.tsx b/web/app/components/app/overview/customize/index.tsx index 77dae81a01..c7391abe3d 100644 --- a/web/app/components/app/overview/customize/index.tsx +++ b/web/app/components/app/overview/customize/index.tsx @@ -118,7 +118,7 @@ const CustomizeModal: FC = ({ className="mt-2" onClick={() => window.open( - docLink('/guides/application-publishing/developing-with-apis'), + docLink('/use-dify/publish/developing-with-apis'), '_blank', )} > diff --git a/web/app/components/app/overview/settings/index.tsx b/web/app/components/app/overview/settings/index.tsx index 428a475da9..0d087e27c2 100644 --- a/web/app/components/app/overview/settings/index.tsx +++ b/web/app/components/app/overview/settings/index.tsx @@ -23,7 +23,6 @@ import Textarea from '@/app/components/base/textarea' import { useToastContext } from '@/app/components/base/toast' import Tooltip from '@/app/components/base/tooltip' import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants' -import { useDocLink } from '@/context/i18n' import { useModalContext } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' import { languages } from '@/i18n-config/language' @@ -100,7 +99,6 @@ const SettingsModal: FC = ({ const [language, setLanguage] = useState(default_language) const [saveLoading, setSaveLoading] = useState(false) const { t } = useTranslation() - const docLink = useDocLink() const [showAppIconPicker, setShowAppIconPicker] = useState(false) const [appIcon, setAppIcon] = useState( @@ -240,16 +238,6 @@ const SettingsModal: FC = ({
{t(`${prefixSettings}.modalTip`, { ns: 'appOverview' })} - - {t('operation.learnMore', { ns: 'common' })} -
{/* form body */} diff --git a/web/app/components/app/overview/trigger-card.tsx b/web/app/components/app/overview/trigger-card.tsx index a2d28606a1..12a294b4ec 100644 --- a/web/app/components/app/overview/trigger-card.tsx +++ b/web/app/components/app/overview/trigger-card.tsx @@ -208,7 +208,7 @@ function TriggerCard({ appInfo, onToggleResult }: ITriggerCardProps) { {t('overview.triggerInfo.triggerStatusDescription', { ns: 'appOverview' })} {' '} { const { t } = useTranslation() - const docLink = useDocLink() const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text) const { data: text2speechDefaultModel } = useDefaultModel(ModelTypeEnum.tts) @@ -76,14 +73,6 @@ const NewFeaturePanel = ({
diff --git a/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx b/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx index 59b62d0bfd..c9455c98eb 100644 --- a/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx +++ b/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx @@ -319,7 +319,7 @@ const ModerationSettingModal: FC = ({
{t('apiBasedExtension.selector.title', { ns: 'common' })}
{ // Assert const link = screen.getByText('datasetPipeline.addDocuments.stepThree.learnMore') - expect(link).toHaveAttribute('href', 'https://docs.dify.ai/en-US/guides/knowledge-base/integrate-knowledge-within-application') + expect(link).toHaveAttribute('href', 'https://docs.dify.ai/en-US/use-dify/knowledge/integrate-knowledge-within-application') expect(link).toHaveAttribute('target', '_blank') expect(link).toHaveAttribute('rel', 'noreferrer noopener') }) diff --git a/web/app/components/datasets/create/step-three/index.tsx b/web/app/components/datasets/create/step-three/index.tsx index ad26711311..5ab21f6302 100644 --- a/web/app/components/datasets/create/step-three/index.tsx +++ b/web/app/components/datasets/create/step-three/index.tsx @@ -87,7 +87,7 @@ const StepThree = ({ datasetId, datasetName, indexingType, creationCache, retrie
{t('stepThree.sideTipTitle', { ns: 'datasetCreation' })}
{t('stepThree.sideTipContent', { ns: 'datasetCreation' })}
= ({ {t('form.retrievalSetting.learnMore', { ns: 'datasetSettings' })} diff --git a/web/app/components/datasets/documents/components/documents-header.tsx b/web/app/components/datasets/documents/components/documents-header.tsx index ed97742fdd..490893d43f 100644 --- a/web/app/components/datasets/documents/components/documents-header.tsx +++ b/web/app/components/datasets/documents/components/documents-header.tsx @@ -121,7 +121,7 @@ const DocumentsHeader: FC = ({ className="flex items-center text-text-accent" target="_blank" rel="noopener noreferrer" - href={docLink('/guides/knowledge-base/integrate-knowledge-within-application')} + href={docLink('/use-dify/knowledge/integrate-knowledge-within-application')} > {t('list.learnMore', { ns: 'datasetDocuments' })} diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx index 9b0df231bd..4bdaac895b 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx @@ -138,7 +138,7 @@ const OnlineDocuments = ({
{ render() // Assert - expect(mockDocLink).toHaveBeenCalledWith('/guides/knowledge-base/knowledge-pipeline/authorize-data-source') + expect(mockDocLink).toHaveBeenCalledWith('/use-dify/knowledge/knowledge-pipeline/authorize-data-source') }) }) diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/index.tsx index 508745aaeb..4346a2d0af 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/index.tsx @@ -196,7 +196,7 @@ const OnlineDrive = ({
{ // Assert const link = screen.getByRole('link', { name: 'datasetPipeline.addDocuments.stepThree.learnMore' }) - expect(link).toHaveAttribute('href', 'https://docs.dify.ai/en-US/guides/knowledge-base/integrate-knowledge-within-application') + expect(link).toHaveAttribute('href', 'https://docs.dify.ai/en-US/use-dify/knowledge/knowledge-pipeline/authorize-data-source') expect(link).toHaveAttribute('target', '_blank') expect(link).toHaveAttribute('rel', 'noreferrer noopener') }) diff --git a/web/app/components/datasets/documents/create-from-pipeline/processing/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/processing/index.tsx index 97c8937442..283600fa69 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/processing/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/processing/index.tsx @@ -44,7 +44,7 @@ const Processing = ({
{t('stepThree.sideTipTitle', { ns: 'datasetCreation' })}
{t('stepThree.sideTipContent', { ns: 'datasetCreation' })}
= React.memo(({ {variable === 'endpoint' && ( { render() const docLink = screen.getByText('dataset.externalAPIPanelDocumentation') expect(docLink).toBeInTheDocument() - expect(docLink.closest('a')).toHaveAttribute('href', 'https://docs.example.com/guides/knowledge-base/connect-external-knowledge-base') + expect(docLink.closest('a')).toHaveAttribute('href', 'https://docs.example.com/use-dify/knowledge/connect-external-knowledge-base') }) it('should render create button', () => { diff --git a/web/app/components/datasets/external-api/external-api-panel/index.tsx b/web/app/components/datasets/external-api/external-api-panel/index.tsx index a137348626..c37ff20ba7 100644 --- a/web/app/components/datasets/external-api/external-api-panel/index.tsx +++ b/web/app/components/datasets/external-api/external-api-panel/index.tsx @@ -54,7 +54,7 @@ const ExternalAPIPanel: React.FC = ({ onClose }) => {
{t('externalAPIPanelDescription', { ns: 'dataset' })}
diff --git a/web/app/components/datasets/external-knowledge-base/create/InfoPanel.tsx b/web/app/components/datasets/external-knowledge-base/create/InfoPanel.tsx index beb6a3cf71..61b37a0a1d 100644 --- a/web/app/components/datasets/external-knowledge-base/create/InfoPanel.tsx +++ b/web/app/components/datasets/external-knowledge-base/create/InfoPanel.tsx @@ -18,14 +18,14 @@ const InfoPanel = () => { {t('connectDatasetIntro.content.front', { ns: 'dataset' })} - + {t('connectDatasetIntro.content.link', { ns: 'dataset' })} {t('connectDatasetIntro.content.end', { ns: 'dataset' })} diff --git a/web/app/components/datasets/external-knowledge-base/create/index.spec.tsx b/web/app/components/datasets/external-knowledge-base/create/index.spec.tsx index 2fce096cd5..d56833fd36 100644 --- a/web/app/components/datasets/external-knowledge-base/create/index.spec.tsx +++ b/web/app/components/datasets/external-knowledge-base/create/index.spec.tsx @@ -146,7 +146,7 @@ describe('ExternalKnowledgeBaseCreate', () => { renderComponent() const docLink = screen.getByText('dataset.connectHelper.helper4') - expect(docLink).toHaveAttribute('href', 'https://docs.dify.ai/en/guides/knowledge-base/connect-external-knowledge-base') + expect(docLink).toHaveAttribute('href', 'https://docs.dify.ai/en/use-dify/knowledge/connect-external-knowledge-base') expect(docLink).toHaveAttribute('target', '_blank') expect(docLink).toHaveAttribute('rel', 'noopener noreferrer') }) diff --git a/web/app/components/datasets/external-knowledge-base/create/index.tsx b/web/app/components/datasets/external-knowledge-base/create/index.tsx index 1d17b23b43..07b6e71fa6 100644 --- a/web/app/components/datasets/external-knowledge-base/create/index.tsx +++ b/web/app/components/datasets/external-knowledge-base/create/index.tsx @@ -61,7 +61,7 @@ const ExternalKnowledgeBaseCreate: React.FC = {t('connectHelper.helper1', { ns: 'dataset' })} {t('connectHelper.helper2', { ns: 'dataset' })} {t('connectHelper.helper3', { ns: 'dataset' })} - + {t('connectHelper.helper4', { ns: 'dataset' })} diff --git a/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx b/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx index d21297fc93..a942c402ed 100644 --- a/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx +++ b/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx @@ -96,10 +96,7 @@ const ModifyRetrievalModal: FC = ({ {t('form.retrievalSetting.learnMore', { ns: 'datasetSettings' })} diff --git a/web/app/components/datasets/no-linked-apps-panel.tsx b/web/app/components/datasets/no-linked-apps-panel.tsx index 1b0357bc6a..12e87a7379 100644 --- a/web/app/components/datasets/no-linked-apps-panel.tsx +++ b/web/app/components/datasets/no-linked-apps-panel.tsx @@ -15,7 +15,7 @@ const NoLinkedAppsPanel = () => {
{t('datasetMenus.emptyTip', { ns: 'common' })}
diff --git a/web/app/components/datasets/settings/form/index.tsx b/web/app/components/datasets/settings/form/index.tsx index 5fbaefade7..a25d770518 100644 --- a/web/app/components/datasets/settings/form/index.tsx +++ b/web/app/components/datasets/settings/form/index.tsx @@ -281,7 +281,7 @@ const Form = () => { {t('form.chunkStructure.learnMore', { ns: 'datasetSettings' })} @@ -421,10 +421,7 @@ const Form = () => { {t('form.retrievalSetting.learnMore', { ns: 'datasetSettings' })} diff --git a/web/app/components/header/account-dropdown/index.tsx b/web/app/components/header/account-dropdown/index.tsx index e16c00acd0..07dd0fca3d 100644 --- a/web/app/components/header/account-dropdown/index.tsx +++ b/web/app/components/header/account-dropdown/index.tsx @@ -137,7 +137,7 @@ export default function AppSelector() { diff --git a/web/app/components/header/account-setting/api-based-extension-page/empty.tsx b/web/app/components/header/account-setting/api-based-extension-page/empty.tsx index 38525993fa..d75e66f8d0 100644 --- a/web/app/components/header/account-setting/api-based-extension-page/empty.tsx +++ b/web/app/components/header/account-setting/api-based-extension-page/empty.tsx @@ -17,7 +17,7 @@ const Empty = () => {
{t('apiBasedExtension.title', { ns: 'common' })}
diff --git a/web/app/components/header/account-setting/api-based-extension-page/modal.tsx b/web/app/components/header/account-setting/api-based-extension-page/modal.tsx index d3146d7baa..f35986dbb0 100644 --- a/web/app/components/header/account-setting/api-based-extension-page/modal.tsx +++ b/web/app/components/header/account-setting/api-based-extension-page/modal.tsx @@ -102,7 +102,7 @@ const ApiBasedExtensionModal: FC = ({
{t('detailPanel.endpointsTip', { ns: 'plugin' })}
diff --git a/web/app/components/plugins/plugin-page/debug-info.tsx b/web/app/components/plugins/plugin-page/debug-info.tsx index f62f8a4134..f3eed424f4 100644 --- a/web/app/components/plugins/plugin-page/debug-info.tsx +++ b/web/app/components/plugins/plugin-page/debug-info.tsx @@ -8,8 +8,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import Tooltip from '@/app/components/base/tooltip' -import { getDocsUrl } from '@/app/components/plugins/utils' -import { useLocale } from '@/context/i18n' +import { useDocLink } from '@/context/i18n' import { useDebugKey } from '@/service/use-plugins' import KeyValueItem from '../base/key-value-item' @@ -17,7 +16,7 @@ const i18nPrefix = 'debugInfo' const DebugInfo: FC = () => { const { t } = useTranslation() - const locale = useLocale() + const docLink = useDocLink() const { data: info, isLoading } = useDebugKey() // info.key likes 4580bdb7-b878-471c-a8a4-bfd760263a53 mask the middle part using *. @@ -34,7 +33,7 @@ const DebugInfo: FC = () => { <>
{t(`${i18nPrefix}.title`, { ns: 'plugin' })} - + {t(`${i18nPrefix}.viewDocs`, { ns: 'plugin' })} diff --git a/web/app/components/plugins/plugin-page/index.tsx b/web/app/components/plugins/plugin-page/index.tsx index d852e4d0b8..efb665197a 100644 --- a/web/app/components/plugins/plugin-page/index.tsx +++ b/web/app/components/plugins/plugin-page/index.tsx @@ -15,10 +15,9 @@ import Button from '@/app/components/base/button' import TabSlider from '@/app/components/base/tab-slider' import Tooltip from '@/app/components/base/tooltip' import ReferenceSettingModal from '@/app/components/plugins/reference-setting-modal' -import { getDocsUrl } from '@/app/components/plugins/utils' import { MARKETPLACE_API_PREFIX, SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS } from '@/config' import { useGlobalPublicStore } from '@/context/global-public-context' -import { useLocale } from '@/context/i18n' +import { useDocLink } from '@/context/i18n' import useDocumentTitle from '@/hooks/use-document-title' import { usePluginInstallation } from '@/hooks/use-query-params' import { fetchBundleInfoFromMarketPlace, fetchManifestFromMarketPlace } from '@/service/plugins' @@ -47,7 +46,7 @@ const PluginPage = ({ marketplace, }: PluginPageProps) => { const { t } = useTranslation() - const locale = useLocale() + const docLink = useDocLink() useDocumentTitle(t('metadata.title', { ns: 'plugin' })) // Use nuqs hook for installation state @@ -175,7 +174,7 @@ const PluginPage = ({
window.open(docLink('/guides/workflow/node/user-input'), '_blank')} + onClick={() => window.open(docLink('/use-dify/nodes/user-input'), '_blank')} > {t('overview.appInfo.enableTooltip.learnMore', { ns: 'appOverview' })}
diff --git a/web/app/components/tools/provider/custom-create-card.tsx b/web/app/components/tools/provider/custom-create-card.tsx index 637d17c3c3..bf86a1f833 100644 --- a/web/app/components/tools/provider/custom-create-card.tsx +++ b/web/app/components/tools/provider/custom-create-card.tsx @@ -2,16 +2,12 @@ import type { CustomCollectionBackend } from '../types' import { RiAddCircleFill, - RiArrowRightUpLine, - RiBookOpenLine, } from '@remixicon/react' -import { useMemo, useState } from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import Toast from '@/app/components/base/toast' import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal' import { useAppContext } from '@/context/app-context' -import { useDocLink, useLocale } from '@/context/i18n' -import { getLanguage } from '@/i18n-config/language' import { createCustomCollection } from '@/service/tools' type Props = { @@ -20,17 +16,8 @@ type Props = { const Contribute = ({ onRefreshData }: Props) => { const { t } = useTranslation() - const locale = useLocale() - const language = getLanguage(locale) const { isCurrentWorkspaceManager } = useAppContext() - const docLink = useDocLink() - const linkUrl = useMemo(() => { - return docLink('/guides/tools#how-to-create-custom-tools', { - 'zh-Hans': '/guides/tools#ru-he-chuang-jian-zi-ding-yi-gong-ju', - }) - }, [language]) - const [isShowEditCollectionToolModal, setIsShowEditCustomCollectionModal] = useState(false) const doCreateCustomToolCollection = async (data: CustomCollectionBackend) => { await createCustomCollection(data) @@ -54,13 +41,6 @@ const Contribute = ({ onRefreshData }: Props) => {
{t('createCustomTool', { ns: 'tools' })}
-
)} {isShowEditCollectionToolModal && ( diff --git a/web/app/components/workflow-app/components/workflow-onboarding-modal/index.tsx b/web/app/components/workflow-app/components/workflow-onboarding-modal/index.tsx index 0f92982cf2..0faf43bfd1 100644 --- a/web/app/components/workflow-app/components/workflow-onboarding-modal/index.tsx +++ b/web/app/components/workflow-app/components/workflow-onboarding-modal/index.tsx @@ -8,7 +8,6 @@ import { import { useTranslation } from 'react-i18next' import Modal from '@/app/components/base/modal' import { BlockEnum } from '@/app/components/workflow/types' -import { useDocLink } from '@/context/i18n' import StartNodeSelectionPanel from './start-node-selection-panel' type WorkflowOnboardingModalProps = { @@ -23,7 +22,6 @@ const WorkflowOnboardingModal: FC = ({ onSelectStartNode, }) => { const { t } = useTranslation() - const docLink = useDocLink() const handleSelectUserInput = useCallback(() => { onSelectStartNode(BlockEnum.Start) @@ -63,15 +61,6 @@ const WorkflowOnboardingModal: FC = ({
{t('onboarding.description', { ns: 'workflow' })} {' '} - - {t('onboarding.learnMore', { ns: 'workflow' })} - - {' '} {t('onboarding.aboutStartNode', { ns: 'workflow' })}
diff --git a/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts b/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts index 60f0bf3b28..0c5c1e4a40 100644 --- a/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts +++ b/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts @@ -1,4 +1,5 @@ import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store/store' +import type { DocPathWithoutLang } from '@/types/doc-paths' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node' @@ -44,7 +45,7 @@ export const useAvailableNodesMetaData = () => { const { metaData } = node const title = t(`blocks.${metaData.type}`, { ns: 'workflow' }) const description = t(`blocksAbout.${metaData.type}`, { ns: 'workflow' }) - const helpLinkPath = `guides/workflow/node/${metaData.helpLinkUri}` + const helpLinkPath = `/use-dify/nodes/${metaData.helpLinkUri}` as DocPathWithoutLang return { ...node, metaData: { diff --git a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx index 8303681d90..42be3d46e4 100644 --- a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx +++ b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx @@ -251,10 +251,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => { {' '}
diff --git a/web/app/components/workflow/nodes/_base/components/error-handle/default-value.tsx b/web/app/components/workflow/nodes/_base/components/error-handle/default-value.tsx index 538dce09d0..080fa0f107 100644 --- a/web/app/components/workflow/nodes/_base/components/error-handle/default-value.tsx +++ b/web/app/components/workflow/nodes/_base/components/error-handle/default-value.tsx @@ -5,7 +5,6 @@ import Input from '@/app/components/base/input' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import { VarType } from '@/app/components/workflow/types' -import { useDocLink } from '@/context/i18n' type DefaultValueProps = { forms: DefaultValueForm[] @@ -16,7 +15,6 @@ const DefaultValue = ({ onFormChange, }: DefaultValueProps) => { const { t } = useTranslation() - const docLink = useDocLink() const getFormChangeHandler = useCallback(({ key, type }: DefaultValueForm) => { return (payload: any) => { let value @@ -35,15 +33,6 @@ const DefaultValue = ({
{t('nodes.common.errorHandle.defaultValue.desc', { ns: 'workflow' })}   - - {t('common.learnMore', { ns: 'workflow' })} -
{ diff --git a/web/app/components/workflow/nodes/_base/components/error-handle/fail-branch-card.tsx b/web/app/components/workflow/nodes/_base/components/error-handle/fail-branch-card.tsx index fe267f52c4..49cd44160c 100644 --- a/web/app/components/workflow/nodes/_base/components/error-handle/fail-branch-card.tsx +++ b/web/app/components/workflow/nodes/_base/components/error-handle/fail-branch-card.tsx @@ -19,7 +19,7 @@ const FailBranchCard = () => { {t('nodes.common.errorHandle.failBranch.customizeTip', { ns: 'workflow' })}   diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx index 6184bcad9f..26f10b7a1d 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx @@ -6,7 +6,6 @@ import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import ListEmpty from '@/app/components/base/list-empty' import { useStore } from '@/app/components/workflow/store' -import { useDocLink } from '@/context/i18n' import VarReferenceVars from './var-reference-vars' type Props = { @@ -31,7 +30,7 @@ const VarReferencePopup: FC = ({ const pipelineId = useStore(s => s.pipelineId) const showManageRagInputFields = useMemo(() => !!pipelineId, [pipelineId]) const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel) - const docLink = useDocLink() + // max-h-[300px] overflow-y-auto todo: use portal to handle long list return (
= ({ description={( )} /> diff --git a/web/app/components/workflow/nodes/knowledge-base/components/chunk-structure/instruction/index.tsx b/web/app/components/workflow/nodes/knowledge-base/components/chunk-structure/instruction/index.tsx index 77981639cd..73e87ec12b 100644 --- a/web/app/components/workflow/nodes/knowledge-base/components/chunk-structure/instruction/index.tsx +++ b/web/app/components/workflow/nodes/knowledge-base/components/chunk-structure/instruction/index.tsx @@ -31,7 +31,7 @@ const Instruction = ({

{t('nodes.knowledgeBase.chunkStructureTip.message', { ns: 'workflow' })}

{ const { t } = useTranslation() + const docLink = useDocLink() const { options, hybridSearchModeOptions, @@ -61,7 +63,7 @@ const RetrievalSetting = ({ title: t('form.retrievalSetting.title', { ns: 'datasetSettings' }), subTitle: ( diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx index e7ac493bd2..b4dac4b58e 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx @@ -1,14 +1,12 @@ import type { FC } from 'react' import type { SchemaRoot } from '../../types' -import { RiBracesLine, RiCloseLine, RiExternalLinkLine, RiTimelineView } from '@remixicon/react' -import * as React from 'react' +import { RiBracesLine, RiCloseLine, RiTimelineView } from '@remixicon/react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import Divider from '@/app/components/base/divider' import Toast from '@/app/components/base/toast' import { JSON_SCHEMA_MAX_DEPTH } from '@/config' -import { useDocLink } from '@/context/i18n' import { SegmentedControl } from '../../../../../base/segmented-control' import { Type } from '../../types' import { @@ -55,7 +53,6 @@ const JsonSchemaConfig: FC = ({ onClose, }) => { const { t } = useTranslation() - const docLink = useDocLink() const [currentTab, setCurrentTab] = useState(SchemaView.VisualEditor) const [jsonSchema, setJsonSchema] = useState(defaultSchema || DEFAULT_SCHEMA) const [json, setJson] = useState(() => JSON.stringify(jsonSchema, null, 2)) @@ -253,15 +250,6 @@ const JsonSchemaConfig: FC = ({
{/* Footer */}
- - {t('nodes.llm.jsonSchema.doc', { ns: 'workflow' })} - -