From 62a1476a95165d70f5d2b9b503cd31b68a4685a0 Mon Sep 17 00:00:00 2001 From: Rohit Gahlawat Date: Wed, 10 Jun 2026 23:48:02 +0530 Subject: [PATCH 001/122] refactor(web): mark Props of misc components as read-only (#25219) (#37294) --- web/app/components/app-sidebar/app-sidebar-dropdown.tsx | 4 ++-- web/app/components/apps/list.tsx | 4 ++-- web/app/components/goto-anything/command-selector.tsx | 4 ++-- web/app/components/goto-anything/index.tsx | 4 ++-- web/app/components/snippet-list/components/snippet-card.tsx | 4 ++-- web/context/app-list-context.ts | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/web/app/components/app-sidebar/app-sidebar-dropdown.tsx b/web/app/components/app-sidebar/app-sidebar-dropdown.tsx index e3ed938987f..2b41e80a19d 100644 --- a/web/app/components/app-sidebar/app-sidebar-dropdown.tsx +++ b/web/app/components/app-sidebar/app-sidebar-dropdown.tsx @@ -21,7 +21,7 @@ import AppInfo from './app-info' import { getAppModeLabel } from './app-info/app-mode-labels' import NavLink from './nav-link' -type Props = { +type Props = Readonly<{ navigation: Array<{ name: string href: string @@ -29,7 +29,7 @@ type Props = { selectedIcon: NavIcon }> appInfoActions?: AppInfoActions -} +}> const AppSidebarDropdown = ({ navigation, appInfoActions }: Props) => { const { t } = useTranslation() diff --git a/web/app/components/apps/list.tsx b/web/app/components/apps/list.tsx index 3e299beae44..321c104462e 100644 --- a/web/app/components/apps/list.tsx +++ b/web/app/components/apps/list.tsx @@ -37,9 +37,9 @@ const CreateFromDSLModal = dynamic(() => import('@/app/components/app/create-fro ssr: false, }) -type Props = { +type Props = Readonly<{ controlRefreshList?: number -} +}> function List({ controlRefreshList = 0, }: Props) { diff --git a/web/app/components/goto-anything/command-selector.tsx b/web/app/components/goto-anything/command-selector.tsx index 3cadfd742db..815126a3836 100644 --- a/web/app/components/goto-anything/command-selector.tsx +++ b/web/app/components/goto-anything/command-selector.tsx @@ -6,14 +6,14 @@ import { useTranslation } from 'react-i18next' import { usePathname } from '@/next/navigation' import { slashCommandRegistry } from './actions/commands/registry' -type Props = { +type Props = Readonly<{ actions: Record onCommandSelect: (commandKey: string) => void searchFilter?: string commandValue?: string onCommandValueChange?: (value: string) => void originalQuery?: string -} +}> const CommandSelector: FC = ({ actions, onCommandSelect, searchFilter, commandValue, onCommandValueChange, originalQuery }) => { const { t } = useTranslation() diff --git a/web/app/components/goto-anything/index.tsx b/web/app/components/goto-anything/index.tsx index a9214534b82..29cb8983d25 100644 --- a/web/app/components/goto-anything/index.tsx +++ b/web/app/components/goto-anything/index.tsx @@ -18,9 +18,9 @@ import { useGotoAnythingSearch, } from './hooks' -type Props = { +type Props = Readonly<{ onHide?: () => void -} +}> const GotoAnythingDialog: FC = ({ onHide, diff --git a/web/app/components/snippet-list/components/snippet-card.tsx b/web/app/components/snippet-list/components/snippet-card.tsx index 8b211b87367..55d73719ac7 100644 --- a/web/app/components/snippet-list/components/snippet-card.tsx +++ b/web/app/components/snippet-list/components/snippet-card.tsx @@ -30,12 +30,12 @@ import { useDeleteSnippetMutation, useExportSnippetMutation, useUpdateSnippetMut import { downloadBlob } from '@/utils/download' import { formatTime } from '@/utils/time' -type Props = { +type Props = Readonly<{ snippet: SnippetListItem onOpenTagManagement?: () => void onRefresh?: () => void onTagsChange?: () => void -} +}> const SnippetCard = ({ snippet, diff --git a/web/context/app-list-context.ts b/web/context/app-list-context.ts index 7164a07b9ec..6357df2dd73 100644 --- a/web/context/app-list-context.ts +++ b/web/context/app-list-context.ts @@ -2,12 +2,12 @@ import type { SetTryAppPanel, TryAppSelection } from '@/types/try-app' import { noop } from 'es-toolkit/function' import { createContext } from 'use-context-selector' -type Props = { +type Props = Readonly<{ currentApp?: TryAppSelection isShowTryAppPanel: boolean setShowTryAppPanel: SetTryAppPanel controlHideCreateFromTemplatePanel: number -} +}> const AppListContext = createContext({ isShowTryAppPanel: false, From beec13ed61d73c60dad0063af6e2975e64f1716f Mon Sep 17 00:00:00 2001 From: Rohit Gahlawat Date: Wed, 10 Jun 2026 23:48:43 +0530 Subject: [PATCH 002/122] refactor(web): mark Props of header/account-setting components as read-only (#25219) (#37293) --- .../members-page/operation/transfer-ownership.tsx | 4 ++-- .../members-page/transfer-ownership-modal/index.tsx | 4 ++-- .../members-page/transfer-ownership-modal/member-selector.tsx | 4 ++-- .../header/account-setting/model-provider-page/index.tsx | 4 ++-- .../provider-added-card/provider-card-actions.tsx | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/web/app/components/header/account-setting/members-page/operation/transfer-ownership.tsx b/web/app/components/header/account-setting/members-page/operation/transfer-ownership.tsx index c901f834349..8a9f07fa0ad 100644 --- a/web/app/components/header/account-setting/members-page/operation/transfer-ownership.tsx +++ b/web/app/components/header/account-setting/members-page/operation/transfer-ownership.tsx @@ -16,9 +16,9 @@ import { useAppContext } from '@/context/app-context' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { useWorkspacePermissions } from '@/service/use-workspace' -type Props = { +type Props = Readonly<{ onOperate: () => void -} +}> const TransferOwnership = ({ onOperate }: Props) => { const { t } = useTranslation() diff --git a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx index 2b613b2f5d5..7bfb3ee3f1f 100644 --- a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx +++ b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx @@ -9,10 +9,10 @@ import { useAppContext } from '@/context/app-context' import { ownershipTransfer, sendOwnerEmail, verifyOwnerEmail } from '@/service/common' import MemberSelector from './member-selector' -type Props = { +type Props = Readonly<{ show: boolean onClose: () => void -} +}> enum STEP { start = 'start', verify = 'verify', diff --git a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx index 8c60f8244bf..534aaadc9a9 100644 --- a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx +++ b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx @@ -12,11 +12,11 @@ import { useTranslation } from 'react-i18next' import Input from '@/app/components/base/input' import { useMembers } from '@/service/use-common' -type Props = { +type Props = Readonly<{ value?: string onSelect: (value: string) => void exclude?: string[] -} +}> const MemberSelector: FC = ({ value, diff --git a/web/app/components/header/account-setting/model-provider-page/index.tsx b/web/app/components/header/account-setting/model-provider-page/index.tsx index 000a9d0d7e0..a9e98a6430b 100644 --- a/web/app/components/header/account-setting/model-provider-page/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/index.tsx @@ -27,9 +27,9 @@ import { providerToPluginId } from './utils' type SystemModelConfigStatus = 'no-provider' | 'none-configured' | 'partially-configured' | 'fully-configured' -type Props = { +type Props = Readonly<{ searchText: string -} +}> const FixedModelProvider = ['langgenius/openai/openai', 'langgenius/anthropic/anthropic'] diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/provider-card-actions.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/provider-card-actions.tsx index 9157fead4b0..1e505d550e1 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/provider-card-actions.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/provider-card-actions.tsx @@ -15,10 +15,10 @@ import { useLocale } from '@/context/i18n' import useTheme from '@/hooks/use-theme' import { getMarketplaceUrl } from '@/utils/var' -type Props = { +type Props = Readonly<{ detail: PluginDetail onUpdate?: () => void -} +}> const ProviderCardActions: FC = ({ detail, onUpdate }) => { const { t } = useTranslation() From be2034f681d65f570de67612dd85f97530e5da75 Mon Sep 17 00:00:00 2001 From: Rohit Gahlawat Date: Wed, 10 Jun 2026 23:49:37 +0530 Subject: [PATCH 003/122] refactor(web): mark Props of share/ components as read-only (#25219) (#37292) --- web/app/components/share/text-generation/info-modal.tsx | 4 ++-- web/app/components/share/text-generation/menu-dropdown.tsx | 4 ++-- .../share/text-generation/run-batch/csv-reader/index.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/web/app/components/share/text-generation/info-modal.tsx b/web/app/components/share/text-generation/info-modal.tsx index 894ca8781ee..0e5007c9ea5 100644 --- a/web/app/components/share/text-generation/info-modal.tsx +++ b/web/app/components/share/text-generation/info-modal.tsx @@ -5,11 +5,11 @@ import * as React from 'react' import AppIcon from '@/app/components/base/app-icon' import { appDefaultIconBackground } from '@/config' -type Props = { +type Props = Readonly<{ data?: SiteInfo isShow: boolean onClose: () => void -} +}> const InfoModal = ({ isShow, diff --git a/web/app/components/share/text-generation/menu-dropdown.tsx b/web/app/components/share/text-generation/menu-dropdown.tsx index 89983552f7f..e09e0f989b9 100644 --- a/web/app/components/share/text-generation/menu-dropdown.tsx +++ b/web/app/components/share/text-generation/menu-dropdown.tsx @@ -21,11 +21,11 @@ import { usePathname, useRouter } from '@/next/navigation' import { webAppLogout } from '@/service/webapp-auth' import InfoModal from './info-modal' -type Props = { +type Props = Readonly<{ data?: SiteInfo placement?: Placement hideLogout?: boolean -} +}> const MenuDropdown: FC = ({ data, diff --git a/web/app/components/share/text-generation/run-batch/csv-reader/index.tsx b/web/app/components/share/text-generation/run-batch/csv-reader/index.tsx index 33af235f5cf..5147eb7a090 100644 --- a/web/app/components/share/text-generation/run-batch/csv-reader/index.tsx +++ b/web/app/components/share/text-generation/run-batch/csv-reader/index.tsx @@ -9,9 +9,9 @@ import { } from 'react-papaparse' import { Csv as CSVIcon } from '@/app/components/base/icons/src/public/files' -type Props = { +type Props = Readonly<{ onParsed: (data: string[][]) => void -} +}> const CSVReader: FC = ({ onParsed, From 162c478368d5c542f8bea846be8a84ca8dc3fb37 Mon Sep 17 00:00:00 2001 From: Rohit Gahlawat Date: Wed, 10 Jun 2026 23:50:30 +0530 Subject: [PATCH 004/122] refactor(web): mark Props of (commonLayout) components as read-only (#25219) (#37291) --- .../[appId]/overview/long-time-range-picker.tsx | 4 ++-- .../[appId]/overview/time-range-picker/date-picker.tsx | 4 ++-- .../[appId]/overview/time-range-picker/index.tsx | 4 ++-- .../[appId]/overview/time-range-picker/range-selector.tsx | 4 ++-- .../[appId]/overview/tracing/config-button.tsx | 4 ++-- .../app/(appDetailLayout)/[appId]/overview/tracing/field.tsx | 4 ++-- .../[appId]/overview/tracing/provider-config-modal.tsx | 4 ++-- .../[appId]/overview/tracing/provider-panel.tsx | 4 ++-- .../[appId]/overview/tracing/tracing-icon.tsx | 4 ++-- .../(datasetDetailLayout)/[datasetId]/hitTesting/page.tsx | 4 ++-- web/app/(commonLayout)/error.tsx | 4 ++-- 11 files changed, 22 insertions(+), 22 deletions(-) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/long-time-range-picker.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/long-time-range-picker.tsx index 002f8f3bf16..94980d36f97 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/long-time-range-picker.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/long-time-range-picker.tsx @@ -13,11 +13,11 @@ type TimePeriodOption = { name: string } -type Props = { +type Props = Readonly<{ periodMapping: { [key: string]: { value: number, name: TimePeriodName } } onSelect: (payload: PeriodParams) => void queryDateFormat: string -} +}> const today = dayjs() diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx index dbc429cbdc6..2e59c09bce1 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx @@ -12,12 +12,12 @@ import Picker from '@/app/components/base/date-and-time-picker/date-picker' import { useLocale } from '@/context/i18n' import { formatToLocalTime } from '@/utils/format' -type Props = { +type Props = Readonly<{ start: Dayjs end: Dayjs onStartChange: (date?: Dayjs) => void onEndChange: (date?: Dayjs) => void -} +}> const today = dayjs() const DatePicker: FC = ({ diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/index.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/index.tsx index 53794ad8dba..8491f01e8af 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/index.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/index.tsx @@ -16,11 +16,11 @@ const today = dayjs() type TimePeriodName = I18nKeysByPrefix<'appLog', 'filter.period.'> -type Props = { +type Props = Readonly<{ ranges: { value: number, name: TimePeriodName }[] onSelect: (payload: PeriodParams) => void queryDateFormat: string -} +}> const TimeRangePicker: FC = ({ ranges, diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx index c2570337746..18def9308eb 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx @@ -17,11 +17,11 @@ type TimePeriodOption = { name: string } -type Props = { +type Props = Readonly<{ isCustomRange: boolean ranges: { value: number, name: TimePeriodName }[] onSelect: (payload: PeriodParamsWithTimeRange) => void -} +}> const RangeSelector: FC = ({ isCustomRange, diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx index 6d7c1787283..d587c3bd843 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx @@ -12,12 +12,12 @@ import * as React from 'react' import { useState } from 'react' import ConfigPopup from './config-popup' -type Props = { +type Props = Readonly<{ readOnly: boolean className?: string hasConfigured: boolean children?: React.ReactNode -} & PopupProps +}> & PopupProps const ConfigBtn: FC = ({ className, diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx index 2b56ecfeea3..5b1fc27a935 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx @@ -4,7 +4,7 @@ import { cn } from '@langgenius/dify-ui/cn' import * as React from 'react' import Input from '@/app/components/base/input' -type Props = { +type Props = Readonly<{ className?: string label: string labelClassName?: string @@ -12,7 +12,7 @@ type Props = { onChange: (value: string) => void isRequired?: boolean placeholder?: string -} +}> const Field: FC = ({ className, diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx index 4845cf38df5..f537ce9b9b8 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx @@ -28,7 +28,7 @@ import { docURL } from './config' import Field from './field' import { TracingProvider } from './type' -type Props = { +type Props = Readonly<{ appId: string type: TracingProvider payload?: ArizeConfig | PhoenixConfig | LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig | AliyunConfig | MLflowConfig | DatabricksConfig | TencentConfig | null @@ -36,7 +36,7 @@ type Props = { onCancel: () => void onSaved: (payload: ArizeConfig | PhoenixConfig | LangSmithConfig | LangFuseConfig | OpikConfig | WeaveConfig | AliyunConfig | MLflowConfig | DatabricksConfig | TencentConfig) => void onChosen: (provider: TracingProvider) => void -} +}> const I18N_PREFIX = 'tracing.configProvider' diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx index 4e166c52baf..cb37de98c0b 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx @@ -13,7 +13,7 @@ import { TracingProvider } from './type' const I18N_PREFIX = 'tracing' -type Props = { +type Props = Readonly<{ type: TracingProvider readOnly: boolean isChosen: boolean @@ -21,7 +21,7 @@ type Props = { onChoose: () => void hasConfigured: boolean onConfig: () => void -} +}> const getIcon = (type: TracingProvider) => { return ({ diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx index ed097f9333e..a7450e70354 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx @@ -4,10 +4,10 @@ import { cn } from '@langgenius/dify-ui/cn' import * as React from 'react' import { TracingIcon as Icon } from '@/app/components/base/icons/src/public/tracing' -type Props = { +type Props = Readonly<{ className?: string size: 'lg' | 'md' -} +}> const sizeClassMap = { lg: 'w-9 h-9 p-2 rounded-[10px]', diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/hitTesting/page.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/hitTesting/page.tsx index 9a339030b97..5b8c8e31976 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/hitTesting/page.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/hitTesting/page.tsx @@ -1,9 +1,9 @@ import * as React from 'react' import Main from '@/app/components/datasets/hit-testing' -type Props = { +type Props = Readonly<{ params: Promise<{ datasetId: string }> -} +}> const HitTesting = async (props: Props) => { const params = await props.params diff --git a/web/app/(commonLayout)/error.tsx b/web/app/(commonLayout)/error.tsx index 1b38ad89969..daaf7b87763 100644 --- a/web/app/(commonLayout)/error.tsx +++ b/web/app/(commonLayout)/error.tsx @@ -5,10 +5,10 @@ import { useTranslation } from 'react-i18next' import { FullScreenLoading } from '@/app/components/full-screen-loading' import { isLegacyBase401 } from '@/features/account-profile/client' -type Props = { +type Props = Readonly<{ error: Error & { digest?: string } unstable_retry: () => void -} +}> export default function CommonLayoutError({ error, unstable_retry }: Props) { const { t } = useTranslation('common') From b4205af9b9f78d1ae16453cd37f681e6806f7947 Mon Sep 17 00:00:00 2001 From: Rohit Gahlawat Date: Wed, 10 Jun 2026 23:51:00 +0530 Subject: [PATCH 005/122] refactor(web): mark Props of explore/ components as read-only (#25219) (#37290) --- web/app/components/explore/try-app/app-info/index.tsx | 4 ++-- web/app/components/explore/try-app/app/chat.tsx | 4 ++-- web/app/components/explore/try-app/app/index.tsx | 4 ++-- web/app/components/explore/try-app/app/text-generation.tsx | 4 ++-- web/app/components/explore/try-app/index.tsx | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/web/app/components/explore/try-app/app-info/index.tsx b/web/app/components/explore/try-app/app-info/index.tsx index ec595ea2473..e7e6e5edde7 100644 --- a/web/app/components/explore/try-app/app-info/index.tsx +++ b/web/app/components/explore/try-app/app-info/index.tsx @@ -9,13 +9,13 @@ import { AppTypeIcon } from '@/app/components/app/type-selector' import AppIcon from '@/app/components/base/app-icon' import useGetRequirements from './use-get-requirements' -type Props = { +type Props = Readonly<{ appId: string appDetail: TryAppInfo categories?: string[] className?: string onCreate: () => void -} +}> const headerClassName = 'system-sm-semibold-uppercase text-text-secondary mb-3' const requirementIconSize = 20 diff --git a/web/app/components/explore/try-app/app/chat.tsx b/web/app/components/explore/try-app/app/chat.tsx index c1c404e49e0..0078a3b22d7 100644 --- a/web/app/components/explore/try-app/app/chat.tsx +++ b/web/app/components/explore/try-app/app/chat.tsx @@ -26,11 +26,11 @@ import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { AppSourceType } from '@/service/share' import { useThemeContext } from '../../../base/chat/embedded-chatbot/theme/theme-context' -type Props = { +type Props = Readonly<{ appId: string appDetail: TryAppInfo className: string -} +}> const TryApp: FC = ({ appId, diff --git a/web/app/components/explore/try-app/app/index.tsx b/web/app/components/explore/try-app/app/index.tsx index 4224719bbd8..ecb894b6db7 100644 --- a/web/app/components/explore/try-app/app/index.tsx +++ b/web/app/components/explore/try-app/app/index.tsx @@ -7,10 +7,10 @@ import useDocumentTitle from '@/hooks/use-document-title' import Chat from './chat' import TextGeneration from './text-generation' -type Props = { +type Props = Readonly<{ appId: string appDetail: TryAppInfo -} +}> const TryApp: FC = ({ appId, diff --git a/web/app/components/explore/try-app/app/text-generation.tsx b/web/app/components/explore/try-app/app/text-generation.tsx index 5a51199d864..7c1f77acb3d 100644 --- a/web/app/components/explore/try-app/app/text-generation.tsx +++ b/web/app/components/explore/try-app/app/text-generation.tsx @@ -24,12 +24,12 @@ import { Resolution, TransferMethod } from '@/types/app' import { userInputsFormToPromptVariables } from '@/utils/model-config' import RunOnce from '../../../share/text-generation/run-once' -type Props = { +type Props = Readonly<{ appId: string className?: string isWorkflow?: boolean appData: AppData | null -} +}> const TextGeneration: FC = ({ appId, diff --git a/web/app/components/explore/try-app/index.tsx b/web/app/components/explore/try-app/index.tsx index 3a19075cdad..04c08e8e330 100644 --- a/web/app/components/explore/try-app/index.tsx +++ b/web/app/components/explore/try-app/index.tsx @@ -19,13 +19,13 @@ import AppInfo from './app-info' import Preview from './preview' import { TypeEnum } from './types' -type Props = { +type Props = Readonly<{ appId: string app?: AppType categories?: string[] onClose: () => void onCreate: () => void -} +}> const TryApp: FC = ({ appId, From 9c6577804cf375a8bcb93601f1cf19d91359c587 Mon Sep 17 00:00:00 2001 From: Rohit Gahlawat <283466839+Rohit-Gahlawat@users.noreply.github.com> Date: Thu, 11 Jun 2026 05:45:31 +0530 Subject: [PATCH 006/122] refactor(web): mark Props of app/ components as read-only (#25219) (#37301) Co-authored-by: Asuka Minato --- .../app/configuration/config-prompt/advanced-prompt-input.tsx | 4 ++-- .../config-prompt/conversation-history/edit-modal.tsx | 4 ++-- .../config-prompt/conversation-history/history-panel.tsx | 4 ++-- .../app/configuration/config-prompt/message-type-selector.tsx | 4 ++-- .../config-prompt/prompt-editor-height-resize-wrap.tsx | 4 ++-- .../app/configuration/config-var/config-modal/field.tsx | 4 ++-- .../app/configuration/config-var/config-modal/type-select.tsx | 4 ++-- .../app/configuration/config-var/select-var-type.tsx | 4 ++-- .../app/configuration/config/agent-setting-button.tsx | 4 ++-- .../app/configuration/config/agent/agent-setting/index.tsx | 4 ++-- .../configuration/config/agent/agent-setting/item-panel.tsx | 4 ++-- .../config/agent/agent-tools/setting-built-in-tool.tsx | 4 ++-- .../app/configuration/config/assistant-type-picker/index.tsx | 4 ++-- .../app/configuration/config/automatic/idea-output.tsx | 4 ++-- .../config/automatic/instruction-editor-in-workflow.tsx | 4 ++-- .../app/configuration/config/automatic/instruction-editor.tsx | 4 ++-- .../configuration/config/automatic/prompt-res-in-workflow.tsx | 4 ++-- .../app/configuration/config/automatic/prompt-res.tsx | 4 ++-- .../app/configuration/config/automatic/prompt-toast.tsx | 4 ++-- .../components/app/configuration/config/automatic/result.tsx | 4 ++-- .../configuration/dataset-config/context-var/var-picker.tsx | 4 ++-- web/app/components/app/configuration/dataset-config/index.tsx | 4 ++-- .../dataset-config/params-config/config-content.tsx | 4 ++-- .../components/app/configuration/debug/chat-user-input.tsx | 4 ++-- web/app/components/app/create-from-dsl-modal/uploader.tsx | 4 ++-- web/app/components/app/log-annotation/index.tsx | 4 ++-- web/app/components/app/log/model-info.tsx | 4 ++-- web/app/components/app/log/var-panel.tsx | 4 ++-- web/app/components/app/overview/embedded/index.tsx | 4 ++-- 29 files changed, 58 insertions(+), 58 deletions(-) diff --git a/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx b/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx index 339a6d62f6f..4ecca3c6168 100644 --- a/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx +++ b/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx @@ -33,7 +33,7 @@ import MessageTypeSelector from './message-type-selector' import PromptEditorHeightResizeWrap from './prompt-editor-height-resize-wrap' import s from './style.module.css' -type Props = { +type Props = Readonly<{ type: PromptRole isChatMode: boolean value: string @@ -45,7 +45,7 @@ type Props = { isContextMissing: boolean onHideContextMissingTip: () => void noResize?: boolean -} +}> const AdvancedPromptInput: FC = ({ type, diff --git a/web/app/components/app/configuration/config-prompt/conversation-history/edit-modal.tsx b/web/app/components/app/configuration/config-prompt/conversation-history/edit-modal.tsx index 9738accdf75..d68a2695a51 100644 --- a/web/app/components/app/configuration/config-prompt/conversation-history/edit-modal.tsx +++ b/web/app/components/app/configuration/config-prompt/conversation-history/edit-modal.tsx @@ -7,13 +7,13 @@ import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' -type Props = { +type Props = Readonly<{ isShow: boolean saveLoading: boolean data: ConversationHistoriesRole onClose: () => void onSave: (data: any) => void -} +}> const EditModal: FC = ({ isShow, 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 cc5862eced4..fd7a685e75d 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 @@ -6,10 +6,10 @@ 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' -type Props = { +type Props = Readonly<{ showWarning: boolean onShowEditModal: () => void -} +}> const HistoryPanel: FC = ({ showWarning, diff --git a/web/app/components/app/configuration/config-prompt/message-type-selector.tsx b/web/app/components/app/configuration/config-prompt/message-type-selector.tsx index cba5737cf8d..4dec3f4433f 100644 --- a/web/app/components/app/configuration/config-prompt/message-type-selector.tsx +++ b/web/app/components/app/configuration/config-prompt/message-type-selector.tsx @@ -6,10 +6,10 @@ import * as React from 'react' import { ChevronSelectorVertical } from '@/app/components/base/icons/src/vender/line/arrows' import { PromptRole } from '@/models/debug' -type Props = { +type Props = Readonly<{ value: PromptRole onChange: (value: PromptRole) => void -} +}> const allTypes = [PromptRole.system, PromptRole.user, PromptRole.assistant] const MessageTypeSelector: FC = ({ diff --git a/web/app/components/app/configuration/config-prompt/prompt-editor-height-resize-wrap.tsx b/web/app/components/app/configuration/config-prompt/prompt-editor-height-resize-wrap.tsx index ac8029a2e7d..6c94336ca5c 100644 --- a/web/app/components/app/configuration/config-prompt/prompt-editor-height-resize-wrap.tsx +++ b/web/app/components/app/configuration/config-prompt/prompt-editor-height-resize-wrap.tsx @@ -5,7 +5,7 @@ import { useDebounceFn } from 'ahooks' import * as React from 'react' import { useCallback, useEffect, useState } from 'react' -type Props = { +type Props = Readonly<{ className?: string height: number minHeight: number @@ -13,7 +13,7 @@ type Props = { children: React.JSX.Element footer?: React.JSX.Element hideResize?: boolean -} +}> const PromptEditorHeightResizeWrap: FC = ({ className, diff --git a/web/app/components/app/configuration/config-var/config-modal/field.tsx b/web/app/components/app/configuration/config-var/config-modal/field.tsx index 6ed837812bc..83fcbc36197 100644 --- a/web/app/components/app/configuration/config-var/config-modal/field.tsx +++ b/web/app/components/app/configuration/config-var/config-modal/field.tsx @@ -4,12 +4,12 @@ import { cn } from '@langgenius/dify-ui/cn' import * as React from 'react' import { useTranslation } from 'react-i18next' -type Props = { +type Props = Readonly<{ className?: string title: string isOptional?: boolean children: React.JSX.Element -} +}> const Field: FC = ({ className, diff --git a/web/app/components/app/configuration/config-var/config-modal/type-select.tsx b/web/app/components/app/configuration/config-var/config-modal/type-select.tsx index 352b3139ff9..26d1b5c4a81 100644 --- a/web/app/components/app/configuration/config-var/config-modal/type-select.tsx +++ b/web/app/components/app/configuration/config-var/config-modal/type-select.tsx @@ -20,7 +20,7 @@ export type Item = { name: string } -type Props = { +type Props = Readonly<{ value: string | number onSelect: (value: Item) => void items: Item[] @@ -28,7 +28,7 @@ type Props = { popupInnerClassName?: string readonly?: boolean hideChecked?: boolean -} +}> const TypeSelector: FC = ({ value, onSelect, diff --git a/web/app/components/app/configuration/config-var/select-var-type.tsx b/web/app/components/app/configuration/config-var/select-var-type.tsx index 92302cf0e39..015e4b1d5ee 100644 --- a/web/app/components/app/configuration/config-var/select-var-type.tsx +++ b/web/app/components/app/configuration/config-var/select-var-type.tsx @@ -14,9 +14,9 @@ import { ApiConnection } from '@/app/components/base/icons/src/vender/solid/deve import InputVarTypeIcon from '@/app/components/workflow/nodes/_base/components/input-var-type-icon' import { InputVarType } from '@/app/components/workflow/types' -type Props = { +type Props = Readonly<{ onChange: (value: string) => void -} +}> type ItemProps = { text: string diff --git a/web/app/components/app/configuration/config/agent-setting-button.tsx b/web/app/components/app/configuration/config/agent-setting-button.tsx index 600ed1cc622..68a3f8cef16 100644 --- a/web/app/components/app/configuration/config/agent-setting-button.tsx +++ b/web/app/components/app/configuration/config/agent-setting-button.tsx @@ -8,12 +8,12 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import AgentSetting from './agent/agent-setting' -type Props = { +type Props = Readonly<{ isFunctionCall: boolean isChatModel: boolean agentConfig?: AgentConfig onAgentSettingChange: (payload: AgentConfig) => void -} +}> const AgentSettingButton: FC = ({ onAgentSettingChange, diff --git a/web/app/components/app/configuration/config/agent/agent-setting/index.tsx b/web/app/components/app/configuration/config/agent/agent-setting/index.tsx index 51f4a16a255..04f1b4b2a61 100644 --- a/web/app/components/app/configuration/config/agent/agent-setting/index.tsx +++ b/web/app/components/app/configuration/config/agent/agent-setting/index.tsx @@ -14,13 +14,13 @@ import { Unblur } from '@/app/components/base/icons/src/vender/solid/education' import { DEFAULT_AGENT_PROMPT, MAX_ITERATIONS_NUM } from '@/config' import ItemPanel from './item-panel' -type Props = { +type Props = Readonly<{ isChatModel: boolean payload: AgentConfig isFunctionCall: boolean onCancel: () => void onSave: (payload: any) => void -} +}> const maxIterationsMin = 1 diff --git a/web/app/components/app/configuration/config/agent/agent-setting/item-panel.tsx b/web/app/components/app/configuration/config/agent/agent-setting/item-panel.tsx index 1fc67ab6613..b860a8fd86e 100644 --- a/web/app/components/app/configuration/config/agent/agent-setting/item-panel.tsx +++ b/web/app/components/app/configuration/config/agent/agent-setting/item-panel.tsx @@ -4,13 +4,13 @@ import { cn } from '@langgenius/dify-ui/cn' import * as React from 'react' import { Infotip } from '@/app/components/base/infotip' -type Props = { +type Props = Readonly<{ className?: string icon: React.JSX.Element name: string description: string children: React.JSX.Element -} +}> const ItemPanel: FC = ({ className, diff --git a/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx b/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx index f6c74eee325..ccfbd3be14b 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx @@ -37,7 +37,7 @@ import { useLocale } from '@/context/i18n' import { getLanguage } from '@/i18n-config/language' import { fetchBuiltInToolList, fetchCustomToolList, fetchModelToolList, fetchWorkflowToolList } from '@/service/tools' -type Props = { +type Props = Readonly<{ showBackButton?: boolean collection: Collection | ToolWithProvider isBuiltIn?: boolean @@ -49,7 +49,7 @@ type Props = { onSave?: (value: Record) => void credentialId?: string onAuthorizationItemClick?: (id: string) => void -} +}> const SettingBuiltInTool: FC = ({ showBackButton = false, diff --git a/web/app/components/app/configuration/config/assistant-type-picker/index.tsx b/web/app/components/app/configuration/config/assistant-type-picker/index.tsx index 89f02efe6cb..1ab4074950e 100644 --- a/web/app/components/app/configuration/config/assistant-type-picker/index.tsx +++ b/web/app/components/app/configuration/config/assistant-type-picker/index.tsx @@ -21,7 +21,7 @@ import { CuteRobot } from '@/app/components/base/icons/src/vender/solid/communic import { BubbleText } from '@/app/components/base/icons/src/vender/solid/education' import AgentSetting from '../agent/agent-setting' -type Props = { +type Props = Readonly<{ value: string disabled: boolean onChange: (value: string) => void @@ -29,7 +29,7 @@ type Props = { isChatModel: boolean agentConfig?: AgentConfig onAgentSettingChange: (payload: AgentConfig) => void -} +}> type ItemProps = { text: string diff --git a/web/app/components/app/configuration/config/automatic/idea-output.tsx b/web/app/components/app/configuration/config/automatic/idea-output.tsx index 32844975fd2..9725becd8a4 100644 --- a/web/app/components/app/configuration/config/automatic/idea-output.tsx +++ b/web/app/components/app/configuration/config/automatic/idea-output.tsx @@ -9,10 +9,10 @@ import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid const i18nPrefix = 'generate' -type Props = { +type Props = Readonly<{ value: string onChange: (value: string) => void -} +}> const IdeaOutput: FC = ({ value, diff --git a/web/app/components/app/configuration/config/automatic/instruction-editor-in-workflow.tsx b/web/app/components/app/configuration/config/automatic/instruction-editor-in-workflow.tsx index 31a96c58187..92012ea8d3e 100644 --- a/web/app/components/app/configuration/config/automatic/instruction-editor-in-workflow.tsx +++ b/web/app/components/app/configuration/config/automatic/instruction-editor-in-workflow.tsx @@ -10,14 +10,14 @@ import { useWorkflowStore } from '@/app/components/workflow/store' import { VarType } from '@/app/components/workflow/types' import InstructionEditor from './instruction-editor' -type Props = { +type Props = Readonly<{ nodeId: string value: string editorKey: string onChange: (text: string) => void generatorType: GeneratorType isShowCurrentBlock: boolean -} +}> const InstructionEditorInWorkflow: FC = ({ nodeId, diff --git a/web/app/components/app/configuration/config/automatic/instruction-editor.tsx b/web/app/components/app/configuration/config/automatic/instruction-editor.tsx index 18169f39ad3..5be69cc25c4 100644 --- a/web/app/components/app/configuration/config/automatic/instruction-editor.tsx +++ b/web/app/components/app/configuration/config/automatic/instruction-editor.tsx @@ -12,7 +12,7 @@ import { Type } from '@/app/components/workflow/nodes/llm/types' import { BlockEnum } from '@/app/components/workflow/types' import { useEventEmitterContextContext } from '@/context/event-emitter' -type Props = { +type Props = Readonly<{ editorKey: string value: string onChange: (text: string) => void @@ -25,7 +25,7 @@ type Props = { }) => Type isShowCurrentBlock: boolean isShowLastRunBlock: boolean -} +}> const i18nPrefix = 'generate' diff --git a/web/app/components/app/configuration/config/automatic/prompt-res-in-workflow.tsx b/web/app/components/app/configuration/config/automatic/prompt-res-in-workflow.tsx index 23399261d6d..58a05677e00 100644 --- a/web/app/components/app/configuration/config/automatic/prompt-res-in-workflow.tsx +++ b/web/app/components/app/configuration/config/automatic/prompt-res-in-workflow.tsx @@ -7,10 +7,10 @@ import { Type } from '@/app/components/workflow/nodes/llm/types' import { BlockEnum } from '@/app/components/workflow/types' import PromptRes from './prompt-res' -type Props = { +type Props = Readonly<{ value: string nodeId: string -} +}> const PromptResInWorkflow: FC = ({ value, diff --git a/web/app/components/app/configuration/config/automatic/prompt-res.tsx b/web/app/components/app/configuration/config/automatic/prompt-res.tsx index ced438a18aa..cdf5ad9b138 100644 --- a/web/app/components/app/configuration/config/automatic/prompt-res.tsx +++ b/web/app/components/app/configuration/config/automatic/prompt-res.tsx @@ -5,10 +5,10 @@ import * as React from 'react' import { useEffect } from 'react' import PromptEditor from '@/app/components/base/prompt-editor' -type Props = { +type Props = Readonly<{ value: string workflowVariableBlock: WorkflowVariableBlockType -} +}> const keyIdPrefix = 'prompt-res-editor' const PromptRes: FC = ({ diff --git a/web/app/components/app/configuration/config/automatic/prompt-toast.tsx b/web/app/components/app/configuration/config/automatic/prompt-toast.tsx index 8a17e6b0ce2..1a45a291cc3 100644 --- a/web/app/components/app/configuration/config/automatic/prompt-toast.tsx +++ b/web/app/components/app/configuration/config/automatic/prompt-toast.tsx @@ -6,10 +6,10 @@ import { useTranslation } from 'react-i18next' import { Markdown } from '@/app/components/base/markdown' import s from './style.module.css' -type Props = { +type Props = Readonly<{ message: string className?: string -} +}> const PromptToast = ({ message, className, diff --git a/web/app/components/app/configuration/config/automatic/result.tsx b/web/app/components/app/configuration/config/automatic/result.tsx index 98d0d1390c4..c0cfcf4ab1d 100644 --- a/web/app/components/app/configuration/config/automatic/result.tsx +++ b/web/app/components/app/configuration/config/automatic/result.tsx @@ -14,7 +14,7 @@ import PromptToast from './prompt-toast' import { GeneratorType } from './types' import VersionSelector from './version-selector' -type Props = { +type Props = Readonly<{ isBasicMode?: boolean nodeId?: string current: GenRes @@ -23,7 +23,7 @@ type Props = { versions: GenRes[] onApply: () => void generatorType: GeneratorType -} +}> const Result: FC = ({ isBasicMode, diff --git a/web/app/components/app/configuration/dataset-config/context-var/var-picker.tsx b/web/app/components/app/configuration/dataset-config/context-var/var-picker.tsx index ba1d35bc87f..0bbde5af371 100644 --- a/web/app/components/app/configuration/dataset-config/context-var/var-picker.tsx +++ b/web/app/components/app/configuration/dataset-config/context-var/var-picker.tsx @@ -14,14 +14,14 @@ import { useTranslation } from 'react-i18next' import IconTypeIcon from '@/app/components/app/configuration/config-var/input-type-icon' type Option = { name: string, value: string, type: string } -export type Props = { +export type Props = Readonly<{ triggerClassName?: string className?: string value: string | undefined options: Option[] onChange: (value: string) => void notSelectedVarTip?: string | null -} +}> const VarItem: FC<{ item: Option }> = ({ item }) => (
diff --git a/web/app/components/app/configuration/dataset-config/index.tsx b/web/app/components/app/configuration/dataset-config/index.tsx index a89725419e1..6ea2a638396 100644 --- a/web/app/components/app/configuration/dataset-config/index.tsx +++ b/web/app/components/app/configuration/dataset-config/index.tsx @@ -39,10 +39,10 @@ import CardItem from './card-item' import ContextVar from './context-var' import ParamsConfig from './params-config' -type Props = { +type Props = Readonly<{ readonly?: boolean hideMetadataFilter?: boolean -} +}> const DatasetConfig: FC = ({ readonly, hideMetadataFilter }) => { const { t } = useTranslation() const userProfile = useAppContextSelector(s => s.userProfile) diff --git a/web/app/components/app/configuration/dataset-config/params-config/config-content.tsx b/web/app/components/app/configuration/dataset-config/params-config/config-content.tsx index 5702776d602..0d0a56e5845 100644 --- a/web/app/components/app/configuration/dataset-config/params-config/config-content.tsx +++ b/web/app/components/app/configuration/dataset-config/params-config/config-content.tsx @@ -27,7 +27,7 @@ import { RerankingModeEnum } from '@/models/datasets' import { RETRIEVE_TYPE } from '@/types/app' import WeightedScore from './weighted-score' -type Props = { +type Props = Readonly<{ datasetConfigs: DatasetConfigs onChange: (configs: DatasetConfigs, isRetrievalModeChange?: boolean) => void selectedDatasets?: DataSet[] @@ -35,7 +35,7 @@ type Props = { singleRetrievalModelConfig?: ModelConfig onSingleRetrievalModelChange?: ModelParameterModalProps['setModel'] onSingleRetrievalModelParamsChange?: ModelParameterModalProps['onCompletionParamsChange'] -} +}> const noopModelChange: ModelParameterModalProps['setModel'] = () => {} const noopParamsChange: ModelParameterModalProps['onCompletionParamsChange'] = () => {} diff --git a/web/app/components/app/configuration/debug/chat-user-input.tsx b/web/app/components/app/configuration/debug/chat-user-input.tsx index c518bbc32dc..65322cae1b1 100644 --- a/web/app/components/app/configuration/debug/chat-user-input.tsx +++ b/web/app/components/app/configuration/debug/chat-user-input.tsx @@ -10,9 +10,9 @@ import Input from '@/app/components/base/input' import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input' import ConfigContext from '@/context/debug-configuration' -type Props = { +type Props = Readonly<{ inputs: Inputs -} +}> const ChatUserInput = ({ inputs, diff --git a/web/app/components/app/create-from-dsl-modal/uploader.tsx b/web/app/components/app/create-from-dsl-modal/uploader.tsx index 0d25fb2f58c..4cde6d522ca 100644 --- a/web/app/components/app/create-from-dsl-modal/uploader.tsx +++ b/web/app/components/app/create-from-dsl-modal/uploader.tsx @@ -13,13 +13,13 @@ import ActionButton from '@/app/components/base/action-button' import { Yaml as YamlIcon } from '@/app/components/base/icons/src/public/files' import { formatFileSize } from '@/utils/format' -type Props = { +type Props = Readonly<{ file: File | undefined updateFile: (file?: File) => void className?: string accept?: string displayName?: string -} +}> const Uploader: FC = ({ file, diff --git a/web/app/components/app/log-annotation/index.tsx b/web/app/components/app/log-annotation/index.tsx index 857b306f105..be0de309024 100644 --- a/web/app/components/app/log-annotation/index.tsx +++ b/web/app/components/app/log-annotation/index.tsx @@ -14,9 +14,9 @@ import TabSlider from '@/app/components/base/tab-slider-plain' import { useRouter } from '@/next/navigation' import { AppModeEnum } from '@/types/app' -type Props = { +type Props = Readonly<{ pageType: PageType -} +}> const LogAnnotation: FC = ({ pageType, diff --git a/web/app/components/app/log/model-info.tsx b/web/app/components/app/log/model-info.tsx index 0344c706b30..ce1f27a3153 100644 --- a/web/app/components/app/log/model-info.tsx +++ b/web/app/components/app/log/model-info.tsx @@ -20,9 +20,9 @@ const PARAM_MAP = { frequency_penalty: 'Frequency Penalty', } -type Props = { +type Props = Readonly<{ model: any -} +}> const ModelInfo: FC = ({ model, diff --git a/web/app/components/app/log/var-panel.tsx b/web/app/components/app/log/var-panel.tsx index 3c70775dbab..d68ae5774a1 100644 --- a/web/app/components/app/log/var-panel.tsx +++ b/web/app/components/app/log/var-panel.tsx @@ -12,10 +12,10 @@ import { useTranslation } from 'react-i18next' import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' import ImagePreview from '@/app/components/base/image-uploader/image-preview' -type Props = { +type Props = Readonly<{ varList: { label: string, value: string }[] message_files: string[] -} +}> const VarPanel: FC = ({ varList, diff --git a/web/app/components/app/overview/embedded/index.tsx b/web/app/components/app/overview/embedded/index.tsx index b68096b1c50..70b90c3cc04 100644 --- a/web/app/components/app/overview/embedded/index.tsx +++ b/web/app/components/app/overview/embedded/index.tsx @@ -27,7 +27,7 @@ import { import WorkflowHiddenInputFields from '../workflow-hidden-input-fields' import style from './style.module.css' -type Props = { +type Props = Readonly<{ siteInfo?: SiteInfo isShow: boolean onClose: () => void @@ -35,7 +35,7 @@ type Props = { appBaseUrl?: string hiddenInputs?: WorkflowHiddenStartVariable[] className?: string -} +}> const OPTION_KEYS = ['iframe', 'scripts', 'chromePlugin'] as const const prefixEmbedded = 'overview.appInfo.embedded' From 56b82449fc6b6ca7c54de4a0d7cf1400d2487513 Mon Sep 17 00:00:00 2001 From: Rohit Gahlawat <283466839+Rohit-Gahlawat@users.noreply.github.com> Date: Thu, 11 Jun 2026 05:46:21 +0530 Subject: [PATCH 007/122] refactor(web): mark Props of datasets/ components as read-only (#25219) (#37300) --- web/app/components/datasets/common/chunking-mode-label.tsx | 4 ++-- web/app/components/datasets/common/document-file-icon.tsx | 4 ++-- .../datasets/common/document-picker/document-list.tsx | 4 ++-- web/app/components/datasets/common/document-picker/index.tsx | 4 ++-- .../common/document-picker/preview-document-picker.tsx | 4 ++-- .../document-status-with-action/auto-disabled-document.tsx | 4 ++-- .../common/document-status-with-action/index-failed.tsx | 4 ++-- .../common/document-status-with-action/status-with-action.tsx | 4 ++-- .../common/economical-retrieval-method-config/index.tsx | 4 ++-- .../datasets/common/retrieval-method-config/index.tsx | 4 ++-- .../datasets/common/retrieval-method-info/index.tsx | 4 ++-- .../datasets/common/retrieval-param-config/index.tsx | 4 ++-- .../create-options/create-from-dsl-modal/uploader.tsx | 4 ++-- .../datasets/create/website/base/checkbox-with-label.tsx | 4 ++-- .../datasets/create/website/base/crawled-result-item.tsx | 4 ++-- .../datasets/create/website/base/crawled-result.tsx | 4 ++-- web/app/components/datasets/create/website/base/crawling.tsx | 4 ++-- .../components/datasets/create/website/base/error-message.tsx | 4 ++-- web/app/components/datasets/create/website/base/field.tsx | 4 ++-- .../components/datasets/create/website/base/options-wrap.tsx | 4 ++-- .../components/datasets/create/website/base/text-input.tsx | 4 ++-- web/app/components/datasets/create/website/base/url-input.tsx | 4 ++-- .../components/datasets/create/website/firecrawl/index.tsx | 4 ++-- .../components/datasets/create/website/firecrawl/options.tsx | 4 ++-- web/app/components/datasets/create/website/index.tsx | 4 ++-- .../datasets/create/website/jina-reader/base/url-input.tsx | 4 ++-- .../components/datasets/create/website/jina-reader/index.tsx | 4 ++-- .../datasets/create/website/jina-reader/options.tsx | 4 ++-- web/app/components/datasets/create/website/no-data.tsx | 4 ++-- .../components/datasets/create/website/watercrawl/index.tsx | 4 ++-- .../components/datasets/create/website/watercrawl/options.tsx | 4 ++-- .../components/datasets/documents/components/rename-modal.tsx | 4 ++-- .../datasets/documents/detail/batch-modal/csv-uploader.tsx | 4 ++-- web/app/components/datasets/hit-testing/index.tsx | 4 ++-- .../datasets/hit-testing/modify-retrieval-modal.tsx | 4 ++-- web/app/components/datasets/list/datasets.tsx | 4 ++-- web/app/components/datasets/metadata/base/date-picker.tsx | 4 ++-- .../datasets/metadata/edit-metadata-batch/add-row.tsx | 4 ++-- .../datasets/metadata/edit-metadata-batch/edit-row.tsx | 4 ++-- .../datasets/metadata/edit-metadata-batch/edited-beacon.tsx | 4 ++-- .../datasets/metadata/edit-metadata-batch/input-combined.tsx | 4 ++-- .../edit-metadata-batch/input-has-set-multiple-value.tsx | 4 ++-- .../datasets/metadata/edit-metadata-batch/label.tsx | 4 ++-- .../datasets/metadata/edit-metadata-batch/modal.tsx | 4 ++-- .../metadata/hooks/use-batch-edit-document-metadata.ts | 4 ++-- .../datasets/metadata/hooks/use-metadata-document.ts | 4 ++-- .../datasets/metadata/metadata-dataset/create-content.tsx | 4 ++-- .../metadata/metadata-dataset/create-metadata-modal.tsx | 4 ++-- .../metadata/metadata-dataset/dataset-metadata-drawer.tsx | 4 ++-- .../components/datasets/metadata/metadata-dataset/field.tsx | 4 ++-- .../components/datasets/metadata/metadata-document/field.tsx | 4 ++-- .../components/datasets/metadata/metadata-document/index.tsx | 4 ++-- .../datasets/metadata/metadata-document/info-group.tsx | 4 ++-- .../datasets/metadata/metadata-document/no-data.tsx | 4 ++-- 54 files changed, 108 insertions(+), 108 deletions(-) diff --git a/web/app/components/datasets/common/chunking-mode-label.tsx b/web/app/components/datasets/common/chunking-mode-label.tsx index 7afc56f0d6f..6917650297e 100644 --- a/web/app/components/datasets/common/chunking-mode-label.tsx +++ b/web/app/components/datasets/common/chunking-mode-label.tsx @@ -5,10 +5,10 @@ import { useTranslation } from 'react-i18next' import Badge from '@/app/components/base/badge' import { GeneralChunk, ParentChildChunk } from '@/app/components/base/icons/src/vender/knowledge' -type Props = { +type Props = Readonly<{ isGeneralMode: boolean isQAMode: boolean -} +}> const ChunkingModeLabel: FC = ({ isGeneralMode, diff --git a/web/app/components/datasets/common/document-file-icon.tsx b/web/app/components/datasets/common/document-file-icon.tsx index d831cd78aad..edf79c6c114 100644 --- a/web/app/components/datasets/common/document-file-icon.tsx +++ b/web/app/components/datasets/common/document-file-icon.tsx @@ -19,12 +19,12 @@ const extendToFileTypeMap: { [key: string]: FileAppearanceType } = { docx: FileAppearanceTypeEnum.word, } -type Props = { +type Props = Readonly<{ extension?: string name?: string size?: 'sm' | 'md' | 'lg' | 'xl' className?: string -} +}> const DocumentFileIcon: FC = ({ extension, diff --git a/web/app/components/datasets/common/document-picker/document-list.tsx b/web/app/components/datasets/common/document-picker/document-list.tsx index 366e744cbd1..726efb5f260 100644 --- a/web/app/components/datasets/common/document-picker/document-list.tsx +++ b/web/app/components/datasets/common/document-picker/document-list.tsx @@ -8,9 +8,9 @@ import { } from '@langgenius/dify-ui/combobox' import FileIcon from '../document-file-icon' -type Props = { +type Props = Readonly<{ className?: string -} +}> function getDocumentExtension(document: SimpleDocumentDetail) { const detailExtension = document.data_source_detail_dict?.upload_file?.extension diff --git a/web/app/components/datasets/common/document-picker/index.tsx b/web/app/components/datasets/common/document-picker/index.tsx index 9f06727ed9b..49a225f6e23 100644 --- a/web/app/components/datasets/common/document-picker/index.tsx +++ b/web/app/components/datasets/common/document-picker/index.tsx @@ -23,12 +23,12 @@ import { useDocumentList } from '@/service/knowledge/use-document' import FileIcon from '../document-file-icon' import DocumentList from './document-list' -type Props = { +type Props = Readonly<{ datasetId: string value?: SimpleDocumentDetail | null parentMode?: ParentMode onChange: (value: SimpleDocumentDetail) => void -} +}> function getDocumentLabel(document: SimpleDocumentDetail) { return document.name diff --git a/web/app/components/datasets/common/document-picker/preview-document-picker.tsx b/web/app/components/datasets/common/document-picker/preview-document-picker.tsx index 3869ffd2a15..c9d387ce90d 100644 --- a/web/app/components/datasets/common/document-picker/preview-document-picker.tsx +++ b/web/app/components/datasets/common/document-picker/preview-document-picker.tsx @@ -15,12 +15,12 @@ import { useTranslation } from 'react-i18next' import Loading from '@/app/components/base/loading' import FileIcon from '../document-file-icon' -type Props = { +type Props = Readonly<{ className?: string value?: DocumentItem files: DocumentItem[] onChange: (value: DocumentItem) => void -} +}> const PreviewDocumentPicker: FC = ({ className, diff --git a/web/app/components/datasets/common/document-status-with-action/auto-disabled-document.tsx b/web/app/components/datasets/common/document-status-with-action/auto-disabled-document.tsx index c150ac823f8..9b4c44c29db 100644 --- a/web/app/components/datasets/common/document-status-with-action/auto-disabled-document.tsx +++ b/web/app/components/datasets/common/document-status-with-action/auto-disabled-document.tsx @@ -7,9 +7,9 @@ import { useTranslation } from 'react-i18next' import { useAutoDisabledDocuments, useDocumentEnable, useInvalidDisabledDocument } from '@/service/knowledge/use-document' import StatusWithAction from './status-with-action' -type Props = { +type Props = Readonly<{ datasetId: string -} +}> const AutoDisabledDocument: FC = ({ datasetId, diff --git a/web/app/components/datasets/common/document-status-with-action/index-failed.tsx b/web/app/components/datasets/common/document-status-with-action/index-failed.tsx index b691f4e8c5b..02373cfe266 100644 --- a/web/app/components/datasets/common/document-status-with-action/index-failed.tsx +++ b/web/app/components/datasets/common/document-status-with-action/index-failed.tsx @@ -9,9 +9,9 @@ import { retryErrorDocs } from '@/service/datasets' import { useDatasetErrorDocs } from '@/service/knowledge/use-dataset' import StatusWithAction from './status-with-action' -type Props = { +type Props = Readonly<{ datasetId: string -} +}> type IIndexState = { value: string } diff --git a/web/app/components/datasets/common/document-status-with-action/status-with-action.tsx b/web/app/components/datasets/common/document-status-with-action/status-with-action.tsx index ce93201e812..f9d8023826c 100644 --- a/web/app/components/datasets/common/document-status-with-action/status-with-action.tsx +++ b/web/app/components/datasets/common/document-status-with-action/status-with-action.tsx @@ -6,13 +6,13 @@ import * as React from 'react' import Divider from '@/app/components/base/divider' type Status = 'success' | 'error' | 'warning' | 'info' -type Props = { +type Props = Readonly<{ type?: Status description: string actionText?: string onAction?: () => void disabled?: boolean -} +}> const IconMap = { success: { diff --git a/web/app/components/datasets/common/economical-retrieval-method-config/index.tsx b/web/app/components/datasets/common/economical-retrieval-method-config/index.tsx index d5364507e74..65facad266f 100644 --- a/web/app/components/datasets/common/economical-retrieval-method-config/index.tsx +++ b/web/app/components/datasets/common/economical-retrieval-method-config/index.tsx @@ -9,11 +9,11 @@ import { EffectColor } from '../../settings/chunk-structure/types' import OptionCard from '../../settings/option-card' import RetrievalParamConfig from '../retrieval-param-config' -type Props = { +type Props = Readonly<{ disabled?: boolean value: RetrievalConfig onChange: (value: RetrievalConfig) => void -} +}> const EconomicalRetrievalMethodConfig: FC = ({ disabled = false, diff --git a/web/app/components/datasets/common/retrieval-method-config/index.tsx b/web/app/components/datasets/common/retrieval-method-config/index.tsx index dd37187e690..52fdfa97529 100644 --- a/web/app/components/datasets/common/retrieval-method-config/index.tsx +++ b/web/app/components/datasets/common/retrieval-method-config/index.tsx @@ -18,12 +18,12 @@ import { EffectColor } from '../../settings/chunk-structure/types' import OptionCard from '../../settings/option-card' import RetrievalParamConfig from '../retrieval-param-config' -type Props = { +type Props = Readonly<{ disabled?: boolean value: RetrievalConfig showMultiModalTip?: boolean onChange: (value: RetrievalConfig) => void -} +}> const RetrievalMethodConfig: FC = ({ disabled = false, diff --git a/web/app/components/datasets/common/retrieval-method-info/index.tsx b/web/app/components/datasets/common/retrieval-method-info/index.tsx index c219a5c15f6..6b6b4138aa0 100644 --- a/web/app/components/datasets/common/retrieval-method-info/index.tsx +++ b/web/app/components/datasets/common/retrieval-method-info/index.tsx @@ -7,9 +7,9 @@ import RadioCard from '@/app/components/base/radio-card' import { RETRIEVE_METHOD } from '@/types/app' import { retrievalIcon } from '../../create/icons' -type Props = { +type Props = Readonly<{ value: RetrievalConfig -} +}> export const getIcon = (type: RETRIEVE_METHOD) => { return ({ diff --git a/web/app/components/datasets/common/retrieval-param-config/index.tsx b/web/app/components/datasets/common/retrieval-param-config/index.tsx index 43c0071115d..fe9152944c2 100644 --- a/web/app/components/datasets/common/retrieval-param-config/index.tsx +++ b/web/app/components/datasets/common/retrieval-param-config/index.tsx @@ -27,12 +27,12 @@ import { RETRIEVE_METHOD } from '@/types/app' import ProgressIndicator from '../../create/assets/progress-indicator.svg' import Reranking from '../../create/assets/rerank.svg' -type Props = { +type Props = Readonly<{ type: RETRIEVE_METHOD value: RetrievalConfig showMultiModalTip?: boolean onChange: (value: RetrievalConfig) => void -} +}> const RetrievalParamConfig: FC = ({ type, diff --git a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/uploader.tsx b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/uploader.tsx index cc2c159fa27..9f19af4acc0 100644 --- a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/uploader.tsx +++ b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/uploader.tsx @@ -9,11 +9,11 @@ import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' import { formatFileSize } from '@/utils/format' -type Props = { +type Props = Readonly<{ file: File | undefined updateFile: (file?: File) => void className?: string -} +}> const Uploader: FC = ({ file, updateFile, className }) => { const { t } = useTranslation() const [dragging, setDragging] = useState(false) diff --git a/web/app/components/datasets/create/website/base/checkbox-with-label.tsx b/web/app/components/datasets/create/website/base/checkbox-with-label.tsx index 5ccb5cbf06b..2868e8e45de 100644 --- a/web/app/components/datasets/create/website/base/checkbox-with-label.tsx +++ b/web/app/components/datasets/create/website/base/checkbox-with-label.tsx @@ -3,14 +3,14 @@ import { Checkbox } from '@langgenius/dify-ui/checkbox' import { cn } from '@langgenius/dify-ui/cn' import { Infotip } from '@/app/components/base/infotip' -type Props = { +type Props = Readonly<{ className?: string isChecked: boolean onChange: (isChecked: boolean) => void label: string labelClassName?: string tooltip?: string -} +}> export default function CheckboxWithLabel({ className = '', diff --git a/web/app/components/datasets/create/website/base/crawled-result-item.tsx b/web/app/components/datasets/create/website/base/crawled-result-item.tsx index 2283e2d36b5..94b0088deef 100644 --- a/web/app/components/datasets/create/website/base/crawled-result-item.tsx +++ b/web/app/components/datasets/create/website/base/crawled-result-item.tsx @@ -7,13 +7,13 @@ import { cn } from '@langgenius/dify-ui/cn' import * as React from 'react' import { useTranslation } from 'react-i18next' -type Props = { +type Props = Readonly<{ payload: CrawlResultItemType isChecked: boolean isPreview: boolean onCheckChange: (checked: boolean) => void onPreview: () => void -} +}> const CrawledResultItem: FC = ({ isPreview, diff --git a/web/app/components/datasets/create/website/base/crawled-result.tsx b/web/app/components/datasets/create/website/base/crawled-result.tsx index b008f9e7c67..15f85e3c078 100644 --- a/web/app/components/datasets/create/website/base/crawled-result.tsx +++ b/web/app/components/datasets/create/website/base/crawled-result.tsx @@ -10,14 +10,14 @@ import CrawledResultItem from './crawled-result-item' const I18N_PREFIX = 'stepOne.website' -type Props = { +type Props = Readonly<{ className?: string list: CrawlResultItem[] checkedList: CrawlResultItem[] onSelectedChange: (selected: CrawlResultItem[]) => void onPreview: (payload: CrawlResultItem) => void usedTime: number -} +}> const CrawledResult: FC = ({ className = '', diff --git a/web/app/components/datasets/create/website/base/crawling.tsx b/web/app/components/datasets/create/website/base/crawling.tsx index c2619efc380..4fd3928031f 100644 --- a/web/app/components/datasets/create/website/base/crawling.tsx +++ b/web/app/components/datasets/create/website/base/crawling.tsx @@ -4,11 +4,11 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { RowStruct } from '@/app/components/base/icons/src/public/other' -type Props = { +type Props = Readonly<{ className?: string crawledNum: number totalNum: number -} +}> const Crawling: FC = ({ className = '', diff --git a/web/app/components/datasets/create/website/base/error-message.tsx b/web/app/components/datasets/create/website/base/error-message.tsx index 74b80ef8cf2..0344c0b066f 100644 --- a/web/app/components/datasets/create/website/base/error-message.tsx +++ b/web/app/components/datasets/create/website/base/error-message.tsx @@ -4,11 +4,11 @@ import { cn } from '@langgenius/dify-ui/cn' import * as React from 'react' import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' -type Props = { +type Props = Readonly<{ className?: string title: string errorMsg?: string -} +}> const ErrorMessage: FC = ({ className, diff --git a/web/app/components/datasets/create/website/base/field.tsx b/web/app/components/datasets/create/website/base/field.tsx index 42e32b987ff..33227adc3fc 100644 --- a/web/app/components/datasets/create/website/base/field.tsx +++ b/web/app/components/datasets/create/website/base/field.tsx @@ -5,7 +5,7 @@ import * as React from 'react' import { Infotip } from '@/app/components/base/infotip' import Input from './text-input' -type Props = { +type Props = Readonly<{ className?: string label: string labelClassName?: string @@ -15,7 +15,7 @@ type Props = { placeholder?: string isNumber?: boolean tooltip?: string -} +}> const Field: FC = ({ className, diff --git a/web/app/components/datasets/create/website/base/options-wrap.tsx b/web/app/components/datasets/create/website/base/options-wrap.tsx index 569b0c8ee47..cbf45f2e9fc 100644 --- a/web/app/components/datasets/create/website/base/options-wrap.tsx +++ b/web/app/components/datasets/create/website/base/options-wrap.tsx @@ -10,11 +10,11 @@ import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows const I18N_PREFIX = 'stepOne.website' -type Props = { +type Props = Readonly<{ className?: string children: React.ReactNode controlFoldOptions?: number -} +}> const OptionsWrap: FC = ({ className = '', diff --git a/web/app/components/datasets/create/website/base/text-input.tsx b/web/app/components/datasets/create/website/base/text-input.tsx index a9c42a6b7c5..8048d614ff1 100644 --- a/web/app/components/datasets/create/website/base/text-input.tsx +++ b/web/app/components/datasets/create/website/base/text-input.tsx @@ -3,12 +3,12 @@ import type { FC } from 'react' import * as React from 'react' import { useCallback } from 'react' -type Props = { +type Props = Readonly<{ value: string | number onChange: (value: string | number) => void placeholder?: string isNumber?: boolean -} +}> const MIN_VALUE = 0 diff --git a/web/app/components/datasets/create/website/base/url-input.tsx b/web/app/components/datasets/create/website/base/url-input.tsx index da6dc768457..a4b461dbb82 100644 --- a/web/app/components/datasets/create/website/base/url-input.tsx +++ b/web/app/components/datasets/create/website/base/url-input.tsx @@ -9,10 +9,10 @@ import Input from './text-input' const I18N_PREFIX = 'stepOne.website' -type Props = { +type Props = Readonly<{ isRunning: boolean onRun: (url: string) => void -} +}> const UrlInput: FC = ({ isRunning, diff --git a/web/app/components/datasets/create/website/firecrawl/index.tsx b/web/app/components/datasets/create/website/firecrawl/index.tsx index 4f6077d6cc1..4edb0200f27 100644 --- a/web/app/components/datasets/create/website/firecrawl/index.tsx +++ b/web/app/components/datasets/create/website/firecrawl/index.tsx @@ -19,14 +19,14 @@ import Options from './options' const ERROR_I18N_PREFIX = 'errorMsg' const I18N_PREFIX = 'stepOne.website' -type Props = { +type Props = Readonly<{ onPreview: (payload: CrawlResultItem) => void checkedCrawlResult: CrawlResultItem[] onCheckedCrawlResultChange: (payload: CrawlResultItem[]) => void onJobIdChange: (jobId: string) => void crawlOptions: CrawlOptions onCrawlOptionsChange: (payload: CrawlOptions) => void -} +}> const Step = { init: 'init', running: 'running', diff --git a/web/app/components/datasets/create/website/firecrawl/options.tsx b/web/app/components/datasets/create/website/firecrawl/options.tsx index 3496bbb236c..6eb52cc87e4 100644 --- a/web/app/components/datasets/create/website/firecrawl/options.tsx +++ b/web/app/components/datasets/create/website/firecrawl/options.tsx @@ -10,11 +10,11 @@ import Field from '../base/field' const I18N_PREFIX = 'stepOne.website' -type Props = { +type Props = Readonly<{ className?: string payload: CrawlOptions onChange: (payload: CrawlOptions) => void -} +}> const Options: FC = ({ className = '', diff --git a/web/app/components/datasets/create/website/index.tsx b/web/app/components/datasets/create/website/index.tsx index 5e6fce527da..c8f02f3aa27 100644 --- a/web/app/components/datasets/create/website/index.tsx +++ b/web/app/components/datasets/create/website/index.tsx @@ -16,7 +16,7 @@ import JinaReader from './jina-reader' import NoData from './no-data' import Watercrawl from './watercrawl' -type Props = { +type Props = Readonly<{ onPreview: (payload: CrawlResultItem) => void checkedCrawlResult: CrawlResultItem[] onCheckedCrawlResultChange: (payload: CrawlResultItem[]) => void @@ -25,7 +25,7 @@ type Props = { crawlOptions: CrawlOptions onCrawlOptionsChange: (payload: CrawlOptions) => void authedDataSourceList: DataSourceAuth[] -} +}> const Website: FC = ({ onPreview, diff --git a/web/app/components/datasets/create/website/jina-reader/base/url-input.tsx b/web/app/components/datasets/create/website/jina-reader/base/url-input.tsx index 9d0cbfe169a..c7df281d658 100644 --- a/web/app/components/datasets/create/website/jina-reader/base/url-input.tsx +++ b/web/app/components/datasets/create/website/jina-reader/base/url-input.tsx @@ -9,10 +9,10 @@ import { useDocLink } from '@/context/i18n' const I18N_PREFIX = 'stepOne.website' -type Props = { +type Props = Readonly<{ isRunning: boolean onRun: (url: string) => void -} +}> const UrlInput: FC = ({ isRunning, diff --git a/web/app/components/datasets/create/website/jina-reader/index.tsx b/web/app/components/datasets/create/website/jina-reader/index.tsx index 3dc3505bb44..6d718535fa2 100644 --- a/web/app/components/datasets/create/website/jina-reader/index.tsx +++ b/web/app/components/datasets/create/website/jina-reader/index.tsx @@ -19,14 +19,14 @@ import Options from './options' const ERROR_I18N_PREFIX = 'errorMsg' const I18N_PREFIX = 'stepOne.website' -type Props = { +type Props = Readonly<{ onPreview: (payload: CrawlResultItem) => void checkedCrawlResult: CrawlResultItem[] onCheckedCrawlResultChange: (payload: CrawlResultItem[]) => void onJobIdChange: (jobId: string) => void crawlOptions: CrawlOptions onCrawlOptionsChange: (payload: CrawlOptions) => void -} +}> const Step = { init: 'init', running: 'running', diff --git a/web/app/components/datasets/create/website/jina-reader/options.tsx b/web/app/components/datasets/create/website/jina-reader/options.tsx index ce13f38ee3f..97ac1bdad88 100644 --- a/web/app/components/datasets/create/website/jina-reader/options.tsx +++ b/web/app/components/datasets/create/website/jina-reader/options.tsx @@ -10,11 +10,11 @@ import Field from '../base/field' const I18N_PREFIX = 'stepOne.website' -type Props = { +type Props = Readonly<{ className?: string payload: CrawlOptions onChange: (payload: CrawlOptions) => void -} +}> const Options: FC = ({ className = '', diff --git a/web/app/components/datasets/create/website/no-data.tsx b/web/app/components/datasets/create/website/no-data.tsx index 0659d103335..ca0bcd8e66c 100644 --- a/web/app/components/datasets/create/website/no-data.tsx +++ b/web/app/components/datasets/create/website/no-data.tsx @@ -10,10 +10,10 @@ import s from './index.module.css' const I18N_PREFIX = 'stepOne.website' -type Props = { +type Props = Readonly<{ onConfig: () => void provider: DataSourceProvider -} +}> const NoData: FC = ({ onConfig, diff --git a/web/app/components/datasets/create/website/watercrawl/index.tsx b/web/app/components/datasets/create/website/watercrawl/index.tsx index 62285479660..1893994af01 100644 --- a/web/app/components/datasets/create/website/watercrawl/index.tsx +++ b/web/app/components/datasets/create/website/watercrawl/index.tsx @@ -19,14 +19,14 @@ import Options from './options' const ERROR_I18N_PREFIX = 'errorMsg' const I18N_PREFIX = 'stepOne.website' -type Props = { +type Props = Readonly<{ onPreview: (payload: CrawlResultItem) => void checkedCrawlResult: CrawlResultItem[] onCheckedCrawlResultChange: (payload: CrawlResultItem[]) => void onJobIdChange: (jobId: string) => void crawlOptions: CrawlOptions onCrawlOptionsChange: (payload: CrawlOptions) => void -} +}> const Step = { init: 'init', running: 'running', diff --git a/web/app/components/datasets/create/website/watercrawl/options.tsx b/web/app/components/datasets/create/website/watercrawl/options.tsx index 3496bbb236c..6eb52cc87e4 100644 --- a/web/app/components/datasets/create/website/watercrawl/options.tsx +++ b/web/app/components/datasets/create/website/watercrawl/options.tsx @@ -10,11 +10,11 @@ import Field from '../base/field' const I18N_PREFIX = 'stepOne.website' -type Props = { +type Props = Readonly<{ className?: string payload: CrawlOptions onChange: (payload: CrawlOptions) => void -} +}> const Options: FC = ({ className = '', diff --git a/web/app/components/datasets/documents/components/rename-modal.tsx b/web/app/components/datasets/documents/components/rename-modal.tsx index ac4fc7fc848..e42fd5654d6 100644 --- a/web/app/components/datasets/documents/components/rename-modal.tsx +++ b/web/app/components/datasets/documents/components/rename-modal.tsx @@ -10,13 +10,13 @@ import { useTranslation } from 'react-i18next' import Input from '@/app/components/base/input' import { renameDocumentName } from '@/service/datasets' -type Props = { +type Props = Readonly<{ datasetId: string documentId: string name: string onClose: () => void onSaved: () => void -} +}> const RenameModal: FC = ({ documentId, diff --git a/web/app/components/datasets/documents/detail/batch-modal/csv-uploader.tsx b/web/app/components/datasets/documents/detail/batch-modal/csv-uploader.tsx index b96f50932e7..ebf7a69aeec 100644 --- a/web/app/components/datasets/documents/detail/batch-modal/csv-uploader.tsx +++ b/web/app/components/datasets/documents/detail/batch-modal/csv-uploader.tsx @@ -16,10 +16,10 @@ import { upload } from '@/service/base' import { useFileUploadConfig } from '@/service/use-common' import { Theme } from '@/types/app' -type Props = { +type Props = Readonly<{ file: FileItem | undefined updateFile: (file?: FileItem) => void -} +}> const CSVUploader: FC = ({ file, updateFile }) => { const { t } = useTranslation() const [dragging, setDragging] = useState(false) diff --git a/web/app/components/datasets/hit-testing/index.tsx b/web/app/components/datasets/hit-testing/index.tsx index f6106761332..161748104e1 100644 --- a/web/app/components/datasets/hit-testing/index.tsx +++ b/web/app/components/datasets/hit-testing/index.tsx @@ -44,9 +44,9 @@ import ModifyRetrievalModal from './modify-retrieval-modal' const limit = 10 -type Props = { +type Props = Readonly<{ datasetId: string -} +}> const HitTestingPage: FC = ({ datasetId }: Props) => { const { t } = useTranslation() 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 87151657f33..4295bd9260d 100644 --- a/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx +++ b/web/app/components/datasets/hit-testing/modify-retrieval-modal.tsx @@ -17,13 +17,13 @@ import { useDocLink } from '@/context/i18n' import { ModelTypeEnum } from '../../header/account-setting/model-provider-page/declarations' import { checkShowMultiModalTip } from '../settings/utils' -type Props = { +type Props = Readonly<{ indexMethod: string value: RetrievalConfig isShow: boolean onHide: () => void onSave: (value: RetrievalConfig) => void -} +}> const ModifyRetrievalModal: FC = ({ indexMethod, value, isShow, onHide, onSave }) => { const ref = useRef(null) const { t } = useTranslation() diff --git a/web/app/components/datasets/list/datasets.tsx b/web/app/components/datasets/list/datasets.tsx index 7ba9326ce5c..8f5c42babc0 100644 --- a/web/app/components/datasets/list/datasets.tsx +++ b/web/app/components/datasets/list/datasets.tsx @@ -9,12 +9,12 @@ import DatasetCard from './dataset-card' import DatasetCardSkeleton from './dataset-card-skeleton' import NewDatasetCard from './new-dataset-card' -type Props = { +type Props = Readonly<{ tags: string[] keywords: string includeAll: boolean onOpenTagManagement?: () => void -} +}> const Datasets = ({ tags, diff --git a/web/app/components/datasets/metadata/base/date-picker.tsx b/web/app/components/datasets/metadata/base/date-picker.tsx index 4306bb6330f..2bb807d2a5b 100644 --- a/web/app/components/datasets/metadata/base/date-picker.tsx +++ b/web/app/components/datasets/metadata/base/date-picker.tsx @@ -12,12 +12,12 @@ import DatePicker from '@/app/components/base/date-and-time-picker/date-picker' import { userProfileQueryOptions } from '@/features/account-profile/client' import useTimestamp from '@/hooks/use-timestamp' -type Props = { +type Props = Readonly<{ className?: string label?: string value?: number onChange: (date: number | null) => void -} +}> const WrappedDatePicker = ({ className, label, diff --git a/web/app/components/datasets/metadata/edit-metadata-batch/add-row.tsx b/web/app/components/datasets/metadata/edit-metadata-batch/add-row.tsx index 8d98df3202d..f7dd573a735 100644 --- a/web/app/components/datasets/metadata/edit-metadata-batch/add-row.tsx +++ b/web/app/components/datasets/metadata/edit-metadata-batch/add-row.tsx @@ -7,12 +7,12 @@ import * as React from 'react' import InputCombined from './input-combined' import Label from './label' -type Props = { +type Props = Readonly<{ className?: string payload: MetadataItemWithEdit onChange: (value: MetadataItemWithEdit) => void onRemove: () => void -} +}> const AddRow: FC = ({ className, diff --git a/web/app/components/datasets/metadata/edit-metadata-batch/edit-row.tsx b/web/app/components/datasets/metadata/edit-metadata-batch/edit-row.tsx index 2fffdb6e549..94da42f18e0 100644 --- a/web/app/components/datasets/metadata/edit-metadata-batch/edit-row.tsx +++ b/web/app/components/datasets/metadata/edit-metadata-batch/edit-row.tsx @@ -10,12 +10,12 @@ import InputCombined from './input-combined' import InputHasSetMultipleValue from './input-has-set-multiple-value' import Label from './label' -type Props = { +type Props = Readonly<{ payload: MetadataItemWithEdit onChange: (payload: MetadataItemWithEdit) => void onRemove: (id: string) => void onReset: (id: string) => void -} +}> const EditMetadatabatchItem: FC = ({ payload, diff --git a/web/app/components/datasets/metadata/edit-metadata-batch/edited-beacon.tsx b/web/app/components/datasets/metadata/edit-metadata-batch/edited-beacon.tsx index da531146af7..6418874949f 100644 --- a/web/app/components/datasets/metadata/edit-metadata-batch/edited-beacon.tsx +++ b/web/app/components/datasets/metadata/edit-metadata-batch/edited-beacon.tsx @@ -7,9 +7,9 @@ import * as React from 'react' import { useRef } from 'react' import { useTranslation } from 'react-i18next' -type Props = { +type Props = Readonly<{ onReset: () => void -} +}> const EditedBeacon: FC = ({ onReset, diff --git a/web/app/components/datasets/metadata/edit-metadata-batch/input-combined.tsx b/web/app/components/datasets/metadata/edit-metadata-batch/input-combined.tsx index 0f9033a0498..5156944c679 100644 --- a/web/app/components/datasets/metadata/edit-metadata-batch/input-combined.tsx +++ b/web/app/components/datasets/metadata/edit-metadata-batch/input-combined.tsx @@ -14,14 +14,14 @@ import * as React from 'react' import Datepicker from '../base/date-picker' import { DataType } from '../types' -type Props = { +type Props = Readonly<{ className?: string label: string type: DataType value: any onChange: (value: any) => void readOnly?: boolean -} +}> const InputCombined: FC = ({ className: configClassName, diff --git a/web/app/components/datasets/metadata/edit-metadata-batch/input-has-set-multiple-value.tsx b/web/app/components/datasets/metadata/edit-metadata-batch/input-has-set-multiple-value.tsx index 337bd47ab0c..c919236c504 100644 --- a/web/app/components/datasets/metadata/edit-metadata-batch/input-has-set-multiple-value.tsx +++ b/web/app/components/datasets/metadata/edit-metadata-batch/input-has-set-multiple-value.tsx @@ -5,10 +5,10 @@ import { RiCloseLine } from '@remixicon/react' import * as React from 'react' import { useTranslation } from 'react-i18next' -type Props = { +type Props = Readonly<{ onClear: () => void readOnly?: boolean -} +}> const InputHasSetMultipleValue: FC = ({ onClear, diff --git a/web/app/components/datasets/metadata/edit-metadata-batch/label.tsx b/web/app/components/datasets/metadata/edit-metadata-batch/label.tsx index 186334e511a..2a1204a4186 100644 --- a/web/app/components/datasets/metadata/edit-metadata-batch/label.tsx +++ b/web/app/components/datasets/metadata/edit-metadata-batch/label.tsx @@ -3,11 +3,11 @@ import type { FC } from 'react' import { cn } from '@langgenius/dify-ui/cn' import * as React from 'react' -type Props = { +type Props = Readonly<{ isDeleted?: boolean className?: string text: string -} +}> const Label: FC = ({ isDeleted, diff --git a/web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx b/web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx index f5642e833aa..b13f4978031 100644 --- a/web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx +++ b/web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx @@ -19,14 +19,14 @@ import AddedMetadataItem from './add-row' import EditMetadataBatchItem from './edit-row' const i18nPrefix = 'metadata.batchEditMetadata' -type Props = { +type Props = Readonly<{ datasetId: string documentNum: number list: MetadataItemInBatchEdit[] onSave: (editedList: MetadataItemInBatchEdit[], addedList: MetadataItemInBatchEdit[], isApplyToAllSelectDocument: boolean) => void onHide: () => void onShowManage: () => void -} +}> const EditMetadataBatchModal: FC = ({ datasetId, documentNum, list, onSave, onHide, onShowManage }) => { const { t } = useTranslation() const [templeList, setTempleList] = useState(list) diff --git a/web/app/components/datasets/metadata/hooks/use-batch-edit-document-metadata.ts b/web/app/components/datasets/metadata/hooks/use-batch-edit-document-metadata.ts index df78b09fcac..a21226fc9f0 100644 --- a/web/app/components/datasets/metadata/hooks/use-batch-edit-document-metadata.ts +++ b/web/app/components/datasets/metadata/hooks/use-batch-edit-document-metadata.ts @@ -7,12 +7,12 @@ import { useMemo } from 'react' import { useBatchUpdateDocMetadata } from '@/service/knowledge/use-metadata' import { UpdateType } from '../types' -type Props = { +type Props = Readonly<{ datasetId: string docList: SimpleDocumentDetail[] selectedDocumentIds?: string[] onUpdate: () => void -} +}> const useBatchEditDocumentMetadata = ({ datasetId, docList, selectedDocumentIds, onUpdate }: Props) => { const [isShowEditModal, { setTrue: showEditModal, setFalse: hideEditModal }] = useBoolean(false) const metaDataList: MetadataItemWithValue[][] = (() => { diff --git a/web/app/components/datasets/metadata/hooks/use-metadata-document.ts b/web/app/components/datasets/metadata/hooks/use-metadata-document.ts index aa6162fb0d0..0e844cf44b1 100644 --- a/web/app/components/datasets/metadata/hooks/use-metadata-document.ts +++ b/web/app/components/datasets/metadata/hooks/use-metadata-document.ts @@ -10,11 +10,11 @@ import { useBatchUpdateDocMetadata, useCreateMetaData, useDatasetMetaData, useDo import { DataType } from '../types' import useCheckMetadataName from './use-check-metadata-name' -type Props = { +type Props = Readonly<{ datasetId: string documentId: string docDetail: FullDocumentDetail -} +}> const useMetadataDocument = ({ datasetId, documentId, docDetail }: Props) => { const { t } = useTranslation() const { dataset } = useDatasetDetailContext() diff --git a/web/app/components/datasets/metadata/metadata-dataset/create-content.tsx b/web/app/components/datasets/metadata/metadata-dataset/create-content.tsx index a1236f0ea3b..061cf346193 100644 --- a/web/app/components/datasets/metadata/metadata-dataset/create-content.tsx +++ b/web/app/components/datasets/metadata/metadata-dataset/create-content.tsx @@ -11,12 +11,12 @@ import Field from './field' const i18nPrefix = 'metadata.createMetadata' -export type Props = { +export type Props = Readonly<{ onClose?: () => void onSave: (data: BuiltInMetadataItem) => void hasBack?: boolean onBack?: () => void -} +}> export function CreateContent({ onClose = noop, diff --git a/web/app/components/datasets/metadata/metadata-dataset/create-metadata-modal.tsx b/web/app/components/datasets/metadata/metadata-dataset/create-metadata-modal.tsx index 68163bd8d5f..50b2ca8fc94 100644 --- a/web/app/components/datasets/metadata/metadata-dataset/create-metadata-modal.tsx +++ b/web/app/components/datasets/metadata/metadata-dataset/create-metadata-modal.tsx @@ -4,12 +4,12 @@ import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/pop import * as React from 'react' import { CreateContent } from './create-content' -type Props = { +type Props = Readonly<{ open: boolean setOpen: (open: boolean) => void trigger: React.ReactNode popupLeft?: number -} & CreateContentProps +}> & CreateContentProps export function CreateMetadataModal({ open, diff --git a/web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx b/web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx index 68c031349e0..1e2c3633457 100644 --- a/web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx +++ b/web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx @@ -38,7 +38,7 @@ import Field from './field' const i18nPrefix = 'metadata.datasetMetadata' -type Props = { +type Props = Readonly<{ userMetadata: MetadataItemWithValueLength[] builtInMetadata: BuiltInMetadataItem[] isBuiltInEnabled: boolean @@ -47,7 +47,7 @@ type Props = { onAdd: (payload: BuiltInMetadataItem) => void onRename: (payload: MetadataItemWithValueLength) => void onRemove: (metaDataId: string) => void -} +}> type ItemProps = { readonly?: boolean diff --git a/web/app/components/datasets/metadata/metadata-dataset/field.tsx b/web/app/components/datasets/metadata/metadata-dataset/field.tsx index e78c32ff62d..84d1a9563f9 100644 --- a/web/app/components/datasets/metadata/metadata-dataset/field.tsx +++ b/web/app/components/datasets/metadata/metadata-dataset/field.tsx @@ -2,11 +2,11 @@ import type { FC } from 'react' import * as React from 'react' -type Props = { +type Props = Readonly<{ className?: string label: string children: React.ReactNode -} +}> const Field: FC = ({ className, diff --git a/web/app/components/datasets/metadata/metadata-document/field.tsx b/web/app/components/datasets/metadata/metadata-document/field.tsx index 3993cfbb408..7118738e38e 100644 --- a/web/app/components/datasets/metadata/metadata-document/field.tsx +++ b/web/app/components/datasets/metadata/metadata-document/field.tsx @@ -2,10 +2,10 @@ import type { FC } from 'react' import * as React from 'react' -type Props = { +type Props = Readonly<{ label: string children: React.ReactNode -} +}> const Field: FC = ({ label, diff --git a/web/app/components/datasets/metadata/metadata-document/index.tsx b/web/app/components/datasets/metadata/metadata-document/index.tsx index 18ee6e04eaa..c54b99edbc1 100644 --- a/web/app/components/datasets/metadata/metadata-document/index.tsx +++ b/web/app/components/datasets/metadata/metadata-document/index.tsx @@ -13,12 +13,12 @@ import NoData from './no-data' const i18nPrefix = 'metadata.documentMetadata' -type Props = { +type Props = Readonly<{ datasetId: string documentId: string className?: string docDetail: FullDocumentDetail -} +}> const MetadataDocument: FC = ({ datasetId, documentId, diff --git a/web/app/components/datasets/metadata/metadata-document/info-group.tsx b/web/app/components/datasets/metadata/metadata-document/info-group.tsx index 5c4da3a1994..4ef93919043 100644 --- a/web/app/components/datasets/metadata/metadata-document/info-group.tsx +++ b/web/app/components/datasets/metadata/metadata-document/info-group.tsx @@ -14,7 +14,7 @@ import { DatasetMetadataPicker } from '../metadata-dataset/dataset-metadata-pick import { DataType, isShowManageMetadataLocalStorageKey } from '../types' import Field from './field' -type Props = { +type Props = Readonly<{ dataSetId: string className?: string noHeader?: boolean @@ -29,7 +29,7 @@ type Props = { onDelete?: (item: MetadataItemWithValue) => void onSelect?: (item: MetadataItemWithValue) => void onAdd?: (item: BuiltInMetadataItem) => void -} +}> const InfoGroup: FC = ({ dataSetId, diff --git a/web/app/components/datasets/metadata/metadata-document/no-data.tsx b/web/app/components/datasets/metadata/metadata-document/no-data.tsx index 82c4b8c2746..fb4e3d3a4e6 100644 --- a/web/app/components/datasets/metadata/metadata-document/no-data.tsx +++ b/web/app/components/datasets/metadata/metadata-document/no-data.tsx @@ -5,9 +5,9 @@ import { RiArrowRightLine } from '@remixicon/react' import * as React from 'react' import { useTranslation } from 'react-i18next' -type Props = { +type Props = Readonly<{ onStart: () => void -} +}> const NoData: FC = ({ onStart, From ffeccfff0cc351c7227569cdc5092553aff655c2 Mon Sep 17 00:00:00 2001 From: Rohit Gahlawat <283466839+Rohit-Gahlawat@users.noreply.github.com> Date: Thu, 11 Jun 2026 05:47:17 +0530 Subject: [PATCH 008/122] refactor(web): mark Props of base/ components as read-only (#25219) (#37302) --- web/app/components/base/alert.tsx | 4 ++-- .../chat-with-history/header/mobile-operation-dropdown.tsx | 4 ++-- .../base/chat/chat-with-history/header/operation.tsx | 4 ++-- .../base/chat/chat-with-history/inputs-form/content.tsx | 4 ++-- .../base/chat/chat-with-history/inputs-form/index.tsx | 4 ++-- .../components/base/chat/chat-with-history/sidebar/index.tsx | 4 ++-- .../base/chat/chat-with-history/sidebar/operation.tsx | 4 ++-- .../chat/chat/answer/human-input-content/field-renderer.tsx | 4 ++-- .../base/chat/embedded-chatbot/inputs-form/content.tsx | 4 ++-- .../base/chat/embedded-chatbot/inputs-form/index.tsx | 4 ++-- .../chat/embedded-chatbot/inputs-form/view-form-dropdown.tsx | 4 ++-- web/app/components/base/copy-feedback/index.tsx | 4 ++-- web/app/components/base/copy-icon/index.tsx | 4 ++-- .../base/date-and-time-picker/time-picker/header.tsx | 4 ++-- web/app/components/base/encrypted-bottom/index.tsx | 4 ++-- .../annotation-reply/annotation-ctrl-button.tsx | 4 ++-- .../new-feature-panel/annotation-reply/config-param-modal.tsx | 4 ++-- .../features/new-feature-panel/annotation-reply/index.tsx | 4 ++-- .../new-feature-panel/annotation-reply/score-slider/index.tsx | 4 ++-- .../components/base/features/new-feature-panel/citation.tsx | 4 ++-- .../features/new-feature-panel/conversation-opener/index.tsx | 4 ++-- .../base/features/new-feature-panel/feature-bar.tsx | 4 ++-- .../base/features/new-feature-panel/feature-card.tsx | 4 ++-- .../base/features/new-feature-panel/file-upload/index.tsx | 4 ++-- .../components/base/features/new-feature-panel/follow-up.tsx | 4 ++-- .../base/features/new-feature-panel/image-upload/index.tsx | 4 ++-- web/app/components/base/features/new-feature-panel/index.tsx | 4 ++-- .../base/features/new-feature-panel/moderation/index.tsx | 4 ++-- .../base/features/new-feature-panel/more-like-this.tsx | 4 ++-- .../base/features/new-feature-panel/speech-to-text.tsx | 4 ++-- .../base/features/new-feature-panel/text-to-speech/index.tsx | 4 ++-- web/app/components/base/file-uploader/file-list-in-log.tsx | 4 ++-- web/app/components/base/image-gallery/index.tsx | 4 ++-- web/app/components/base/param-item/index.tsx | 4 ++-- web/app/components/base/param-item/score-threshold-item.tsx | 4 ++-- web/app/components/base/param-item/top-k-item.tsx | 4 ++-- .../prompt-editor/plugins/error-message-block/component.tsx | 4 ++-- .../prompt-editor/plugins/hitl-input-block/pre-populate.tsx | 4 ++-- .../base/prompt-editor/plugins/hitl-input-block/tag-label.tsx | 4 ++-- .../prompt-editor/plugins/hitl-input-block/type-switch.tsx | 4 ++-- .../base/prompt-editor/plugins/last-run-block/component.tsx | 4 ++-- web/app/components/base/qrcode/index.tsx | 4 ++-- web/app/components/base/sort/index.tsx | 4 ++-- web/app/components/base/spinner/index.tsx | 4 ++-- web/app/components/base/video-gallery/index.tsx | 4 ++-- 45 files changed, 90 insertions(+), 90 deletions(-) diff --git a/web/app/components/base/alert.tsx b/web/app/components/base/alert.tsx index 2eb3e580bdf..626f644a8c2 100644 --- a/web/app/components/base/alert.tsx +++ b/web/app/components/base/alert.tsx @@ -5,12 +5,12 @@ import { } from 'react' import { useTranslation } from 'react-i18next' -type Props = { +type Props = Readonly<{ type?: 'info' message: string onHide: () => void className?: string -} +}> const bgVariants = cva( '', { diff --git a/web/app/components/base/chat/chat-with-history/header/mobile-operation-dropdown.tsx b/web/app/components/base/chat/chat-with-history/header/mobile-operation-dropdown.tsx index 29821e3c5af..d44c98380fa 100644 --- a/web/app/components/base/chat/chat-with-history/header/mobile-operation-dropdown.tsx +++ b/web/app/components/base/chat/chat-with-history/header/mobile-operation-dropdown.tsx @@ -8,11 +8,11 @@ import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import ActionButton, { ActionButtonState } from '@/app/components/base/action-button' -type Props = { +type Props = Readonly<{ handleResetChat: () => void handleViewChatSettings: () => void hideViewChatSettings?: boolean -} +}> const MobileOperationDropdown = ({ handleResetChat, diff --git a/web/app/components/base/chat/chat-with-history/header/operation.tsx b/web/app/components/base/chat/chat-with-history/header/operation.tsx index 57ac96e366d..8580bbc0caf 100644 --- a/web/app/components/base/chat/chat-with-history/header/operation.tsx +++ b/web/app/components/base/chat/chat-with-history/header/operation.tsx @@ -10,7 +10,7 @@ import { import * as React from 'react' import { useTranslation } from 'react-i18next' -type Props = { +type Props = Readonly<{ title: string isPinned: boolean isShowRenameConversation?: boolean @@ -19,7 +19,7 @@ type Props = { togglePin: () => void onDelete: () => void placement?: Placement -} +}> const deferAction = (action: () => void) => { queueMicrotask(action) diff --git a/web/app/components/base/chat/chat-with-history/inputs-form/content.tsx b/web/app/components/base/chat/chat-with-history/inputs-form/content.tsx index 9b3e89c79ae..14258e91b32 100644 --- a/web/app/components/base/chat/chat-with-history/inputs-form/content.tsx +++ b/web/app/components/base/chat/chat-with-history/inputs-form/content.tsx @@ -11,9 +11,9 @@ import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import { InputVarType } from '@/app/components/workflow/types' import { useChatWithHistoryContext } from '../context' -type Props = { +type Props = Readonly<{ showTip?: boolean -} +}> const InputsFormContent = ({ showTip }: Props) => { const { t } = useTranslation() diff --git a/web/app/components/base/chat/chat-with-history/inputs-form/index.tsx b/web/app/components/base/chat/chat-with-history/inputs-form/index.tsx index b0da546bb38..d8f2337985a 100644 --- a/web/app/components/base/chat/chat-with-history/inputs-form/index.tsx +++ b/web/app/components/base/chat/chat-with-history/inputs-form/index.tsx @@ -7,10 +7,10 @@ import Divider from '@/app/components/base/divider' import { Message3Fill } from '@/app/components/base/icons/src/public/other' import { useChatWithHistoryContext } from '../context' -type Props = { +type Props = Readonly<{ collapsed: boolean setCollapsed: (collapsed: boolean) => void -} +}> const InputsFormNode = ({ collapsed, diff --git a/web/app/components/base/chat/chat-with-history/sidebar/index.tsx b/web/app/components/base/chat/chat-with-history/sidebar/index.tsx index 48a76b993a3..89d638df74d 100644 --- a/web/app/components/base/chat/chat-with-history/sidebar/index.tsx +++ b/web/app/components/base/chat/chat-with-history/sidebar/index.tsx @@ -25,10 +25,10 @@ import MenuDropdown from '@/app/components/share/text-generation/menu-dropdown' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { useChatWithHistoryContext } from '../context' -type Props = { +type Props = Readonly<{ isPanel?: boolean panelVisible?: boolean -} +}> const Sidebar = ({ isPanel }: Props) => { const { t } = useTranslation() diff --git a/web/app/components/base/chat/chat-with-history/sidebar/operation.tsx b/web/app/components/base/chat/chat-with-history/sidebar/operation.tsx index 89ddc12caa2..0a2e9f4298a 100644 --- a/web/app/components/base/chat/chat-with-history/sidebar/operation.tsx +++ b/web/app/components/base/chat/chat-with-history/sidebar/operation.tsx @@ -13,7 +13,7 @@ import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import ActionButton, { ActionButtonState } from '@/app/components/base/action-button' -type Props = { +type Props = Readonly<{ isActive?: boolean isItemHovering?: boolean isPinned: boolean @@ -22,7 +22,7 @@ type Props = { isShowDelete: boolean togglePin: () => void onDelete: () => void -} +}> const Operation: FC = ({ isActive, diff --git a/web/app/components/base/chat/chat/answer/human-input-content/field-renderer.tsx b/web/app/components/base/chat/chat/answer/human-input-content/field-renderer.tsx index d682fc3e237..b76df433d5b 100644 --- a/web/app/components/base/chat/chat/answer/human-input-content/field-renderer.tsx +++ b/web/app/components/base/chat/chat/answer/human-input-content/field-renderer.tsx @@ -20,11 +20,11 @@ import { export type HumanInputFieldValue = string | FileEntity | FileEntity[] | null -type Props = { +type Props = Readonly<{ field: FormInputItem value?: HumanInputFieldValue onChange: (value: HumanInputFieldValue) => void -} +}> const HumanInputFieldRenderer = ({ field, diff --git a/web/app/components/base/chat/embedded-chatbot/inputs-form/content.tsx b/web/app/components/base/chat/embedded-chatbot/inputs-form/content.tsx index a3c57d16526..5116c2aff2c 100644 --- a/web/app/components/base/chat/embedded-chatbot/inputs-form/content.tsx +++ b/web/app/components/base/chat/embedded-chatbot/inputs-form/content.tsx @@ -11,9 +11,9 @@ import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import { InputVarType } from '@/app/components/workflow/types' import { useEmbeddedChatbotContext } from '../context' -type Props = { +type Props = Readonly<{ showTip?: boolean -} +}> const InputsFormContent = ({ showTip }: Props) => { const { t } = useTranslation() diff --git a/web/app/components/base/chat/embedded-chatbot/inputs-form/index.tsx b/web/app/components/base/chat/embedded-chatbot/inputs-form/index.tsx index 1c43894701a..f55c1f27ccc 100644 --- a/web/app/components/base/chat/embedded-chatbot/inputs-form/index.tsx +++ b/web/app/components/base/chat/embedded-chatbot/inputs-form/index.tsx @@ -7,10 +7,10 @@ import Divider from '@/app/components/base/divider' import { AppSourceType } from '@/service/share' import { useEmbeddedChatbotContext } from '../context' -type Props = { +type Props = Readonly<{ collapsed: boolean setCollapsed: (collapsed: boolean) => void -} +}> const InputsFormNode = ({ collapsed, diff --git a/web/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown.tsx b/web/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown.tsx index aa6f5d65cc1..ebdde76960c 100644 --- a/web/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown.tsx +++ b/web/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown.tsx @@ -6,9 +6,9 @@ import { useTranslation } from 'react-i18next' import ActionButton, { ActionButtonState } from '@/app/components/base/action-button' import InputsFormContent from '@/app/components/base/chat/embedded-chatbot/inputs-form/content' -type Props = { +type Props = Readonly<{ iconColor?: string -} +}> const ViewFormDropdown = ({ iconColor, diff --git a/web/app/components/base/copy-feedback/index.tsx b/web/app/components/base/copy-feedback/index.tsx index 6b2eee6871c..12b70b3c0e2 100644 --- a/web/app/components/base/copy-feedback/index.tsx +++ b/web/app/components/base/copy-feedback/index.tsx @@ -10,10 +10,10 @@ import ActionButton from '@/app/components/base/action-button' import { useClipboard } from '@/hooks/use-clipboard' import copyStyle from './style.module.css' -type Props = { +type Props = Readonly<{ content: string className?: string -} +}> const prefixEmbedded = 'overview.appInfo.embedded' diff --git a/web/app/components/base/copy-icon/index.tsx b/web/app/components/base/copy-icon/index.tsx index d110c813a07..6c6fa987a0e 100644 --- a/web/app/components/base/copy-icon/index.tsx +++ b/web/app/components/base/copy-icon/index.tsx @@ -4,9 +4,9 @@ import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useClipboard } from '@/hooks/use-clipboard' -type Props = { +type Props = Readonly<{ content: string -} +}> const prefixEmbedded = 'overview.appInfo.embedded' diff --git a/web/app/components/base/date-and-time-picker/time-picker/header.tsx b/web/app/components/base/date-and-time-picker/time-picker/header.tsx index 23e86a18c16..eb8820a4293 100644 --- a/web/app/components/base/date-and-time-picker/time-picker/header.tsx +++ b/web/app/components/base/date-and-time-picker/time-picker/header.tsx @@ -1,9 +1,9 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -type Props = { +type Props = Readonly<{ title?: string -} +}> const Header = ({ title, }: Props) => { diff --git a/web/app/components/base/encrypted-bottom/index.tsx b/web/app/components/base/encrypted-bottom/index.tsx index 2bbb339adf9..f15db4ad478 100644 --- a/web/app/components/base/encrypted-bottom/index.tsx +++ b/web/app/components/base/encrypted-bottom/index.tsx @@ -6,11 +6,11 @@ import Link from '@/next/link' type EncryptedKey = I18nKeysWithPrefix<'common', 'provider.encrypted.'> -type Props = { +type Props = Readonly<{ className?: string frontTextKey?: EncryptedKey backTextKey?: EncryptedKey -} +}> const DEFAULT_FRONT_KEY: EncryptedKey = 'provider.encrypted.front' const DEFAULT_BACK_KEY: EncryptedKey = 'provider.encrypted.back' diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx index 0bd27a5c135..b73b98a963e 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx @@ -10,7 +10,7 @@ import { useModalContext } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' import { addAnnotation } from '@/service/annotation' -type Props = { +type Props = Readonly<{ appId: string messageId?: string cached: boolean @@ -18,7 +18,7 @@ type Props = { answer: string onAdded: (annotationId: string, authorName: string) => void onEdit: () => void -} +}> const AnnotationCtrlButton: FC = ({ cached, query, answer, appId, messageId, onAdded, onEdit }) => { const { t } = useTranslation() const { plan, enableBilling } = useProviderContext() diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx index 0b23ae63ad6..b3d33c03aea 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx @@ -14,7 +14,7 @@ import { ANNOTATION_DEFAULT } from '@/config' import { Item } from './config-param' import ScoreSlider from './score-slider' -type Props = { +type Props = Readonly<{ appId: string isShow: boolean onHide: () => void @@ -24,7 +24,7 @@ type Props = { }, score: number) => void isInit?: boolean annotationConfig: AnnotationReplyConfig -} +}> const ConfigParamModal: FC = ({ isShow, onHide: doHide, onSave, isInit, annotationConfig: oldAnnotationConfig }) => { const { t } = useTranslation() const { modelList: embeddingsModelList, defaultModel: embeddingsDefaultModel, currentModel: isEmbeddingsDefaultModelValid } = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textEmbedding) diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/index.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/index.tsx index 0b1495221ce..e37e621c44a 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/index.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/index.tsx @@ -15,10 +15,10 @@ import AnnotationFullModal from '@/app/components/billing/annotation-full/modal' import { ANNOTATION_DEFAULT } from '@/config' import { usePathname, useRouter } from '@/next/navigation' -type Props = { +type Props = Readonly<{ disabled?: boolean onChange?: OnFeaturesChange -} +}> const AnnotationReply = ({ disabled, diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/index.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/index.tsx index d3b6340c898..b569d4573dd 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/index.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/index.tsx @@ -4,11 +4,11 @@ import { Slider } from '@langgenius/dify-ui/slider' import * as React from 'react' import { useTranslation } from 'react-i18next' -type Props = { +type Props = Readonly<{ className?: string value: number onChange: (value: number) => void -} +}> const clamp = (value: number, min: number, max: number) => { if (!Number.isFinite(value)) diff --git a/web/app/components/base/features/new-feature-panel/citation.tsx b/web/app/components/base/features/new-feature-panel/citation.tsx index 590a919b87e..8be737880b2 100644 --- a/web/app/components/base/features/new-feature-panel/citation.tsx +++ b/web/app/components/base/features/new-feature-panel/citation.tsx @@ -8,10 +8,10 @@ import FeatureCard from '@/app/components/base/features/new-feature-panel/featur import { FeatureEnum } from '@/app/components/base/features/types' import { Citations } from '@/app/components/base/icons/src/vender/features' -type Props = { +type Props = Readonly<{ disabled?: boolean onChange?: OnFeaturesChange -} +}> const Citation = ({ disabled, diff --git a/web/app/components/base/features/new-feature-panel/conversation-opener/index.tsx b/web/app/components/base/features/new-feature-panel/conversation-opener/index.tsx index 0149dd2377d..96683758809 100644 --- a/web/app/components/base/features/new-feature-panel/conversation-opener/index.tsx +++ b/web/app/components/base/features/new-feature-panel/conversation-opener/index.tsx @@ -13,13 +13,13 @@ import { FeatureEnum } from '@/app/components/base/features/types' import { LoveMessage } from '@/app/components/base/icons/src/vender/features' import { useModalContext } from '@/context/modal-context' -type Props = { +type Props = Readonly<{ disabled?: boolean onChange?: OnFeaturesChange promptVariables?: PromptVariable[] workflowVariables?: InputVar[] onAutoAddPromptVariable?: (variable: PromptVariable[]) => void -} +}> const ConversationOpener = ({ disabled, diff --git a/web/app/components/base/features/new-feature-panel/feature-bar.tsx b/web/app/components/base/features/new-feature-panel/feature-bar.tsx index f92782c2f7f..5593191cedc 100644 --- a/web/app/components/base/features/new-feature-panel/feature-bar.tsx +++ b/web/app/components/base/features/new-feature-panel/feature-bar.tsx @@ -9,13 +9,13 @@ import { useFeatures } from '@/app/components/base/features/hooks' import VoiceSettings from '@/app/components/base/features/new-feature-panel/text-to-speech/voice-settings' import { Citations, ContentModeration, FolderUpload, LoveMessage, MessageFast, Microphone01, TextToAudio, VirtualAssistant } from '@/app/components/base/icons/src/vender/features' -type Props = { +type Props = Readonly<{ isChatMode?: boolean showFileUpload?: boolean disabled?: boolean onFeatureBarClick?: (state: boolean) => void hideEditEntrance?: boolean -} +}> const FeatureBar = ({ isChatMode = true, diff --git a/web/app/components/base/features/new-feature-panel/feature-card.tsx b/web/app/components/base/features/new-feature-panel/feature-card.tsx index 9d3322ecf36..e9e88fd1e48 100644 --- a/web/app/components/base/features/new-feature-panel/feature-card.tsx +++ b/web/app/components/base/features/new-feature-panel/feature-card.tsx @@ -2,7 +2,7 @@ import { Switch } from '@langgenius/dify-ui/switch' import * as React from 'react' import { Infotip } from '@/app/components/base/infotip' -type Props = { +type Props = Readonly<{ icon: any title: any tooltip?: any @@ -13,7 +13,7 @@ type Props = { onChange?: (state: any) => void onMouseEnter?: () => void onMouseLeave?: () => void -} +}> const FeatureCard = ({ icon, diff --git a/web/app/components/base/features/new-feature-panel/file-upload/index.tsx b/web/app/components/base/features/new-feature-panel/file-upload/index.tsx index 2cea0e77453..d268a140798 100644 --- a/web/app/components/base/features/new-feature-panel/file-upload/index.tsx +++ b/web/app/components/base/features/new-feature-panel/file-upload/index.tsx @@ -11,10 +11,10 @@ import SettingModal from '@/app/components/base/features/new-feature-panel/file- import { FeatureEnum } from '@/app/components/base/features/types' import { FolderUpload } from '@/app/components/base/icons/src/vender/features' -type Props = { +type Props = Readonly<{ disabled: boolean onChange?: OnFeaturesChange -} +}> const FileUpload = ({ disabled, diff --git a/web/app/components/base/features/new-feature-panel/follow-up.tsx b/web/app/components/base/features/new-feature-panel/follow-up.tsx index c988b4756c6..033ce4fce4a 100644 --- a/web/app/components/base/features/new-feature-panel/follow-up.tsx +++ b/web/app/components/base/features/new-feature-panel/follow-up.tsx @@ -14,10 +14,10 @@ import FollowUpSettingModal from '@/app/components/base/features/new-feature-pan import { FeatureEnum } from '@/app/components/base/features/types' import { VirtualAssistant } from '@/app/components/base/icons/src/vender/features' -type Props = { +type Props = Readonly<{ disabled?: boolean onChange?: OnFeaturesChange -} +}> const FollowUp = ({ disabled, diff --git a/web/app/components/base/features/new-feature-panel/image-upload/index.tsx b/web/app/components/base/features/new-feature-panel/image-upload/index.tsx index 606f06d43d0..2177210fa8e 100644 --- a/web/app/components/base/features/new-feature-panel/image-upload/index.tsx +++ b/web/app/components/base/features/new-feature-panel/image-upload/index.tsx @@ -11,10 +11,10 @@ import FeatureCard from '@/app/components/base/features/new-feature-panel/featur import SettingModal from '@/app/components/base/features/new-feature-panel/file-upload/setting-modal' import { FeatureEnum } from '@/app/components/base/features/types' -type Props = { +type Props = Readonly<{ disabled: boolean onChange?: OnFeaturesChange -} +}> const FileUpload = ({ disabled, diff --git a/web/app/components/base/features/new-feature-panel/index.tsx b/web/app/components/base/features/new-feature-panel/index.tsx index 2d1e0063421..3c8f441521a 100644 --- a/web/app/components/base/features/new-feature-panel/index.tsx +++ b/web/app/components/base/features/new-feature-panel/index.tsx @@ -18,7 +18,7 @@ import TextToSpeech from '@/app/components/base/features/new-feature-panel/text- import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks' -type Props = { +type Props = Readonly<{ show: boolean isChatMode: boolean disabled: boolean @@ -29,7 +29,7 @@ type Props = { promptVariables?: PromptVariable[] workflowVariables?: InputVar[] onAutoAddPromptVariable?: (variable: PromptVariable[]) => void -} +}> const NewFeaturePanel = ({ show, diff --git a/web/app/components/base/features/new-feature-panel/moderation/index.tsx b/web/app/components/base/features/new-feature-panel/moderation/index.tsx index a6191e90502..43587d2800f 100644 --- a/web/app/components/base/features/new-feature-panel/moderation/index.tsx +++ b/web/app/components/base/features/new-feature-panel/moderation/index.tsx @@ -12,10 +12,10 @@ import { useLocale } from '@/context/i18n' import { useModalContext } from '@/context/modal-context' import { useCodeBasedExtensions } from '@/service/use-common' -type Props = { +type Props = Readonly<{ disabled?: boolean onChange?: OnFeaturesChange -} +}> const Moderation = ({ disabled, diff --git a/web/app/components/base/features/new-feature-panel/more-like-this.tsx b/web/app/components/base/features/new-feature-panel/more-like-this.tsx index d32bb0732dc..58568dbad1c 100644 --- a/web/app/components/base/features/new-feature-panel/more-like-this.tsx +++ b/web/app/components/base/features/new-feature-panel/more-like-this.tsx @@ -8,10 +8,10 @@ import { useFeatures, useFeaturesStore } from '@/app/components/base/features/ho import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card' import { FeatureEnum } from '@/app/components/base/features/types' -type Props = { +type Props = Readonly<{ disabled?: boolean onChange?: OnFeaturesChange -} +}> const MoreLikeThis = ({ disabled, diff --git a/web/app/components/base/features/new-feature-panel/speech-to-text.tsx b/web/app/components/base/features/new-feature-panel/speech-to-text.tsx index a147aeb9bc5..7f19c7c1062 100644 --- a/web/app/components/base/features/new-feature-panel/speech-to-text.tsx +++ b/web/app/components/base/features/new-feature-panel/speech-to-text.tsx @@ -8,10 +8,10 @@ import FeatureCard from '@/app/components/base/features/new-feature-panel/featur import { FeatureEnum } from '@/app/components/base/features/types' import { Microphone01 } from '@/app/components/base/icons/src/vender/features' -type Props = { +type Props = Readonly<{ disabled: boolean onChange?: OnFeaturesChange -} +}> const SpeechToText = ({ disabled, diff --git a/web/app/components/base/features/new-feature-panel/text-to-speech/index.tsx b/web/app/components/base/features/new-feature-panel/text-to-speech/index.tsx index a3dc06c5a0d..eeee2a862ce 100644 --- a/web/app/components/base/features/new-feature-panel/text-to-speech/index.tsx +++ b/web/app/components/base/features/new-feature-panel/text-to-speech/index.tsx @@ -13,10 +13,10 @@ import { TextToAudio } from '@/app/components/base/icons/src/vender/features' import { languages } from '@/i18n-config/language' import { TtsAutoPlay } from '@/types/app' -type Props = { +type Props = Readonly<{ disabled: boolean onChange?: OnFeaturesChange -} +}> const TextToSpeech = ({ disabled, diff --git a/web/app/components/base/file-uploader/file-list-in-log.tsx b/web/app/components/base/file-uploader/file-list-in-log.tsx index d01a766eb5a..a140271bce5 100644 --- a/web/app/components/base/file-uploader/file-list-in-log.tsx +++ b/web/app/components/base/file-uploader/file-list-in-log.tsx @@ -13,7 +13,7 @@ import { getFileAppearanceType, } from './utils' -type Props = { +type Props = Readonly<{ fileList: { varName: string list: FileEntity[] @@ -21,7 +21,7 @@ type Props = { isExpanded?: boolean noBorder?: boolean noPadding?: boolean -} +}> const FileListInLog = ({ fileList, isExpanded = false, noBorder = false, noPadding = false }: Props) => { const { t } = useTranslation() diff --git a/web/app/components/base/image-gallery/index.tsx b/web/app/components/base/image-gallery/index.tsx index 3e9d0308311..6b9c7e89e1b 100644 --- a/web/app/components/base/image-gallery/index.tsx +++ b/web/app/components/base/image-gallery/index.tsx @@ -6,9 +6,9 @@ import { useState } from 'react' import ImagePreview from '@/app/components/base/image-uploader/image-preview' import s from './style.module.css' -type Props = { +type Props = Readonly<{ srcs: string[] -} +}> const getWidthStyle = (imgNum: number) => { if (imgNum === 1) { diff --git a/web/app/components/base/param-item/index.tsx b/web/app/components/base/param-item/index.tsx index bf4bfdd1068..8599a607232 100644 --- a/web/app/components/base/param-item/index.tsx +++ b/web/app/components/base/param-item/index.tsx @@ -13,7 +13,7 @@ import { Slider } from '@langgenius/dify-ui/slider' import { Switch } from '@langgenius/dify-ui/switch' import { Infotip } from '@/app/components/base/infotip' -type Props = { +type Props = Readonly<{ className?: string id: string name: string @@ -27,7 +27,7 @@ type Props = { onChange: (key: string, value: number) => void hasSwitch?: boolean onSwitchChange?: (key: string, enable: boolean) => void -} +}> const ParamItem: FC = ({ className, id, name, noTooltip, tip, step = 0.1, min = 0, max, value, enable, onChange, hasSwitch, onSwitchChange }) => { return ( diff --git a/web/app/components/base/param-item/score-threshold-item.tsx b/web/app/components/base/param-item/score-threshold-item.tsx index cbaf190b999..dbbf0a96edd 100644 --- a/web/app/components/base/param-item/score-threshold-item.tsx +++ b/web/app/components/base/param-item/score-threshold-item.tsx @@ -4,14 +4,14 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import ParamItem from '.' -type Props = { +type Props = Readonly<{ className?: string value?: number onChange: (key: string, value: number) => void enable: boolean hasSwitch?: boolean onSwitchChange?: (key: string, enable: boolean) => void -} +}> const VALUE_LIMIT = { default: 0.7, diff --git a/web/app/components/base/param-item/top-k-item.tsx b/web/app/components/base/param-item/top-k-item.tsx index 9e9b7323db8..0449eb34d7f 100644 --- a/web/app/components/base/param-item/top-k-item.tsx +++ b/web/app/components/base/param-item/top-k-item.tsx @@ -5,12 +5,12 @@ import { useTranslation } from 'react-i18next' import { env } from '@/env' import ParamItem from '.' -type Props = { +type Props = Readonly<{ className?: string value: number onChange: (key: string, value: number) => void enable: boolean -} +}> const maxTopK = env.NEXT_PUBLIC_TOP_K_MAX_VALUE const VALUE_LIMIT = { diff --git a/web/app/components/base/prompt-editor/plugins/error-message-block/component.tsx b/web/app/components/base/prompt-editor/plugins/error-message-block/component.tsx index fe862111b5e..810cbdad2a3 100644 --- a/web/app/components/base/prompt-editor/plugins/error-message-block/component.tsx +++ b/web/app/components/base/prompt-editor/plugins/error-message-block/component.tsx @@ -6,9 +6,9 @@ import { DELETE_ERROR_MESSAGE_COMMAND, ErrorMessageBlockNode } from '.' import { Variable02 } from '../../../icons/src/vender/solid/development' import { useSelectOrDelete } from '../../hooks' -type Props = { +type Props = Readonly<{ nodeKey: string -} +}> const ErrorMessageBlockComponent: FC = ({ nodeKey, diff --git a/web/app/components/base/prompt-editor/plugins/hitl-input-block/pre-populate.tsx b/web/app/components/base/prompt-editor/plugins/hitl-input-block/pre-populate.tsx index 209e5d4c7ef..ad00132aa5a 100644 --- a/web/app/components/base/prompt-editor/plugins/hitl-input-block/pre-populate.tsx +++ b/web/app/components/base/prompt-editor/plugins/hitl-input-block/pre-populate.tsx @@ -11,7 +11,7 @@ import { VarType } from '@/app/components/workflow/types' import TagLabel from './tag-label' import TypeSwitch from './type-switch' -type Props = { +type Props = Readonly<{ isVariable?: boolean onIsVariableChange?: (isVariable: boolean) => void nodeId: string @@ -19,7 +19,7 @@ type Props = { onValueSelectorChange?: (valueSelector: ValueSelector | string) => void value?: string onValueChange?: (value: string) => void -} +}> const i18nPrefix = 'nodes.humanInput.insertInputField' diff --git a/web/app/components/base/prompt-editor/plugins/hitl-input-block/tag-label.tsx b/web/app/components/base/prompt-editor/plugins/hitl-input-block/tag-label.tsx index 6516b7041d1..4e0786348a5 100644 --- a/web/app/components/base/prompt-editor/plugins/hitl-input-block/tag-label.tsx +++ b/web/app/components/base/prompt-editor/plugins/hitl-input-block/tag-label.tsx @@ -5,12 +5,12 @@ import { RiEditLine } from '@remixicon/react' import * as React from 'react' import { Variable02 } from '../../../icons/src/vender/solid/development' -type Props = { +type Props = Readonly<{ type: 'edit' | 'variable' children: string className?: string onClick?: () => void -} +}> const TagLabel: FC = ({ type, diff --git a/web/app/components/base/prompt-editor/plugins/hitl-input-block/type-switch.tsx b/web/app/components/base/prompt-editor/plugins/hitl-input-block/type-switch.tsx index f4e0f4304e0..49c4b6e4f71 100644 --- a/web/app/components/base/prompt-editor/plugins/hitl-input-block/type-switch.tsx +++ b/web/app/components/base/prompt-editor/plugins/hitl-input-block/type-switch.tsx @@ -5,11 +5,11 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { Variable02 } from '../../../icons/src/vender/solid/development' -type Props = { +type Props = Readonly<{ className?: string isVariable?: boolean onIsVariableChange?: (isVariable: boolean) => void -} +}> const TypeSwitch: FC = ({ className, diff --git a/web/app/components/base/prompt-editor/plugins/last-run-block/component.tsx b/web/app/components/base/prompt-editor/plugins/last-run-block/component.tsx index c0fa8364b70..93ac0a4d71e 100644 --- a/web/app/components/base/prompt-editor/plugins/last-run-block/component.tsx +++ b/web/app/components/base/prompt-editor/plugins/last-run-block/component.tsx @@ -6,9 +6,9 @@ import { DELETE_LAST_RUN_COMMAND, LastRunBlockNode } from '.' import { Variable02 } from '../../../icons/src/vender/solid/development' import { useSelectOrDelete } from '../../hooks' -type Props = { +type Props = Readonly<{ nodeKey: string -} +}> const LastRunBlockComponent: FC = ({ nodeKey, diff --git a/web/app/components/base/qrcode/index.tsx b/web/app/components/base/qrcode/index.tsx index 05958347f57..9e7b303c538 100644 --- a/web/app/components/base/qrcode/index.tsx +++ b/web/app/components/base/qrcode/index.tsx @@ -7,9 +7,9 @@ import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' import { downloadUrl } from '@/utils/download' -type Props = { +type Props = Readonly<{ content: string -} +}> const prefixEmbedded = 'overview.appInfo.qrcode.title' diff --git a/web/app/components/base/sort/index.tsx b/web/app/components/base/sort/index.tsx index 2f835616bc7..67091379fd4 100644 --- a/web/app/components/base/sort/index.tsx +++ b/web/app/components/base/sort/index.tsx @@ -15,12 +15,12 @@ type Item = { name: string } & Record -type Props = { +type Props = Readonly<{ order?: string value: number | string items: Item[] onSelect: (value: string) => void -} +}> function Sort({ order, diff --git a/web/app/components/base/spinner/index.tsx b/web/app/components/base/spinner/index.tsx index f457d8d07cd..76cc53eb798 100644 --- a/web/app/components/base/spinner/index.tsx +++ b/web/app/components/base/spinner/index.tsx @@ -1,11 +1,11 @@ import type { FC } from 'react' import * as React from 'react' -type Props = { +type Props = Readonly<{ loading?: boolean className?: string children?: React.ReactNode | string -} +}> const Spinner: FC = ({ loading = false, children, className }) => { return ( diff --git a/web/app/components/base/video-gallery/index.tsx b/web/app/components/base/video-gallery/index.tsx index 31390989b69..f704af10ca6 100644 --- a/web/app/components/base/video-gallery/index.tsx +++ b/web/app/components/base/video-gallery/index.tsx @@ -1,9 +1,9 @@ import * as React from 'react' import VideoPlayer from './VideoPlayer' -type Props = { +type Props = Readonly<{ srcs: string[] -} +}> const VideoGallery: React.FC = ({ srcs }) => { const validSrcs = srcs.filter(src => src) From e07c50c83f03149b74b1bbc79d9552945fa5b982 Mon Sep 17 00:00:00 2001 From: Rohit Gahlawat <283466839+Rohit-Gahlawat@users.noreply.github.com> Date: Thu, 11 Jun 2026 05:48:00 +0530 Subject: [PATCH 009/122] refactor(web): mark Props of plugins/ components as read-only (#25219) (#37303) --- web/app/components/plugins/base/key-value-item.tsx | 4 ++-- web/app/components/plugins/card/base/description.tsx | 4 ++-- web/app/components/plugins/card/base/download-count.tsx | 4 ++-- web/app/components/plugins/card/base/org-info.tsx | 4 ++-- web/app/components/plugins/card/base/placeholder.tsx | 4 ++-- web/app/components/plugins/card/card-more-info.tsx | 4 ++-- web/app/components/plugins/card/index.tsx | 4 ++-- web/app/components/plugins/install-plugin/base/installed.tsx | 4 ++-- .../plugins/install-plugin/hooks/use-check-installed.tsx | 4 ++-- .../plugins/install-plugin/install-bundle/index.tsx | 4 ++-- .../install-plugin/install-bundle/item/github-item.tsx | 4 ++-- .../install-plugin/install-bundle/item/loaded-item.tsx | 4 ++-- .../install-plugin/install-bundle/item/marketplace-item.tsx | 4 ++-- .../install-plugin/install-bundle/item/package-item.tsx | 4 ++-- .../install-plugin/install-bundle/ready-to-install.tsx | 4 ++-- .../install-plugin/install-bundle/steps/install-multi.tsx | 4 ++-- .../plugins/install-plugin/install-bundle/steps/install.tsx | 4 ++-- .../plugins/install-plugin/install-bundle/steps/installed.tsx | 4 ++-- .../install-from-local-package/ready-to-install.tsx | 4 ++-- .../install-from-local-package/steps/install.tsx | 4 ++-- .../install-from-local-package/steps/uploading.tsx | 4 ++-- .../install-plugin/install-from-marketplace/steps/install.tsx | 4 ++-- web/app/components/plugins/marketplace/empty/index.tsx | 4 ++-- .../components/plugins/plugin-detail-panel/action-list.tsx | 4 ++-- .../plugins/plugin-detail-panel/agent-strategy-list.tsx | 4 ++-- .../plugin-detail-panel/app-selector/app-inputs-form.tsx | 4 ++-- .../plugin-detail-panel/app-selector/app-inputs-panel.tsx | 4 ++-- .../plugins/plugin-detail-panel/datasource-action-list.tsx | 4 ++-- .../plugins/plugin-detail-panel/detail-header/index.tsx | 4 ++-- .../components/plugins/plugin-detail-panel/endpoint-card.tsx | 4 ++-- .../components/plugins/plugin-detail-panel/endpoint-list.tsx | 4 ++-- .../components/plugins/plugin-detail-panel/endpoint-modal.tsx | 4 ++-- web/app/components/plugins/plugin-detail-panel/index.tsx | 4 ++-- web/app/components/plugins/plugin-detail-panel/model-list.tsx | 4 ++-- .../plugin-detail-panel/model-selector/llm-params-panel.tsx | 4 ++-- .../plugin-detail-panel/model-selector/tts-params-panel.tsx | 4 ++-- .../plugin-detail-panel/multiple-tool-selector/index.tsx | 4 ++-- .../plugins/plugin-detail-panel/operation-dropdown.tsx | 4 ++-- .../plugins/plugin-detail-panel/strategy-detail.tsx | 4 ++-- .../components/plugins/plugin-detail-panel/strategy-item.tsx | 4 ++-- .../subscription-list/create/common-modal.tsx | 4 ++-- .../plugin-detail-panel/subscription-list/create/index.tsx | 4 ++-- .../subscription-list/create/oauth-client.tsx | 4 ++-- .../plugin-detail-panel/subscription-list/delete-confirm.tsx | 4 ++-- .../subscription-list/edit/apikey-edit-modal.tsx | 4 ++-- .../plugin-detail-panel/subscription-list/edit/index.tsx | 4 ++-- .../subscription-list/edit/manual-edit-modal.tsx | 4 ++-- .../subscription-list/edit/oauth-edit-modal.tsx | 4 ++-- .../plugin-detail-panel/subscription-list/log-viewer.tsx | 4 ++-- .../subscription-list/subscription-card.tsx | 4 ++-- .../tool-selector/components/reasoning-config-form.tsx | 4 ++-- .../tool-selector/components/schema-modal.tsx | 4 ++-- .../tool-selector/components/tool-credentials-form.tsx | 4 ++-- .../tool-selector/components/tool-item.tsx | 4 ++-- .../tool-selector/components/tool-trigger.tsx | 4 ++-- .../plugins/plugin-detail-panel/tool-selector/index.tsx | 4 ++-- web/app/components/plugins/plugin-item/action.tsx | 4 ++-- web/app/components/plugins/plugin-item/index.tsx | 4 ++-- web/app/components/plugins/plugin-mutation-model/index.tsx | 4 ++-- .../plugins/plugin-page/install-plugin-dropdown.tsx | 4 ++-- web/app/components/plugins/plugin-page/plugin-info.tsx | 4 ++-- web/app/components/plugins/provider-card.tsx | 4 ++-- .../reference-setting-modal/auto-update-setting/index.tsx | 4 ++-- .../auto-update-setting/no-data-placeholder.tsx | 4 ++-- .../auto-update-setting/no-plugin-selected.tsx | 4 ++-- .../auto-update-setting/plugins-picker.tsx | 4 ++-- .../auto-update-setting/plugins-selected.tsx | 4 ++-- .../auto-update-setting/strategy-picker.tsx | 4 ++-- .../reference-setting-modal/auto-update-setting/tool-item.tsx | 4 ++-- .../auto-update-setting/tool-picker.tsx | 4 ++-- web/app/components/plugins/reference-setting-modal/index.tsx | 4 ++-- web/app/components/plugins/reference-setting-modal/label.tsx | 4 ++-- .../components/plugins/update-plugin/downgrade-warning.tsx | 4 ++-- web/app/components/plugins/update-plugin/from-github.tsx | 4 ++-- .../components/plugins/update-plugin/from-market-place.tsx | 4 ++-- .../plugins/update-plugin/plugin-version-picker.tsx | 4 ++-- 76 files changed, 152 insertions(+), 152 deletions(-) diff --git a/web/app/components/plugins/base/key-value-item.tsx b/web/app/components/plugins/base/key-value-item.tsx index 822eefdcdc5..71ecf0e746e 100644 --- a/web/app/components/plugins/base/key-value-item.tsx +++ b/web/app/components/plugins/base/key-value-item.tsx @@ -9,13 +9,13 @@ import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' import { CopyCheck } from '../../base/icons/src/vender/line/files' -type Props = { +type Props = Readonly<{ label: string labelWidthClassName?: string value: string maskedValue?: string valueMaxWidthClassName?: string -} +}> const KeyValueItem: FC = ({ label, diff --git a/web/app/components/plugins/card/base/description.tsx b/web/app/components/plugins/card/base/description.tsx index ff43f491dd9..171b0e02735 100644 --- a/web/app/components/plugins/card/base/description.tsx +++ b/web/app/components/plugins/card/base/description.tsx @@ -3,11 +3,11 @@ import { cn } from '@langgenius/dify-ui/cn' import * as React from 'react' import { useMemo } from 'react' -type Props = { +type Props = Readonly<{ className?: string text: string descriptionLineRows: number -} +}> const Description: FC = ({ className, diff --git a/web/app/components/plugins/card/base/download-count.tsx b/web/app/components/plugins/card/base/download-count.tsx index db822158a7d..916d59802b3 100644 --- a/web/app/components/plugins/card/base/download-count.tsx +++ b/web/app/components/plugins/card/base/download-count.tsx @@ -2,9 +2,9 @@ import { RiInstallLine } from '@remixicon/react' import * as React from 'react' import { formatNumber } from '@/utils/format' -type Props = { +type Props = Readonly<{ downloadCount: number -} +}> const DownloadCountComponent = ({ downloadCount, diff --git a/web/app/components/plugins/card/base/org-info.tsx b/web/app/components/plugins/card/base/org-info.tsx index ac1b670204a..1112ea044e1 100644 --- a/web/app/components/plugins/card/base/org-info.tsx +++ b/web/app/components/plugins/card/base/org-info.tsx @@ -1,11 +1,11 @@ import { cn } from '@langgenius/dify-ui/cn' -type Props = { +type Props = Readonly<{ className?: string orgName?: string packageName: string packageNameClassName?: string -} +}> const OrgInfo = ({ className, diff --git a/web/app/components/plugins/card/base/placeholder.tsx b/web/app/components/plugins/card/base/placeholder.tsx index 6076a27ac3b..a686f3fd981 100644 --- a/web/app/components/plugins/card/base/placeholder.tsx +++ b/web/app/components/plugins/card/base/placeholder.tsx @@ -3,10 +3,10 @@ import { SkeletonContainer, SkeletonPoint, SkeletonRectangle, SkeletonRow } from import { Group } from '../../../base/icons/src/vender/other' import Title from './title' -type Props = { +type Props = Readonly<{ wrapClassName: string loadingFileName?: string -} +}> export const LoadingPlaceholder = ({ className }: { className?: string }) => (
diff --git a/web/app/components/plugins/card/card-more-info.tsx b/web/app/components/plugins/card/card-more-info.tsx index 237e9232d22..9e6b3b13276 100644 --- a/web/app/components/plugins/card/card-more-info.tsx +++ b/web/app/components/plugins/card/card-more-info.tsx @@ -1,10 +1,10 @@ import * as React from 'react' import DownloadCount from './base/download-count' -type Props = { +type Props = Readonly<{ downloadCount?: number tags: string[] -} +}> const CardMoreInfoComponent = ({ downloadCount, diff --git a/web/app/components/plugins/card/index.tsx b/web/app/components/plugins/card/index.tsx index d0202f2b351..82a18f02b5f 100644 --- a/web/app/components/plugins/card/index.tsx +++ b/web/app/components/plugins/card/index.tsx @@ -22,7 +22,7 @@ import OrgInfo from './base/org-info' import Placeholder from './base/placeholder' import Title from './base/title' -type Props = { +type Props = Readonly<{ className?: string payload: Plugin titleLeft?: React.ReactNode @@ -34,7 +34,7 @@ type Props = { isLoading?: boolean loadingFileName?: string limitedInstall?: boolean -} +}> const Card = ({ className, diff --git a/web/app/components/plugins/install-plugin/base/installed.tsx b/web/app/components/plugins/install-plugin/base/installed.tsx index e8d500ffe37..5eae96c8825 100644 --- a/web/app/components/plugins/install-plugin/base/installed.tsx +++ b/web/app/components/plugins/install-plugin/base/installed.tsx @@ -8,13 +8,13 @@ import Badge, { BadgeState } from '@/app/components/base/badge/index' import Card from '../../card' import { pluginManifestInMarketToPluginProps, pluginManifestToCardPluginProps } from '../utils' -type Props = { +type Props = Readonly<{ payload?: Plugin | PluginDeclaration | PluginManifestInMarket | null isMarketPayload?: boolean isFailed: boolean errMsg?: string | null onCancel: () => void -} +}> const Installed: FC = ({ payload, diff --git a/web/app/components/plugins/install-plugin/hooks/use-check-installed.tsx b/web/app/components/plugins/install-plugin/hooks/use-check-installed.tsx index a90620dbc67..899bb5c7d78 100644 --- a/web/app/components/plugins/install-plugin/hooks/use-check-installed.tsx +++ b/web/app/components/plugins/install-plugin/hooks/use-check-installed.tsx @@ -3,10 +3,10 @@ import type { VersionInfo } from '../../types' import { useMemo } from 'react' import { useCheckInstalled as useDoCheckInstalled } from '@/service/use-plugins' -type Props = { +type Props = Readonly<{ pluginIds: string[] enabled: boolean -} +}> const useCheckInstalled = (props: Props) => { const { data, isLoading, error } = useDoCheckInstalled(props) diff --git a/web/app/components/plugins/install-plugin/install-bundle/index.tsx b/web/app/components/plugins/install-plugin/install-bundle/index.tsx index 7f07ee21502..6a7b7ab84fb 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/index.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/index.tsx @@ -18,12 +18,12 @@ export enum InstallType { fromDSL = 'fromDSL', } -type Props = { +type Props = Readonly<{ installType?: InstallType fromDSLPayload: Dependency[] // plugins?: PluginDeclaration[] onClose: () => void -} +}> const InstallBundle: FC = ({ installType = InstallType.fromMarketplace, diff --git a/web/app/components/plugins/install-plugin/install-bundle/item/github-item.tsx b/web/app/components/plugins/install-plugin/install-bundle/item/github-item.tsx index a25c2f59784..8dfde85722a 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/item/github-item.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/item/github-item.tsx @@ -9,14 +9,14 @@ import Loading from '../../base/loading' import { pluginManifestToCardPluginProps } from '../../utils' import LoadedItem from './loaded-item' -type Props = { +type Props = Readonly<{ checked: boolean onCheckedChange: (plugin: Plugin) => void dependency: GitHubItemAndMarketPlaceDependency versionInfo: VersionProps onFetchedPayload: (payload: Plugin) => void onFetchError: () => void -} +}> const Item: FC = ({ checked, diff --git a/web/app/components/plugins/install-plugin/install-bundle/item/loaded-item.tsx b/web/app/components/plugins/install-plugin/install-bundle/item/loaded-item.tsx index 9f1db59ba16..b9a60802e2c 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/item/loaded-item.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/item/loaded-item.tsx @@ -9,13 +9,13 @@ import useGetIcon from '../../base/use-get-icon' import Version from '../../base/version' import usePluginInstallLimit from '../../hooks/use-install-plugin-limit' -type Props = { +type Props = Readonly<{ checked: boolean onCheckedChange: (plugin: Plugin) => void payload: Plugin isFromMarketPlace?: boolean versionInfo: VersionProps -} +}> const LoadedItem: FC = ({ checked, diff --git a/web/app/components/plugins/install-plugin/install-bundle/item/marketplace-item.tsx b/web/app/components/plugins/install-plugin/install-bundle/item/marketplace-item.tsx index d02cb0f74f0..4fd596f6626 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/item/marketplace-item.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/item/marketplace-item.tsx @@ -6,13 +6,13 @@ import * as React from 'react' import Loading from '../../base/loading' import LoadedItem from './loaded-item' -type Props = { +type Props = Readonly<{ checked: boolean onCheckedChange: (plugin: Plugin) => void payload?: Plugin version: string versionInfo: VersionProps -} +}> const MarketPlaceItem: FC = ({ checked, diff --git a/web/app/components/plugins/install-plugin/install-bundle/item/package-item.tsx b/web/app/components/plugins/install-plugin/install-bundle/item/package-item.tsx index 63880bde5f0..88a6da207a1 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/item/package-item.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/item/package-item.tsx @@ -7,13 +7,13 @@ import LoadingError from '../../base/loading-error' import { pluginManifestToCardPluginProps } from '../../utils' import LoadedItem from './loaded-item' -type Props = { +type Props = Readonly<{ checked: boolean onCheckedChange: (plugin: Plugin) => void payload: PackageDependency isFromMarketPlace?: boolean versionInfo: VersionProps -} +}> const PackageItem: FC = ({ payload, diff --git a/web/app/components/plugins/install-plugin/install-bundle/ready-to-install.tsx b/web/app/components/plugins/install-plugin/install-bundle/ready-to-install.tsx index 2310063367f..689d3feaaf8 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/ready-to-install.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/ready-to-install.tsx @@ -7,7 +7,7 @@ import { InstallStep } from '../../types' import Install from './steps/install' import Installed from './steps/installed' -type Props = { +type Props = Readonly<{ step: InstallStep onStepChange: (step: InstallStep) => void onStartToInstall: () => void @@ -15,7 +15,7 @@ type Props = { allPlugins: Dependency[] onClose: () => void isFromMarketPlace?: boolean -} +}> const ReadyToInstall: FC = ({ step, diff --git a/web/app/components/plugins/install-plugin/install-bundle/steps/install-multi.tsx b/web/app/components/plugins/install-plugin/install-bundle/steps/install-multi.tsx index 49055f90a5c..6cac57a37ac 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/steps/install-multi.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/steps/install-multi.tsx @@ -8,7 +8,7 @@ import MarketplaceItem from '../item/marketplace-item' import PackageItem from '../item/package-item' import { getPluginKey, useInstallMultiState } from './hooks/use-install-multi-state' -type Props = { +type Props = Readonly<{ allPlugins: Dependency[] selectedPlugins: Plugin[] onSelect: (plugin: Plugin, selectedIndex: number, allCanInstallPluginsLength: number) => void @@ -17,7 +17,7 @@ type Props = { onLoadedAllPlugin: (installedInfo: Record) => void isFromMarketPlace?: boolean ref?: React.Ref -} +}> export type ExposeRefs = { selectAllPlugins: () => void diff --git a/web/app/components/plugins/install-plugin/install-bundle/steps/install.tsx b/web/app/components/plugins/install-plugin/install-bundle/steps/install.tsx index ab4c3c6a796..2932d929df5 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/steps/install.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/steps/install.tsx @@ -22,14 +22,14 @@ import InstallMulti from './install-multi' const i18nPrefix = 'installModal' -type Props = { +type Props = Readonly<{ allPlugins: Dependency[] onStartToInstall?: () => void onInstalled: (plugins: Plugin[], installStatus: InstallStatus[]) => void onCancel: () => void isFromMarketPlace?: boolean isHideButton?: boolean -} +}> const Install: FC = ({ allPlugins, diff --git a/web/app/components/plugins/install-plugin/install-bundle/steps/installed.tsx b/web/app/components/plugins/install-plugin/install-bundle/steps/installed.tsx index 3fbf0c13ec2..d99f284e90b 100644 --- a/web/app/components/plugins/install-plugin/install-bundle/steps/installed.tsx +++ b/web/app/components/plugins/install-plugin/install-bundle/steps/installed.tsx @@ -9,12 +9,12 @@ import Card from '@/app/components/plugins/card' import { MARKETPLACE_API_PREFIX } from '@/config' import useGetIcon from '../../base/use-get-icon' -type Props = { +type Props = Readonly<{ list: Plugin[] installStatus: InstallStatus[] onCancel: () => void isHideButton?: boolean -} +}> const Installed: FC = ({ list, diff --git a/web/app/components/plugins/install-plugin/install-from-local-package/ready-to-install.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/ready-to-install.tsx index b6f4e9d3ce4..cd0562c516f 100644 --- a/web/app/components/plugins/install-plugin/install-from-local-package/ready-to-install.tsx +++ b/web/app/components/plugins/install-plugin/install-from-local-package/ready-to-install.tsx @@ -8,7 +8,7 @@ import Installed from '../base/installed' import useRefreshPluginList from '../hooks/use-refresh-plugin-list' import Install from './steps/install' -type Props = { +type Props = Readonly<{ step: InstallStep onStepChange: (step: InstallStep) => void onStartToInstall: () => void @@ -18,7 +18,7 @@ type Props = { manifest: PluginDeclaration | null errorMsg: string | null onError: (errorMsg: string) => void -} +}> const ReadyToInstall: FC = ({ step, diff --git a/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx index 32f86efe717..1fbccbbb4b7 100644 --- a/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx +++ b/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx @@ -19,14 +19,14 @@ import { pluginManifestToCardPluginProps } from '../../utils' const i18nPrefix = 'installModal' -type Props = { +type Props = Readonly<{ uniqueIdentifier: string payload: PluginDeclaration onCancel: () => void onStartToInstall?: () => void onInstalled: (notRefresh?: boolean) => void onFailed: (message?: string) => void -} +}> const Installed: FC = ({ uniqueIdentifier, diff --git a/web/app/components/plugins/install-plugin/install-from-local-package/steps/uploading.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/steps/uploading.tsx index d5563806590..ffaf81f26d2 100644 --- a/web/app/components/plugins/install-plugin/install-from-local-package/steps/uploading.tsx +++ b/web/app/components/plugins/install-plugin/install-from-local-package/steps/uploading.tsx @@ -43,7 +43,7 @@ function getUploadFailureMessage(response: unknown): string | undefined { return (response as UploadFailureResponse).message } -type Props = { +type Props = Readonly<{ isBundle: boolean file: File onCancel: () => void @@ -53,7 +53,7 @@ type Props = { }) => void onBundleUploaded: (result: Dependency[]) => void onFailed: (errorMsg: string) => void -} +}> const Uploading: FC = ({ isBundle, diff --git a/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx b/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx index 1461d2f7541..152225a6255 100644 --- a/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx +++ b/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx @@ -20,14 +20,14 @@ import { pluginManifestInMarketToPluginProps } from '../../utils' const i18nPrefix = 'installModal' -type Props = { +type Props = Readonly<{ uniqueIdentifier: string payload: PluginManifestInMarket | Plugin onCancel: () => void onStartToInstall?: () => void onInstalled: (notRefresh?: boolean) => void onFailed: (message?: string) => void -} +}> const Installed: FC = ({ uniqueIdentifier, diff --git a/web/app/components/plugins/marketplace/empty/index.tsx b/web/app/components/plugins/marketplace/empty/index.tsx index 2ce95d06bd6..1625860f4eb 100644 --- a/web/app/components/plugins/marketplace/empty/index.tsx +++ b/web/app/components/plugins/marketplace/empty/index.tsx @@ -4,11 +4,11 @@ import { useTranslation } from '#i18n' import { Group } from '@/app/components/base/icons/src/vender/other' import Line from './line' -type Props = { +type Props = Readonly<{ text?: string lightCard?: boolean className?: string -} +}> const Empty = ({ text, diff --git a/web/app/components/plugins/plugin-detail-panel/action-list.tsx b/web/app/components/plugins/plugin-detail-panel/action-list.tsx index 40317f4b788..3cbffdb5438 100644 --- a/web/app/components/plugins/plugin-detail-panel/action-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/action-list.tsx @@ -8,9 +8,9 @@ import { useBuiltinTools, } from '@/service/use-tools' -type Props = { +type Props = Readonly<{ detail: PluginDetail -} +}> const ActionList = ({ detail, diff --git a/web/app/components/plugins/plugin-detail-panel/agent-strategy-list.tsx b/web/app/components/plugins/plugin-detail-panel/agent-strategy-list.tsx index e9aeedfa01f..c689d9a854b 100644 --- a/web/app/components/plugins/plugin-detail-panel/agent-strategy-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/agent-strategy-list.tsx @@ -7,9 +7,9 @@ import { useStrategyProviderDetail, } from '@/service/use-strategy' -type Props = { +type Props = Readonly<{ detail: PluginDetail -} +}> const AgentStrategyList = ({ detail, diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/app-inputs-form.tsx b/web/app/components/plugins/plugin-detail-panel/app-selector/app-inputs-form.tsx index 6d8bb3e4660..57b8f01dd13 100644 --- a/web/app/components/plugins/plugin-detail-panel/app-selector/app-inputs-form.tsx +++ b/web/app/components/plugins/plugin-detail-panel/app-selector/app-inputs-form.tsx @@ -6,12 +6,12 @@ import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uplo import Input from '@/app/components/base/input' import { InputVarType } from '@/app/components/workflow/types' -type Props = { +type Props = Readonly<{ inputsForms: any[] inputs: Record inputsRef: any onFormChange: (value: Record) => void -} +}> const AppInputsForm = ({ inputsForms, inputs, diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/app-inputs-panel.tsx b/web/app/components/plugins/plugin-detail-panel/app-selector/app-inputs-panel.tsx index b525cb9b2f1..78ff395ac36 100644 --- a/web/app/components/plugins/plugin-detail-panel/app-selector/app-inputs-panel.tsx +++ b/web/app/components/plugins/plugin-detail-panel/app-selector/app-inputs-panel.tsx @@ -7,14 +7,14 @@ import Loading from '@/app/components/base/loading' import AppInputsForm from '@/app/components/plugins/plugin-detail-panel/app-selector/app-inputs-form' import { useAppInputsFormSchema } from '@/app/components/plugins/plugin-detail-panel/app-selector/hooks/use-app-inputs-form-schema' -type Props = { +type Props = Readonly<{ value?: { app_id: string inputs: Record } appDetail: App onFormChange: (value: Record) => void -} +}> const AppInputsPanel = ({ value, diff --git a/web/app/components/plugins/plugin-detail-panel/datasource-action-list.tsx b/web/app/components/plugins/plugin-detail-panel/datasource-action-list.tsx index b8768b16a1d..e52ee795da0 100644 --- a/web/app/components/plugins/plugin-detail-panel/datasource-action-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/datasource-action-list.tsx @@ -10,9 +10,9 @@ import { useTranslation } from 'react-i18next' import { transformDataSourceToTool } from '@/app/components/workflow/block-selector/utils' import { useDataSourceList } from '@/service/use-pipeline' -type Props = { +type Props = Readonly<{ detail: PluginDetail -} +}> const ActionList = ({ detail, diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header/index.tsx b/web/app/components/plugins/plugin-detail-panel/detail-header/index.tsx index 2eb84b62aff..3fe0e66f583 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/detail-header/index.tsx @@ -31,12 +31,12 @@ import { PluginCategoryEnum, PluginSource } from '../../types' import { HeaderModals, PluginSourceBadge } from './components' import { useDetailHeaderState, usePluginOperations } from './hooks' -type Props = { +type Props = Readonly<{ detail: PluginDetail isReadmeView?: boolean onHide?: () => void onUpdate?: (isDelete?: boolean) => void -} +}> const getIconSrc = (icon: string | undefined, iconDark: string | undefined, theme: string, tenantId: string): string => { const iconFileName = theme === 'dark' && iconDark ? iconDark : icon diff --git a/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx b/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx index 8fff7c7da5f..7bf9504470d 100644 --- a/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx +++ b/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx @@ -31,11 +31,11 @@ import { NAME_FIELD } from './utils' type EndpointModalFormSchemas = ComponentProps['formSchemas'] -type Props = { +type Props = Readonly<{ pluginDetail: PluginDetail data: EndpointListItem handleChange: () => void -} +}> const EndpointCard = ({ pluginDetail, diff --git a/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx b/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx index 4dfdba58291..28f5d08dd44 100644 --- a/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx @@ -19,9 +19,9 @@ import EndpointCard from './endpoint-card' import EndpointModal from './endpoint-modal' import { NAME_FIELD } from './utils' -type Props = { +type Props = Readonly<{ detail: PluginDetail -} +}> const EndpointList = ({ detail }: Props) => { const { t } = useTranslation() const docLink = useDocLink() diff --git a/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx b/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx index 9bb6da25cbf..cc952a4653d 100644 --- a/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx @@ -21,13 +21,13 @@ import Form from '@/app/components/header/account-setting/model-provider-page/mo import { useRenderI18nObject } from '@/hooks/use-i18n' import { ReadmeEntrance } from '../readme-panel/entrance' -type Props = { +type Props = Readonly<{ formSchemas: FormSchema[] defaultValues?: any onCancel: () => void onSaved: (value: Record) => void pluginDetail: PluginDetail -} +}> const extractDefaultValues = (schemas: any[]) => { const result: Record = {} diff --git a/web/app/components/plugins/plugin-detail-panel/index.tsx b/web/app/components/plugins/plugin-detail-panel/index.tsx index 877b15c51e8..50f5e079251 100644 --- a/web/app/components/plugins/plugin-detail-panel/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/index.tsx @@ -23,11 +23,11 @@ import { usePluginStore } from './store' import { SubscriptionList } from './subscription-list' import { TriggerEventsList } from './trigger/event-list' -type Props = { +type Props = Readonly<{ detail?: PluginDetail onUpdate: () => void onHide: () => void -} +}> const PluginDetailPanel: FC = ({ detail, diff --git a/web/app/components/plugins/plugin-detail-panel/model-list.tsx b/web/app/components/plugins/plugin-detail-panel/model-list.tsx index 21c7a88ffb7..4b2ae80ac75 100644 --- a/web/app/components/plugins/plugin-detail-panel/model-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/model-list.tsx @@ -5,9 +5,9 @@ import ModelIcon from '@/app/components/header/account-setting/model-provider-pa import ModelName from '@/app/components/header/account-setting/model-provider-page/model-name' import { useModelProviderModelList } from '@/service/use-models' -type Props = { +type Props = Readonly<{ detail: PluginDetail -} +}> const ModelList = ({ detail, diff --git a/web/app/components/plugins/plugin-detail-panel/model-selector/llm-params-panel.tsx b/web/app/components/plugins/plugin-detail-panel/model-selector/llm-params-panel.tsx index 4d12efb9b56..a65a077d540 100644 --- a/web/app/components/plugins/plugin-detail-panel/model-selector/llm-params-panel.tsx +++ b/web/app/components/plugins/plugin-detail-panel/model-selector/llm-params-panel.tsx @@ -14,13 +14,13 @@ import { getSupportedPresetConfig } from '@/app/components/header/account-settin import { PROVIDER_WITH_PRESET_TONE, STOP_PARAMETER_RULE } from '@/config' import { useModelParameterRules } from '@/service/use-common' -type Props = { +type Props = Readonly<{ isAdvancedMode: boolean provider: string modelId: string completionParams: FormValue onCompletionParamsChange: (newParams: FormValue) => void -} +}> const LLMParamsPanel = ({ isAdvancedMode, diff --git a/web/app/components/plugins/plugin-detail-panel/model-selector/tts-params-panel.tsx b/web/app/components/plugins/plugin-detail-panel/model-selector/tts-params-panel.tsx index 7edf2f5d18c..c2895db6ee8 100644 --- a/web/app/components/plugins/plugin-detail-panel/model-selector/tts-params-panel.tsx +++ b/web/app/components/plugins/plugin-detail-panel/model-selector/tts-params-panel.tsx @@ -4,12 +4,12 @@ import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { languages } from '@/i18n-config/language' -type Props = { +type Props = Readonly<{ currentModel: any language: string voice: string onChange: (language: string, voice: string) => void -} +}> const supportedLanguages = languages.filter(item => item.supported) diff --git a/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx index aaf614f4be9..3f3889ea66d 100644 --- a/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx @@ -15,7 +15,7 @@ import ToolSelector from '@/app/components/plugins/plugin-detail-panel/tool-sele import { useMCPToolAvailability } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability' import { useAllMCPTools } from '@/service/use-tools' -type Props = { +type Props = Readonly<{ disabled?: boolean value: ToolValue[] label: string @@ -27,7 +27,7 @@ type Props = { nodeOutputVars: NodeOutPutVar[] availableNodes: Node[] nodeId?: string -} +}> const MultipleToolSelector = ({ disabled, diff --git a/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx b/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx index 43e953cac2e..a9c9615e19b 100644 --- a/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx +++ b/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx @@ -15,7 +15,7 @@ import { useTranslation } from 'react-i18next' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { PluginSource } from '../types' -type Props = { +type Props = Readonly<{ source: PluginSource onInfo: () => void onCheckVersion: () => void @@ -25,7 +25,7 @@ type Props = { sideOffset?: number alignOffset?: number popupClassName?: string -} +}> const OperationDropdown: FC = ({ source, diff --git a/web/app/components/plugins/plugin-detail-panel/strategy-detail.tsx b/web/app/components/plugins/plugin-detail-panel/strategy-detail.tsx index f8fd4c6fd43..49e292f0213 100644 --- a/web/app/components/plugins/plugin-detail-panel/strategy-detail.tsx +++ b/web/app/components/plugins/plugin-detail-panel/strategy-detail.tsx @@ -27,7 +27,7 @@ import Description from '@/app/components/plugins/card/base/description' import { API_PREFIX } from '@/config' import { useRenderI18nObject } from '@/hooks/use-i18n' -type Props = { +type Props = Readonly<{ provider: { author: string name: string @@ -39,7 +39,7 @@ type Props = { } detail: StrategyDetailType onHide: () => void -} +}> const StrategyDetail: FC = ({ provider, diff --git a/web/app/components/plugins/plugin-detail-panel/strategy-item.tsx b/web/app/components/plugins/plugin-detail-panel/strategy-item.tsx index b04250efc8f..d3908f9c0e1 100644 --- a/web/app/components/plugins/plugin-detail-panel/strategy-item.tsx +++ b/web/app/components/plugins/plugin-detail-panel/strategy-item.tsx @@ -9,7 +9,7 @@ import { useState } from 'react' import { useRenderI18nObject } from '@/hooks/use-i18n' import StrategyDetailPanel from './strategy-detail' -type Props = { +type Props = Readonly<{ provider: { author: string name: string @@ -20,7 +20,7 @@ type Props = { tags: string[] } detail: StrategyDetail -} +}> const StrategyItem = ({ provider, diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx index 82d42856aed..e08e57c886d 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx @@ -22,12 +22,12 @@ import { useCommonModalState, } from './hooks/use-common-modal-state' -type Props = { +type Props = Readonly<{ open?: boolean onClose: () => void createType: SupportedCreationMethods builder?: TriggerSubscriptionBuilder -} +}> export const CommonCreateModal = ({ open = true, onClose, createType, builder }: Props) => { return ( diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx index 8b4798e9026..cbfc68e4b14 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx @@ -19,11 +19,11 @@ import { CommonCreateModal } from './common-modal' import { OAuthClientSettingsModal } from './oauth-client' import { CreateButtonType, DEFAULT_METHOD } from './types' -type Props = { +type Props = Readonly<{ className?: string buttonType?: CreateButtonType shape?: 'square' | 'circle' -} +}> const MAX_COUNT = 10 diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx index 80e2137b7e2..6ddc088d7d9 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx @@ -10,12 +10,12 @@ import OptionCard from '@/app/components/workflow/nodes/_base/components/option- import { usePluginStore } from '../../store' import { ClientTypeEnum, useOAuthClientState as useOAuthClientSettings } from './hooks/use-oauth-client-state' -type Props = { +type Props = Readonly<{ open: boolean oauthConfig?: TriggerOAuthConfig onOpenChange: (open: boolean) => void showOAuthCreateModal: (builder: TriggerSubscriptionBuilder) => void -} +}> const CLIENT_TYPE_OPTIONS = [ClientTypeEnum.Default, ClientTypeEnum.Custom] as const diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/delete-confirm.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/delete-confirm.tsx index 3599dc6260c..35acd53d654 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/delete-confirm.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/delete-confirm.tsx @@ -14,13 +14,13 @@ import Input from '@/app/components/base/input' import { useDeleteTriggerSubscription } from '@/service/use-triggers' import { useSubscriptionList } from './use-subscription-list' -type Props = { +type Props = Readonly<{ onClose: (deleted: boolean) => void isShow: boolean currentId: string currentName: string workflowsInUse: number -} +}> const tPrefix = 'subscription.list.item.actions.deleteConfirm' diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx index 79d1760b397..e8baf02280c 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx @@ -17,11 +17,11 @@ import { parsePluginErrorMessage } from '@/utils/error-parser' import { usePluginStore } from '../../store' import { useSubscriptionList } from '../use-subscription-list' -type Props = { +type Props = Readonly<{ onClose: () => void subscription: TriggerSubscription pluginDetail?: PluginDetail -} +}> const EditStep = { EditCredentials: 'edit_credentials', diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/index.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/index.tsx index 90e89d043a9..199df8377bb 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/index.tsx @@ -6,11 +6,11 @@ import { ApiKeyEditModal } from './apikey-edit-modal' import { ManualEditModal } from './manual-edit-modal' import { OAuthEditModal } from './oauth-edit-modal' -type Props = { +type Props = Readonly<{ onClose: () => void subscription: TriggerSubscription pluginDetail?: PluginDetail -} +}> export const EditModal = ({ onClose, subscription, pluginDetail }: Props) => { const credentialType = subscription.credential_type diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx index c2783014638..0c5d96a5e22 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx @@ -15,11 +15,11 @@ import { useUpdateTriggerSubscription } from '@/service/use-triggers' import { usePluginStore } from '../../store' import { useSubscriptionList } from '../use-subscription-list' -type Props = { +type Props = Readonly<{ onClose: () => void subscription: TriggerSubscription pluginDetail?: PluginDetail -} +}> const normalizeFormType = (type: string): FormTypeEnum => { switch (type) { diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx index 46e0dab0918..1280d6a4752 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx @@ -15,11 +15,11 @@ import { useUpdateTriggerSubscription } from '@/service/use-triggers' import { usePluginStore } from '../../store' import { useSubscriptionList } from '../use-subscription-list' -type Props = { +type Props = Readonly<{ onClose: () => void subscription: TriggerSubscription pluginDetail?: PluginDetail -} +}> const normalizeFormType = (type: string): FormTypeEnum => { switch (type) { diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/log-viewer.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/log-viewer.tsx index f71b31be46e..2cf7c56080e 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/log-viewer.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/log-viewer.tsx @@ -16,10 +16,10 @@ import { useTranslation } from 'react-i18next' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' -type Props = { +type Props = Readonly<{ logs: TriggerLogEntity[] className?: string -} +}> enum LogTypeEnum { REQUEST = 'request', diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-card.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-card.tsx index 2fe3aae0342..d304f230813 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-card.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-card.tsx @@ -14,10 +14,10 @@ import ActionButton from '@/app/components/base/action-button' import { DeleteConfirm } from './delete-confirm' import { EditModal } from './edit' -type Props = { +type Props = Readonly<{ data: TriggerSubscription pluginDetail?: PluginDetail -} +}> const SubscriptionCard = ({ data, pluginDetail }: Props) => { const { t } = useTranslation() diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.tsx index 33081aa7df7..44c4802b8fa 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.tsx +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.tsx @@ -45,14 +45,14 @@ import SchemaModal from './schema-modal' export type ReasoningConfigValue = ReasoningConfigValueShape -type Props = { +type Props = Readonly<{ value: ReasoningConfigValue onChange: (val: ReasoningConfigValue) => void schemas: ToolFormSchema[] nodeOutputVars: NodeOutPutVar[] availableNodes: Node[] nodeId: string -} +}> const ReasoningConfigForm: React.FC = ({ value, diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/schema-modal.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/schema-modal.tsx index 7522eefbf35..357f74cf957 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/schema-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/schema-modal.tsx @@ -11,12 +11,12 @@ import { useTranslation } from 'react-i18next' import VisualEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor' import { MittProvider, VisualEditorContextProvider } from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context' -type Props = { +type Props = Readonly<{ isShow: boolean schema: SchemaRoot rootName: string onClose: () => void -} +}> const SchemaModal: FC = ({ isShow, diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-credentials-form.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-credentials-form.tsx index 758ee98fb1b..85f2b86569c 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-credentials-form.tsx +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-credentials-form.tsx @@ -17,11 +17,11 @@ import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/t import { useRenderI18nObject } from '@/hooks/use-i18n' import { fetchBuiltInToolCredential, fetchBuiltInToolCredentialSchema } from '@/service/tools' -type Props = { +type Props = Readonly<{ collection: Collection onCancel: () => void onSaved: (value: Record) => void -} +}> const ToolCredentialForm: FC = ({ collection, diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx index 06735865f70..5711326b104 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx @@ -20,7 +20,7 @@ import { useMCPToolAvailability } from '@/app/components/workflow/nodes/_base/co import McpToolNotSupportTooltip from '@/app/components/workflow/nodes/_base/components/mcp-tool-not-support-tooltip' import { SwitchPluginVersion } from '@/app/components/workflow/nodes/_base/components/switch-plugin-version' -type Props = { +type Props = Readonly<{ icon?: string | { content?: string, background?: string } providerName?: string isMCPTool?: boolean @@ -39,7 +39,7 @@ type Props = { versionMismatch?: boolean open: boolean authRemoved?: boolean -} +}> const ToolItem = ({ open, diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-trigger.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-trigger.tsx index 69d9dfee0b6..ff3c4b0aa67 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-trigger.tsx +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-trigger.tsx @@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next' import BlockIcon from '@/app/components/workflow/block-icon' import { BlockEnum } from '@/app/components/workflow/types' -type Props = { +type Props = Readonly<{ open: boolean provider?: ToolWithProvider value?: { @@ -18,7 +18,7 @@ type Props = { tool_name: string } isConfigure?: boolean -} +}> const ToolTrigger = ({ open, diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx index 40166ff9ec2..499c89a0353 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx @@ -24,7 +24,7 @@ import { } from './components' import { useToolSelectorState } from './hooks/use-tool-selector-state' -type Props = { +type Props = Readonly<{ disabled?: boolean placement?: Placement offset?: OffsetOptions @@ -45,7 +45,7 @@ type Props = { nodeOutputVars: NodeOutPutVar[] availableNodes: Node[] nodeId?: string -} +}> const ToolSelector: FC = ({ value, diff --git a/web/app/components/plugins/plugin-item/action.tsx b/web/app/components/plugins/plugin-item/action.tsx index fd54047807e..3c338b5760f 100644 --- a/web/app/components/plugins/plugin-item/action.tsx +++ b/web/app/components/plugins/plugin-item/action.tsx @@ -26,7 +26,7 @@ import { PluginSource } from '../types' const i18nPrefix = 'action' -type Props = { +type Props = Readonly<{ author: string installationId: string pluginUniqueIdentifier: string @@ -38,7 +38,7 @@ type Props = { isShowDelete: boolean onDelete: () => void meta?: MetaData -} +}> const Action: FC = ({ author, installationId, diff --git a/web/app/components/plugins/plugin-item/index.tsx b/web/app/components/plugins/plugin-item/index.tsx index b47c53570c4..ee5caeaa29e 100644 --- a/web/app/components/plugins/plugin-item/index.tsx +++ b/web/app/components/plugins/plugin-item/index.tsx @@ -34,10 +34,10 @@ import { usePluginPageContext } from '../plugin-page/context' import { PluginCategoryEnum, PluginSource } from '../types' import Action from './action' -type Props = { +type Props = Readonly<{ className?: string plugin: PluginDetail -} +}> const PluginItem: FC = ({ className, diff --git a/web/app/components/plugins/plugin-mutation-model/index.tsx b/web/app/components/plugins/plugin-mutation-model/index.tsx index c8f666326a8..a2a208233e4 100644 --- a/web/app/components/plugins/plugin-mutation-model/index.tsx +++ b/web/app/components/plugins/plugin-mutation-model/index.tsx @@ -7,7 +7,7 @@ import * as React from 'react' import { memo } from 'react' import Card from '@/app/components/plugins/card' -type Props = { +type Props = Readonly<{ plugin: Plugin onCancel: () => void mutation: Pick @@ -18,7 +18,7 @@ type Props = { description: ReactNode cardTitleLeft: ReactNode modalBottomLeft?: ReactNode -} +}> const PluginMutationModal: FC = ({ plugin, diff --git a/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx b/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx index 488ca91160c..d54564e0165 100644 --- a/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx +++ b/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx @@ -20,9 +20,9 @@ import InstallFromLocalPackage from '@/app/components/plugins/install-plugin/ins import { SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS } from '@/config' import { systemFeaturesQueryOptions } from '@/features/system-features/client' -type Props = { +type Props = Readonly<{ onSwitchToMarketplaceTab: () => void -} +}> type InstallMethod = { icon: React.FC<{ className?: string }> diff --git a/web/app/components/plugins/plugin-page/plugin-info.tsx b/web/app/components/plugins/plugin-page/plugin-info.tsx index 6c836731cf4..fb771ad83b1 100644 --- a/web/app/components/plugins/plugin-page/plugin-info.tsx +++ b/web/app/components/plugins/plugin-page/plugin-info.tsx @@ -7,12 +7,12 @@ import KeyValueItem from '../base/key-value-item' import { convertRepoToUrl } from '../install-plugin/utils' const i18nPrefix = 'pluginInfoModal' -type Props = { +type Props = Readonly<{ repository?: string release?: string packageName?: string onHide: () => void -} +}> const PlugInfo: FC = ({ repository, diff --git a/web/app/components/plugins/provider-card.tsx b/web/app/components/plugins/provider-card.tsx index 4b1ffb2a7e0..710a748a853 100644 --- a/web/app/components/plugins/provider-card.tsx +++ b/web/app/components/plugins/provider-card.tsx @@ -19,10 +19,10 @@ import Description from './card/base/description' import DownloadCount from './card/base/download-count' import Title from './card/base/title' -type Props = { +type Props = Readonly<{ className?: string payload: Plugin -} +}> const ProviderCardComponent: FC = ({ className, diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/index.tsx b/web/app/components/plugins/reference-setting-modal/auto-update-setting/index.tsx index bf22f26083d..6f8ee83b9f1 100644 --- a/web/app/components/plugins/reference-setting-modal/auto-update-setting/index.tsx +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/index.tsx @@ -22,10 +22,10 @@ import { convertLocalSecondsToUTCDaySeconds, convertUTCDaySecondsToLocalSeconds, const i18nPrefix = 'autoUpdate' -type Props = { +type Props = Readonly<{ payload: AutoUpdateConfig onChange: (payload: AutoUpdateConfig) => void -} +}> const SettingTimeZone: FC<{ children?: React.ReactNode diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/no-data-placeholder.tsx b/web/app/components/plugins/reference-setting-modal/auto-update-setting/no-data-placeholder.tsx index 5cec7d3730e..bf814e80341 100644 --- a/web/app/components/plugins/reference-setting-modal/auto-update-setting/no-data-placeholder.tsx +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/no-data-placeholder.tsx @@ -6,10 +6,10 @@ import { useTranslation } from 'react-i18next' import { SearchMenu } from '@/app/components/base/icons/src/vender/line/general' import { Group } from '@/app/components/base/icons/src/vender/other' -type Props = { +type Props = Readonly<{ className: string noPlugins?: boolean -} +}> const NoDataPlaceholder: FC = ({ className, diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/no-plugin-selected.tsx b/web/app/components/plugins/reference-setting-modal/auto-update-setting/no-plugin-selected.tsx index ccd0423fc3e..ec96e32f9cd 100644 --- a/web/app/components/plugins/reference-setting-modal/auto-update-setting/no-plugin-selected.tsx +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/no-plugin-selected.tsx @@ -4,9 +4,9 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { AUTO_UPDATE_MODE } from './types' -type Props = { +type Props = Readonly<{ updateMode: AUTO_UPDATE_MODE -} +}> const NoPluginSelected: FC = ({ updateMode, diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/plugins-picker.tsx b/web/app/components/plugins/reference-setting-modal/auto-update-setting/plugins-picker.tsx index ef25c355924..8494d28bf99 100644 --- a/web/app/components/plugins/reference-setting-modal/auto-update-setting/plugins-picker.tsx +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/plugins-picker.tsx @@ -12,11 +12,11 @@ import { AUTO_UPDATE_MODE } from './types' const i18nPrefix = 'autoUpdate' -type Props = { +type Props = Readonly<{ updateMode: AUTO_UPDATE_MODE value: string[] // plugin ids onChange: (value: string[]) => void -} +}> const PluginsPicker: FC = ({ updateMode, diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/plugins-selected.tsx b/web/app/components/plugins/reference-setting-modal/auto-update-setting/plugins-selected.tsx index cdb07b88744..fed97497e62 100644 --- a/web/app/components/plugins/reference-setting-modal/auto-update-setting/plugins-selected.tsx +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/plugins-selected.tsx @@ -6,10 +6,10 @@ import Icon from '@/app/components/plugins/card/base/card-icon' import { MARKETPLACE_API_PREFIX } from '@/config' const MAX_DISPLAY_COUNT = 14 -type Props = { +type Props = Readonly<{ className?: string plugins: string[] -} +}> const PluginsSelected: FC = ({ className, diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/strategy-picker.tsx b/web/app/components/plugins/reference-setting-modal/auto-update-setting/strategy-picker.tsx index 11f4aab9124..9818425ee1b 100644 --- a/web/app/components/plugins/reference-setting-modal/auto-update-setting/strategy-picker.tsx +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/strategy-picker.tsx @@ -13,10 +13,10 @@ import { AUTO_UPDATE_STRATEGY } from './types' const i18nPrefix = 'autoUpdate.strategy' -type Props = { +type Props = Readonly<{ value: AUTO_UPDATE_STRATEGY onChange: (value: AUTO_UPDATE_STRATEGY) => void -} +}> const StrategyPicker = ({ value, onChange, diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/tool-item.tsx b/web/app/components/plugins/reference-setting-modal/auto-update-setting/tool-item.tsx index b4dddcf8189..7b98ddb6667 100644 --- a/web/app/components/plugins/reference-setting-modal/auto-update-setting/tool-item.tsx +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/tool-item.tsx @@ -8,11 +8,11 @@ import { MARKETPLACE_API_PREFIX } from '@/config' import { useGetLanguage } from '@/context/i18n' import { renderI18nObject } from '@/i18n-config' -type Props = { +type Props = Readonly<{ payload: PluginDetail isChecked?: boolean onCheckChange: () => void -} +}> const ToolItem: FC = ({ payload, diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/tool-picker.tsx b/web/app/components/plugins/reference-setting-modal/auto-update-setting/tool-picker.tsx index f6ffb1618e3..8360910b48f 100644 --- a/web/app/components/plugins/reference-setting-modal/auto-update-setting/tool-picker.tsx +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/tool-picker.tsx @@ -18,13 +18,13 @@ import { PluginSource } from '../../types' import NoDataPlaceholder from './no-data-placeholder' import ToolItem from './tool-item' -type Props = { +type Props = Readonly<{ trigger: React.ReactNode value: string[] onChange: (value: string[]) => void isShow: boolean onShowChange: (isShow: boolean) => void -} +}> const ToolPicker: FC = ({ trigger, diff --git a/web/app/components/plugins/reference-setting-modal/index.tsx b/web/app/components/plugins/reference-setting-modal/index.tsx index 0ebe08e8f0b..d44572c4453 100644 --- a/web/app/components/plugins/reference-setting-modal/index.tsx +++ b/web/app/components/plugins/reference-setting-modal/index.tsx @@ -16,11 +16,11 @@ import { defaultValue as autoUpdateDefaultValue } from './auto-update-setting/co import Label from './label' const i18nPrefix = 'privilege' -type Props = { +type Props = Readonly<{ payload: ReferenceSetting onHide: () => void onSave: (payload: ReferenceSetting) => void -} +}> const PluginSettingModal: FC = ({ payload, diff --git a/web/app/components/plugins/reference-setting-modal/label.tsx b/web/app/components/plugins/reference-setting-modal/label.tsx index ad833cb30eb..fdc8965e2ec 100644 --- a/web/app/components/plugins/reference-setting-modal/label.tsx +++ b/web/app/components/plugins/reference-setting-modal/label.tsx @@ -3,10 +3,10 @@ import type { FC } from 'react' import { cn } from '@langgenius/dify-ui/cn' import * as React from 'react' -type Props = { +type Props = Readonly<{ label: string description?: string -} +}> const Label: FC = ({ label, diff --git a/web/app/components/plugins/update-plugin/downgrade-warning.tsx b/web/app/components/plugins/update-plugin/downgrade-warning.tsx index 801c8ede515..e16d0e35da0 100644 --- a/web/app/components/plugins/update-plugin/downgrade-warning.tsx +++ b/web/app/components/plugins/update-plugin/downgrade-warning.tsx @@ -3,11 +3,11 @@ import { useTranslation } from 'react-i18next' const i18nPrefix = 'autoUpdate.pluginDowngradeWarning' -type Props = { +type Props = Readonly<{ onCancel: () => void onJustDowngrade: () => void onExcludeAndDowngrade: () => void -} +}> const DowngradeWarningModal = ({ onCancel, onJustDowngrade, diff --git a/web/app/components/plugins/update-plugin/from-github.tsx b/web/app/components/plugins/update-plugin/from-github.tsx index c4bf1a8a0ab..0245b32fa29 100644 --- a/web/app/components/plugins/update-plugin/from-github.tsx +++ b/web/app/components/plugins/update-plugin/from-github.tsx @@ -4,11 +4,11 @@ import type { UpdateFromGitHubPayload } from '../types' import * as React from 'react' import InstallFromGitHub from '../install-plugin/install-from-github' -type Props = { +type Props = Readonly<{ payload: UpdateFromGitHubPayload onSave: () => void onCancel: () => void -} +}> const FromGitHub: FC = ({ payload, diff --git a/web/app/components/plugins/update-plugin/from-market-place.tsx b/web/app/components/plugins/update-plugin/from-market-place.tsx index 52484e65aa7..e312273d539 100644 --- a/web/app/components/plugins/update-plugin/from-market-place.tsx +++ b/web/app/components/plugins/update-plugin/from-market-place.tsx @@ -25,13 +25,13 @@ import DowngradeWarningModal from './downgrade-warning' const i18nPrefix = 'upgrade' -type Props = { +type Props = Readonly<{ payload: UpdateFromMarketPlacePayload pluginId?: string onSave: () => void onCancel: () => void isShowDowngradeWarningModal?: boolean -} +}> type FailedUpgradeResponse = { task?: { diff --git a/web/app/components/plugins/update-plugin/plugin-version-picker.tsx b/web/app/components/plugins/update-plugin/plugin-version-picker.tsx index 458947da6bb..f5b8c3f78e5 100644 --- a/web/app/components/plugins/update-plugin/plugin-version-picker.tsx +++ b/web/app/components/plugins/update-plugin/plugin-version-picker.tsx @@ -15,7 +15,7 @@ import useTimestamp from '@/hooks/use-timestamp' import { useVersionListOfPlugin } from '@/service/use-plugins' import { isEarlierThanVersion } from '@/utils/semver' -type Props = { +type Props = Readonly<{ disabled?: boolean isShow: boolean onShowChange: (isShow: boolean) => void @@ -34,7 +34,7 @@ type Props = { unique_identifier: string isDowngrade: boolean }) => void -} +}> const PluginVersionPicker: FC = ({ disabled = false, From 86ffa119ff9ef9c3e92cb3d027cb61cddc296d82 Mon Sep 17 00:00:00 2001 From: Rohit Gahlawat <283466839+Rohit-Gahlawat@users.noreply.github.com> Date: Thu, 11 Jun 2026 05:48:51 +0530 Subject: [PATCH 010/122] refactor(web): mark Props of workflow/ components as read-only (#25219) (#37304) --- .../workflow/block-selector/market-place-plugin/action.tsx | 4 ++-- .../workflow/block-selector/market-place-plugin/item.tsx | 4 ++-- web/app/components/workflow/block-selector/tool-picker.tsx | 4 ++-- .../components/workflow/block-selector/tool/action-item.tsx | 4 ++-- .../workflow/block-selector/tool/tool-list-flat-view/list.tsx | 4 ++-- .../workflow/block-selector/tool/tool-list-tree-view/item.tsx | 4 ++-- .../workflow/block-selector/tool/tool-list-tree-view/list.tsx | 4 ++-- web/app/components/workflow/block-selector/tool/tool.tsx | 4 ++-- .../workflow/block-selector/trigger-plugin/action-item.tsx | 4 ++-- .../workflow/block-selector/trigger-plugin/item.tsx | 4 ++-- .../components/workflow/block-selector/view-type-select.tsx | 4 ++-- web/app/components/workflow/candidate-node-main.tsx | 4 ++-- .../components/workflow/nodes/_base/components/add-button.tsx | 4 ++-- .../nodes/_base/components/before-run-form/bool-input.tsx | 4 ++-- .../nodes/_base/components/before-run-form/form-item.tsx | 4 ++-- .../workflow/nodes/_base/components/before-run-form/form.tsx | 4 ++-- .../nodes/_base/components/before-run-form/panel-wrap.tsx | 4 ++-- .../workflow/nodes/_base/components/code-generator-button.tsx | 4 ++-- .../workflow/nodes/_base/components/config-vision.tsx | 4 ++-- .../workflow/nodes/_base/components/editor/base.tsx | 4 ++-- .../components/editor/code-editor/editor-support-vars.tsx | 4 ++-- .../nodes/_base/components/editor/code-editor/index.tsx | 4 ++-- .../workflow/nodes/_base/components/editor/text-editor.tsx | 4 ++-- .../workflow/nodes/_base/components/editor/wrap.tsx | 4 ++-- web/app/components/workflow/nodes/_base/components/field.tsx | 4 ++-- .../workflow/nodes/_base/components/file-type-item.tsx | 4 ++-- .../workflow/nodes/_base/components/file-upload-setting.tsx | 4 ++-- .../workflow/nodes/_base/components/form-input-boolean.tsx | 4 ++-- .../workflow/nodes/_base/components/form-input-item.tsx | 4 ++-- .../nodes/_base/components/form-input-type-switch.tsx | 4 ++-- .../components/workflow/nodes/_base/components/info-panel.tsx | 4 ++-- .../nodes/_base/components/input-support-select-var.tsx | 4 ++-- .../workflow/nodes/_base/components/input-var-type-icon.tsx | 4 ++-- .../nodes/_base/components/list-no-data-placeholder.tsx | 4 ++-- .../workflow/nodes/_base/components/memory-config.tsx | 4 ++-- .../workflow/nodes/_base/components/option-card.tsx | 4 ++-- .../workflow/nodes/_base/components/output-vars.tsx | 4 ++-- .../workflow/nodes/_base/components/prompt/editor.tsx | 4 ++-- .../nodes/_base/components/readonly-input-with-select-var.tsx | 4 ++-- .../workflow/nodes/_base/components/remove-button.tsx | 4 ++-- .../nodes/_base/components/remove-effect-var-confirm.tsx | 4 ++-- .../components/workflow/nodes/_base/components/selector.tsx | 4 ++-- web/app/components/workflow/nodes/_base/components/split.tsx | 4 ++-- .../nodes/_base/components/support-var-input/index.tsx | 4 ++-- .../workflow/nodes/_base/components/toggle-expand-btn.tsx | 4 ++-- .../components/variable/assigned-var-reference-popup.tsx | 4 ++-- .../nodes/_base/components/variable/constant-field.tsx | 4 ++-- .../variable/object-child-tree-panel/picker/field.tsx | 4 ++-- .../variable/object-child-tree-panel/picker/index.tsx | 4 ++-- .../variable/object-child-tree-panel/show/field.tsx | 4 ++-- .../variable/object-child-tree-panel/show/index.tsx | 4 ++-- .../variable/object-child-tree-panel/tree-indent-line.tsx | 4 ++-- .../nodes/_base/components/variable/output-var-list.tsx | 4 ++-- .../nodes/_base/components/variable/var-full-path-panel.tsx | 4 ++-- .../workflow/nodes/_base/components/variable/var-list.tsx | 4 ++-- .../components/variable/var-reference-picker.trigger.tsx | 4 ++-- .../nodes/_base/components/variable/var-reference-picker.tsx | 4 ++-- .../nodes/_base/components/variable/var-reference-popup.tsx | 4 ++-- .../nodes/_base/components/variable/var-reference-vars.tsx | 4 ++-- .../nodes/_base/components/variable/var-type-picker.tsx | 4 ++-- .../nodes/_base/components/workflow-panel/last-run/index.tsx | 4 ++-- .../_base/components/workflow-panel/last-run/no-data.tsx | 4 ++-- .../workflow/nodes/assigner/components/var-list/index.tsx | 4 ++-- web/app/components/workflow/nodes/code/dependency-picker.tsx | 4 ++-- .../components/workflow/nodes/http/components/api-input.tsx | 4 ++-- .../workflow/nodes/http/components/authorization/index.tsx | 4 ++-- .../nodes/http/components/authorization/radio-group.tsx | 4 ++-- .../components/workflow/nodes/http/components/curl-panel.tsx | 4 ++-- .../workflow/nodes/http/components/edit-body/index.tsx | 4 ++-- .../nodes/http/components/key-value/bulk-edit/index.tsx | 4 ++-- .../workflow/nodes/http/components/key-value/index.tsx | 4 ++-- .../nodes/http/components/key-value/key-value-edit/index.tsx | 4 ++-- .../http/components/key-value/key-value-edit/input-item.tsx | 4 ++-- .../nodes/http/components/key-value/key-value-edit/item.tsx | 4 ++-- .../workflow/nodes/http/components/timeout/index.tsx | 4 ++-- .../workflow/nodes/human-input/components/add-input-field.tsx | 4 ++-- .../nodes/human-input/components/button-style-dropdown.tsx | 4 ++-- .../nodes/human-input/components/delivery-method/index.tsx | 4 ++-- .../components/delivery-method/recipient/email-input.tsx | 4 ++-- .../components/delivery-method/recipient/email-item.tsx | 4 ++-- .../components/delivery-method/recipient/index.tsx | 4 ++-- .../components/delivery-method/recipient/member-list.tsx | 4 ++-- .../components/delivery-method/recipient/member-selector.tsx | 4 ++-- .../workflow/nodes/human-input/components/single-run-form.tsx | 4 ++-- .../workflow/nodes/human-input/components/timeout.tsx | 4 ++-- .../workflow/nodes/if-else/components/condition-wrap.tsx | 4 ++-- .../nodes/knowledge-retrieval/components/add-dataset.tsx | 4 ++-- .../nodes/knowledge-retrieval/components/dataset-item.tsx | 4 ++-- .../nodes/knowledge-retrieval/components/dataset-list.tsx | 4 ++-- .../nodes/knowledge-retrieval/components/retrieval-config.tsx | 4 ++-- .../workflow/nodes/list-operator/components/extract-input.tsx | 4 ++-- .../nodes/list-operator/components/filter-condition.tsx | 4 ++-- .../workflow/nodes/list-operator/components/limit-config.tsx | 4 ++-- .../nodes/list-operator/components/sub-variable-picker.tsx | 4 ++-- .../workflow/nodes/llm/components/config-prompt-item.tsx | 4 ++-- .../workflow/nodes/llm/components/config-prompt.tsx | 4 ++-- .../workflow/nodes/llm/components/panel-memory-section.tsx | 4 ++-- .../workflow/nodes/llm/components/panel-output-section.tsx | 4 ++-- .../workflow/nodes/llm/components/prompt-generator-btn.tsx | 4 ++-- .../workflow/nodes/llm/components/resolution-picker.tsx | 4 ++-- .../workflow/nodes/llm/components/structure-output.tsx | 4 ++-- .../workflow/nodes/loop/components/condition-wrap.tsx | 4 ++-- .../components/extract-parameter/import-from-tool.tsx | 4 ++-- .../parameter-extractor/components/extract-parameter/item.tsx | 4 ++-- .../parameter-extractor/components/extract-parameter/list.tsx | 4 ++-- .../components/extract-parameter/update.tsx | 4 ++-- .../parameter-extractor/components/reasoning-mode-picker.tsx | 4 ++-- .../nodes/question-classifier/components/advanced-setting.tsx | 4 ++-- .../nodes/question-classifier/components/class-item.tsx | 4 ++-- .../nodes/question-classifier/components/class-list.tsx | 4 ++-- .../components/workflow/nodes/start/components/var-item.tsx | 4 ++-- .../components/workflow/nodes/start/components/var-list.tsx | 4 ++-- web/app/components/workflow/nodes/tool/components/copy-id.tsx | 4 ++-- .../workflow/nodes/tool/components/input-var-list.tsx | 4 ++-- .../workflow/nodes/tool/components/tool-form/index.tsx | 4 ++-- .../workflow/nodes/tool/components/tool-form/item.tsx | 4 ++-- .../nodes/trigger-plugin/components/trigger-form/index.tsx | 4 ++-- .../nodes/trigger-plugin/components/trigger-form/item.tsx | 4 ++-- .../nodes/variable-assigner/components/var-group-item.tsx | 4 ++-- .../nodes/variable-assigner/components/var-list/index.tsx | 4 ++-- .../panel/chat-variable-panel/components/array-bool-list.tsx | 4 ++-- .../panel/chat-variable-panel/components/array-value-list.tsx | 4 ++-- .../panel/chat-variable-panel/components/bool-value.tsx | 4 ++-- .../chat-variable-panel/components/object-value-item.tsx | 4 ++-- .../chat-variable-panel/components/object-value-list.tsx | 4 ++-- .../chat-variable-panel/components/variable-modal-trigger.tsx | 4 ++-- .../panel/debug-and-preview/conversation-variable-modal.tsx | 4 ++-- .../components/workflow/panel/env-panel/variable-trigger.tsx | 4 ++-- .../components/workflow/panel/global-variable-panel/item.tsx | 4 ++-- web/app/components/workflow/panel/inputs-panel.tsx | 4 ++-- .../workflow/workflow-generator/example-prompts.tsx | 4 ++-- .../workflow/workflow-generator/generation-phases.tsx | 4 ++-- 132 files changed, 264 insertions(+), 264 deletions(-) diff --git a/web/app/components/workflow/block-selector/market-place-plugin/action.tsx b/web/app/components/workflow/block-selector/market-place-plugin/action.tsx index fd0e87c7fc0..0b43a2b03ec 100644 --- a/web/app/components/workflow/block-selector/market-place-plugin/action.tsx +++ b/web/app/components/workflow/block-selector/market-place-plugin/action.tsx @@ -16,13 +16,13 @@ import { marketplaceQuery } from '@/service/client' import { downloadBlob } from '@/utils/download' import { getMarketplaceUrl } from '@/utils/var' -type Props = { +type Props = Readonly<{ open: boolean onOpenChange: (v: boolean) => void author: string name: string version: string -} +}> const OperationDropdown: FC = ({ open, diff --git a/web/app/components/workflow/block-selector/market-place-plugin/item.tsx b/web/app/components/workflow/block-selector/market-place-plugin/item.tsx index 84ef3c29c1e..ced39083151 100644 --- a/web/app/components/workflow/block-selector/market-place-plugin/item.tsx +++ b/web/app/components/workflow/block-selector/market-place-plugin/item.tsx @@ -16,10 +16,10 @@ enum ActionType { download = 'download', // viewDetail = 'viewDetail', // wait for marketplace api } -type Props = { +type Props = Readonly<{ payload: Plugin onAction: (type: ActionType) => void -} +}> const Item: FC = ({ payload, diff --git a/web/app/components/workflow/block-selector/tool-picker.tsx b/web/app/components/workflow/block-selector/tool-picker.tsx index dd8a6b6dd4b..571f6770058 100644 --- a/web/app/components/workflow/block-selector/tool-picker.tsx +++ b/web/app/components/workflow/block-selector/tool-picker.tsx @@ -36,7 +36,7 @@ import { useInvalidateAllWorkflowTools, } from '@/service/use-tools' -type Props = { +type Props = Readonly<{ panelClassName?: string disabled: boolean trigger: React.ReactNode @@ -49,7 +49,7 @@ type Props = { supportAddCustomTool?: boolean scope?: string selectedTools?: ToolValue[] -} +}> const ToolPicker: FC = ({ disabled, diff --git a/web/app/components/workflow/block-selector/tool/action-item.tsx b/web/app/components/workflow/block-selector/tool/action-item.tsx index 1d2ccf05bee..7265b65b300 100644 --- a/web/app/components/workflow/block-selector/tool/action-item.tsx +++ b/web/app/components/workflow/block-selector/tool/action-item.tsx @@ -24,14 +24,14 @@ const normalizeProviderIcon = (icon?: ToolWithProvider['icon']) => { return icon } -type Props = { +type Props = Readonly<{ provider: ToolWithProvider payload: Tool previewCardHandle: PreviewCardHandle disabled?: boolean isAdded?: boolean onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void -} +}> export type ToolActionPreviewPayload = { providerIcon: ToolWithProvider['icon'] diff --git a/web/app/components/workflow/block-selector/tool/tool-list-flat-view/list.tsx b/web/app/components/workflow/block-selector/tool/tool-list-flat-view/list.tsx index e2694d1f06f..bdd87717bdf 100644 --- a/web/app/components/workflow/block-selector/tool/tool-list-flat-view/list.tsx +++ b/web/app/components/workflow/block-selector/tool/tool-list-flat-view/list.tsx @@ -8,7 +8,7 @@ import { useMemo } from 'react' import { ViewType } from '../../view-type-select' import Tool from '../tool' -type Props = { +type Props = Readonly<{ payload: ToolWithProvider[] previewCardHandle: ToolActionPreviewCardHandle isShowLetterIndex: boolean @@ -20,7 +20,7 @@ type Props = { letters: string[] toolRefs: RefObject> selectedTools?: ToolValue[] -} +}> const ToolViewFlatView: FC = ({ letters, diff --git a/web/app/components/workflow/block-selector/tool/tool-list-tree-view/item.tsx b/web/app/components/workflow/block-selector/tool/tool-list-tree-view/item.tsx index aa4ce9abe2e..7e30b482c2c 100644 --- a/web/app/components/workflow/block-selector/tool/tool-list-tree-view/item.tsx +++ b/web/app/components/workflow/block-selector/tool/tool-list-tree-view/item.tsx @@ -7,7 +7,7 @@ import * as React from 'react' import { ViewType } from '../../view-type-select' import Tool from '../tool' -type Props = { +type Props = Readonly<{ groupName: string toolList: ToolWithProvider[] previewCardHandle: ToolActionPreviewCardHandle @@ -16,7 +16,7 @@ type Props = { canNotSelectMultiple?: boolean onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void selectedTools?: ToolValue[] -} +}> const Item: FC = ({ groupName, diff --git a/web/app/components/workflow/block-selector/tool/tool-list-tree-view/list.tsx b/web/app/components/workflow/block-selector/tool/tool-list-tree-view/list.tsx index 9afdf1e022d..52ec7b57234 100644 --- a/web/app/components/workflow/block-selector/tool/tool-list-tree-view/list.tsx +++ b/web/app/components/workflow/block-selector/tool/tool-list-tree-view/list.tsx @@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next' import { AGENT_GROUP_NAME, CUSTOM_GROUP_NAME, WORKFLOW_GROUP_NAME } from '../../index-bar' import Item from './item' -type Props = { +type Props = Readonly<{ payload: Record previewCardHandle: ToolActionPreviewCardHandle hasSearchText: boolean @@ -17,7 +17,7 @@ type Props = { canNotSelectMultiple?: boolean onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void selectedTools?: ToolValue[] -} +}> const ToolListTreeView: FC = ({ payload, diff --git a/web/app/components/workflow/block-selector/tool/tool.tsx b/web/app/components/workflow/block-selector/tool/tool.tsx index 080eb129f9f..a307b0672f7 100644 --- a/web/app/components/workflow/block-selector/tool/tool.tsx +++ b/web/app/components/workflow/block-selector/tool/tool.tsx @@ -31,7 +31,7 @@ const normalizeProviderIcon = (icon?: ToolWithProvider['icon']) => { return icon } -type Props = { +type Props = Readonly<{ className?: string payload: ToolWithProvider previewCardHandle: ToolActionPreviewCardHandle @@ -42,7 +42,7 @@ type Props = { onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void selectedTools?: ToolValue[] isShowLetterIndex?: boolean -} +}> const Tool: FC = ({ className, diff --git a/web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx b/web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx index 339ca93ee4f..b4a243b8079 100644 --- a/web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx +++ b/web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx @@ -10,14 +10,14 @@ import { useGetLanguage } from '@/context/i18n' import BlockIcon from '../../block-icon' import { BlockEnum } from '../../types' -type Props = { +type Props = Readonly<{ provider: TriggerWithProvider payload: Event previewCardHandle: TriggerPluginActionPreviewCardHandle disabled?: boolean isAdded?: boolean onSelect: (type: BlockEnum, trigger?: TriggerDefaultValue) => void -} +}> export type TriggerPluginActionPreviewPayload = { provider: TriggerWithProvider diff --git a/web/app/components/workflow/block-selector/trigger-plugin/item.tsx b/web/app/components/workflow/block-selector/trigger-plugin/item.tsx index f633e5e2572..5932bbb94ad 100644 --- a/web/app/components/workflow/block-selector/trigger-plugin/item.tsx +++ b/web/app/components/workflow/block-selector/trigger-plugin/item.tsx @@ -24,13 +24,13 @@ const normalizeProviderIcon = (icon?: TriggerWithProvider['icon']) => { return icon } -type Props = { +type Props = Readonly<{ className?: string payload: TriggerWithProvider hasSearchText: boolean previewCardHandle: TriggerPluginActionPreviewCardHandle onSelect: (type: BlockEnum, trigger?: TriggerDefaultValue) => void -} +}> const TriggerPluginItem: FC = ({ className, diff --git a/web/app/components/workflow/block-selector/view-type-select.tsx b/web/app/components/workflow/block-selector/view-type-select.tsx index 558da02d091..bf9bcb76474 100644 --- a/web/app/components/workflow/block-selector/view-type-select.tsx +++ b/web/app/components/workflow/block-selector/view-type-select.tsx @@ -10,10 +10,10 @@ export enum ViewType { tree = 'tree', } -type Props = { +type Props = Readonly<{ viewType: ViewType onChange: (viewType: ViewType) => void -} +}> const ViewTypeSelect: FC = ({ viewType, diff --git a/web/app/components/workflow/candidate-node-main.tsx b/web/app/components/workflow/candidate-node-main.tsx index a3a62345ff2..8161f4aefef 100644 --- a/web/app/components/workflow/candidate-node-main.tsx +++ b/web/app/components/workflow/candidate-node-main.tsx @@ -26,9 +26,9 @@ import { import { BlockEnum } from './types' import { getIterationStartNode, getLoopStartNode } from './utils' -type Props = { +type Props = Readonly<{ candidateNode: Node -} +}> const CandidateNodeMain: FC = ({ candidateNode, }) => { diff --git a/web/app/components/workflow/nodes/_base/components/add-button.tsx b/web/app/components/workflow/nodes/_base/components/add-button.tsx index 3a1ae22e1db..95d6b10d8b2 100644 --- a/web/app/components/workflow/nodes/_base/components/add-button.tsx +++ b/web/app/components/workflow/nodes/_base/components/add-button.tsx @@ -7,11 +7,11 @@ import { } from '@remixicon/react' import * as React from 'react' -type Props = { +type Props = Readonly<{ className?: string text: string onClick: () => void -} +}> const AddButton: FC = ({ className, diff --git a/web/app/components/workflow/nodes/_base/components/before-run-form/bool-input.tsx b/web/app/components/workflow/nodes/_base/components/before-run-form/bool-input.tsx index c88859f4abc..bee2d3fe13e 100644 --- a/web/app/components/workflow/nodes/_base/components/before-run-form/bool-input.tsx +++ b/web/app/components/workflow/nodes/_base/components/before-run-form/bool-input.tsx @@ -5,13 +5,13 @@ import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' -type Props = { +type Props = Readonly<{ name: string value: boolean required?: boolean onChange: (value: boolean) => void readonly?: boolean -} +}> const BoolInput: FC = ({ value, diff --git a/web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx b/web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx index 8fc82c79130..f24d6c62a07 100644 --- a/web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx +++ b/web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx @@ -28,14 +28,14 @@ import CodeEditor from '../editor/code-editor' import TextEditor from '../editor/text-editor' import BoolInput from './bool-input' -type Props = { +type Props = Readonly<{ payload: InputVar value: any onChange: (value: any) => void className?: string autoFocus?: boolean inStepRun?: boolean -} +}> const FormItem: FC = ({ payload, diff --git a/web/app/components/workflow/nodes/_base/components/before-run-form/form.tsx b/web/app/components/workflow/nodes/_base/components/before-run-form/form.tsx index 1aaf0e9c243..73def4bc49e 100644 --- a/web/app/components/workflow/nodes/_base/components/before-run-form/form.tsx +++ b/web/app/components/workflow/nodes/_base/components/before-run-form/form.tsx @@ -10,13 +10,13 @@ import { RETRIEVAL_OUTPUT_STRUCT } from '@/app/components/workflow/constants' import { InputVarType } from '@/app/components/workflow/types' import FormItem from './form-item' -export type Props = { +export type Props = Readonly<{ className?: string label?: string inputs: InputVar[] values: Record onChange: (newValues: Record) => void -} +}> const Form: FC = ({ className, diff --git a/web/app/components/workflow/nodes/_base/components/before-run-form/panel-wrap.tsx b/web/app/components/workflow/nodes/_base/components/before-run-form/panel-wrap.tsx index 69d4e172dd5..15ca1503b32 100644 --- a/web/app/components/workflow/nodes/_base/components/before-run-form/panel-wrap.tsx +++ b/web/app/components/workflow/nodes/_base/components/before-run-form/panel-wrap.tsx @@ -8,11 +8,11 @@ import { useTranslation } from 'react-i18next' const i18nPrefix = 'singleRun' -type Props = { +type Props = Readonly<{ nodeName: string onHide: () => void children: React.ReactNode -} +}> const PanelWrap: FC = ({ nodeName, diff --git a/web/app/components/workflow/nodes/_base/components/code-generator-button.tsx b/web/app/components/workflow/nodes/_base/components/code-generator-button.tsx index 8ab79d0b6d2..279d8d48626 100644 --- a/web/app/components/workflow/nodes/_base/components/code-generator-button.tsx +++ b/web/app/components/workflow/nodes/_base/components/code-generator-button.tsx @@ -12,13 +12,13 @@ import { Generator } from '@/app/components/base/icons/src/vender/other' import { AppModeEnum } from '@/types/app' import { useHooksStore } from '../../../hooks-store' -type Props = { +type Props = Readonly<{ nodeId: string currentCode?: string className?: string onGenerated?: (prompt: string) => void codeLanguages: CodeLanguage -} +}> const CodeGenerateBtn: FC = ({ nodeId, diff --git a/web/app/components/workflow/nodes/_base/components/config-vision.tsx b/web/app/components/workflow/nodes/_base/components/config-vision.tsx index 546cb1ac6bf..5b173834433 100644 --- a/web/app/components/workflow/nodes/_base/components/config-vision.tsx +++ b/web/app/components/workflow/nodes/_base/components/config-vision.tsx @@ -15,7 +15,7 @@ import VarReferencePicker from './variable/var-reference-picker' const i18nPrefix = 'nodes.llm' -type Props = { +type Props = Readonly<{ isVisionModel: boolean readOnly: boolean enabled: boolean @@ -23,7 +23,7 @@ type Props = { nodeId: string config?: VisionSetting onConfigChange: (config: VisionSetting) => void -} +}> const ConfigVision: FC = ({ isVisionModel, diff --git a/web/app/components/workflow/nodes/_base/components/editor/base.tsx b/web/app/components/workflow/nodes/_base/components/editor/base.tsx index 8d7f1cb94b0..ba73ccfab94 100644 --- a/web/app/components/workflow/nodes/_base/components/editor/base.tsx +++ b/web/app/components/workflow/nodes/_base/components/editor/base.tsx @@ -19,7 +19,7 @@ import CodeGeneratorButton from '../code-generator-button' import ToggleExpandBtn from '../toggle-expand-btn' import Wrap from './wrap' -type Props = { +type Props = Readonly<{ nodeId?: string className?: string title: React.JSX.Element | string @@ -41,7 +41,7 @@ type Props = { nodesOutputVars?: NodeOutPutVar[] availableNodes?: Node[] footer?: React.ReactNode -} +}> const Base: FC = ({ nodeId, diff --git a/web/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars.tsx b/web/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars.tsx index 4944b079de9..fe8138a2f7b 100644 --- a/web/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars.tsx +++ b/web/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars.tsx @@ -13,11 +13,11 @@ import Editor from '.' const TO_WINDOW_OFFSET = 8 -type Props = { +type Props = Readonly<{ availableVars: NodeOutPutVar[] varList: Variable[] onAddVar?: (payload: Variable) => void -} & EditorProps +}> & EditorProps const CodeEditor: FC = ({ availableVars, diff --git a/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx b/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx index 5649afaca49..5c825d6b2b4 100644 --- a/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx @@ -21,7 +21,7 @@ if (typeof window !== 'undefined') const CODE_EDITOR_LINE_HEIGHT = 18 -export type Props = { +export type Props = Readonly<{ nodeId?: string value?: string | object placeholder?: React.JSX.Element | string @@ -42,7 +42,7 @@ export type Props = { className?: string tip?: React.JSX.Element footer?: React.ReactNode -} +}> export const languageMap = { [CodeLanguage.javascript]: 'javascript', diff --git a/web/app/components/workflow/nodes/_base/components/editor/text-editor.tsx b/web/app/components/workflow/nodes/_base/components/editor/text-editor.tsx index 3de713c0672..aeee4b00012 100644 --- a/web/app/components/workflow/nodes/_base/components/editor/text-editor.tsx +++ b/web/app/components/workflow/nodes/_base/components/editor/text-editor.tsx @@ -5,7 +5,7 @@ import * as React from 'react' import { useCallback } from 'react' import Base from './base' -type Props = { +type Props = Readonly<{ value: string onChange: (value: string) => void title: React.JSX.Element | string @@ -15,7 +15,7 @@ type Props = { placeholder?: string readonly?: boolean isInNode?: boolean -} +}> const TextEditor: FC = ({ value, diff --git a/web/app/components/workflow/nodes/_base/components/editor/wrap.tsx b/web/app/components/workflow/nodes/_base/components/editor/wrap.tsx index 4ebedfe596c..94442bce6a4 100644 --- a/web/app/components/workflow/nodes/_base/components/editor/wrap.tsx +++ b/web/app/components/workflow/nodes/_base/components/editor/wrap.tsx @@ -3,13 +3,13 @@ import type { FC } from 'react' import * as React from 'react' import { useStore } from '@/app/components/workflow/store' -type Props = { +type Props = Readonly<{ isInNode?: boolean isExpand: boolean className: string style: React.CSSProperties children: React.ReactNode -} +}> // It doesn't has workflow store const WrapInWebApp = ({ diff --git a/web/app/components/workflow/nodes/_base/components/field.tsx b/web/app/components/workflow/nodes/_base/components/field.tsx index d17e05c258c..d4de8eb5557 100644 --- a/web/app/components/workflow/nodes/_base/components/field.tsx +++ b/web/app/components/workflow/nodes/_base/components/field.tsx @@ -8,7 +8,7 @@ import { useBoolean } from 'ahooks' import * as React from 'react' import { Infotip } from '@/app/components/base/infotip' -type Props = { +type Props = Readonly<{ className?: string title: ReactNode tooltip?: ReactNode @@ -19,7 +19,7 @@ type Props = { inline?: boolean required?: boolean warningDot?: boolean -} +}> const getTextFromNode = (node: ReactNode): string | undefined => { if (typeof node === 'string' || typeof node === 'number') diff --git a/web/app/components/workflow/nodes/_base/components/file-type-item.tsx b/web/app/components/workflow/nodes/_base/components/file-type-item.tsx index 861d525d9ad..cf86aee8d44 100644 --- a/web/app/components/workflow/nodes/_base/components/file-type-item.tsx +++ b/web/app/components/workflow/nodes/_base/components/file-type-item.tsx @@ -11,13 +11,13 @@ import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants' import TagInput from '@/app/components/base/tag-input' import { SupportUploadFileTypes } from '../../../types' -type Props = { +type Props = Readonly<{ type: SupportUploadFileTypes.image | SupportUploadFileTypes.document | SupportUploadFileTypes.audio | SupportUploadFileTypes.video | SupportUploadFileTypes.custom selected: boolean onToggle: (type: SupportUploadFileTypes) => void onCustomFileTypesChange?: (customFileTypes: string[]) => void customFileTypes?: string[] -} +}> const FileTypeItem: FC = ({ type, diff --git a/web/app/components/workflow/nodes/_base/components/file-upload-setting.tsx b/web/app/components/workflow/nodes/_base/components/file-upload-setting.tsx index df662dc3f82..3a9af995381 100644 --- a/web/app/components/workflow/nodes/_base/components/file-upload-setting.tsx +++ b/web/app/components/workflow/nodes/_base/components/file-upload-setting.tsx @@ -15,13 +15,13 @@ import FileTypeItem from './file-type-item' import InputNumberWithSlider from './input-number-with-slider' import OptionCard from './option-card' -type Props = { +type Props = Readonly<{ payload: UploadFileSetting isMultiple: boolean inFeaturePanel?: boolean hideSupportFileType?: boolean onChange: (payload: UploadFileSetting) => void -} +}> const FileUploadSetting: FC = ({ payload, diff --git a/web/app/components/workflow/nodes/_base/components/form-input-boolean.tsx b/web/app/components/workflow/nodes/_base/components/form-input-boolean.tsx index 6873d4f11df..05013ae6949 100644 --- a/web/app/components/workflow/nodes/_base/components/form-input-boolean.tsx +++ b/web/app/components/workflow/nodes/_base/components/form-input-boolean.tsx @@ -2,10 +2,10 @@ import type { FC } from 'react' import { cn } from '@langgenius/dify-ui/cn' -type Props = { +type Props = Readonly<{ value: boolean onChange: (value: boolean) => void -} +}> const FormInputBoolean: FC = ({ value, diff --git a/web/app/components/workflow/nodes/_base/components/form-input-item.tsx b/web/app/components/workflow/nodes/_base/components/form-input-item.tsx index ad4ff29ec0e..f2db48f727f 100644 --- a/web/app/components/workflow/nodes/_base/components/form-input-item.tsx +++ b/web/app/components/workflow/nodes/_base/components/form-input-item.tsx @@ -41,7 +41,7 @@ import { } from './form-input-item.sections' import FormInputTypeSwitch from './form-input-type-switch' -type Props = { +type Props = Readonly<{ readOnly: boolean nodeId: string schema: CredentialFormSchema @@ -55,7 +55,7 @@ type Props = { extraParams?: Record providerType?: string disableVariableInsertion?: boolean -} +}> type FormInputValue = string | number | boolean | string[] | Record | null | undefined diff --git a/web/app/components/workflow/nodes/_base/components/form-input-type-switch.tsx b/web/app/components/workflow/nodes/_base/components/form-input-type-switch.tsx index 6ef12a0eae8..705367f38b6 100644 --- a/web/app/components/workflow/nodes/_base/components/form-input-type-switch.tsx +++ b/web/app/components/workflow/nodes/_base/components/form-input-type-switch.tsx @@ -5,10 +5,10 @@ import { useTranslation } from 'react-i18next' import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' import { VarType } from '@/app/components/workflow/nodes/tool/types' -type Props = { +type Props = Readonly<{ value: VarType onChange: (value: VarType) => void -} +}> const FormInputTypeSwitch: FC = ({ value, diff --git a/web/app/components/workflow/nodes/_base/components/info-panel.tsx b/web/app/components/workflow/nodes/_base/components/info-panel.tsx index a7b2efb7ca2..bb125062e6f 100644 --- a/web/app/components/workflow/nodes/_base/components/info-panel.tsx +++ b/web/app/components/workflow/nodes/_base/components/info-panel.tsx @@ -2,10 +2,10 @@ import type { FC, ReactNode } from 'react' import * as React from 'react' -type Props = { +type Props = Readonly<{ title: string content: ReactNode -} +}> const InfoPanel: FC = ({ title, diff --git a/web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx b/web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx index c0d79138f54..950ce7d92f6 100644 --- a/web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx +++ b/web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx @@ -16,7 +16,7 @@ import PromptEditor from '@/app/components/base/prompt-editor' import { useStore } from '@/app/components/workflow/store' import { BlockEnum } from '@/app/components/workflow/types' -type Props = { +type Props = Readonly<{ instanceId?: string className?: string placeholder?: string @@ -30,7 +30,7 @@ type Props = { nodesOutputVars?: NodeOutPutVar[] availableNodes?: Node[] insertVarTipToLeft?: boolean -} +}> const Editor: FC = ({ instanceId, diff --git a/web/app/components/workflow/nodes/_base/components/input-var-type-icon.tsx b/web/app/components/workflow/nodes/_base/components/input-var-type-icon.tsx index 586149d727a..7d0cf9eaf22 100644 --- a/web/app/components/workflow/nodes/_base/components/input-var-type-icon.tsx +++ b/web/app/components/workflow/nodes/_base/components/input-var-type-icon.tsx @@ -13,10 +13,10 @@ import { import * as React from 'react' import { InputVarType } from '../../../types' -type Props = { +type Props = Readonly<{ className?: string type: InputVarType -} +}> const getIcon = (type: InputVarType) => { return ({ diff --git a/web/app/components/workflow/nodes/_base/components/list-no-data-placeholder.tsx b/web/app/components/workflow/nodes/_base/components/list-no-data-placeholder.tsx index cc801378b2c..e52659808ad 100644 --- a/web/app/components/workflow/nodes/_base/components/list-no-data-placeholder.tsx +++ b/web/app/components/workflow/nodes/_base/components/list-no-data-placeholder.tsx @@ -2,9 +2,9 @@ import type { FC } from 'react' import * as React from 'react' -type Props = { +type Props = Readonly<{ children: React.ReactNode -} +}> const ListNoDataPlaceholder: FC = ({ children, diff --git a/web/app/components/workflow/nodes/_base/components/memory-config.tsx b/web/app/components/workflow/nodes/_base/components/memory-config.tsx index 8d37e161667..27db3ac6028 100644 --- a/web/app/components/workflow/nodes/_base/components/memory-config.tsx +++ b/web/app/components/workflow/nodes/_base/components/memory-config.tsx @@ -46,14 +46,14 @@ const RoleItem: FC = ({ ) } -type Props = { +type Props = Readonly<{ className?: string readonly: boolean config: { data?: Memory } onChange: (memory?: Memory) => void canSetRoleName?: boolean defaultMemory?: Memory -} +}> const MEMORY_DEFAULT: Memory = { window: { enabled: false, size: WINDOW_SIZE_DEFAULT }, diff --git a/web/app/components/workflow/nodes/_base/components/option-card.tsx b/web/app/components/workflow/nodes/_base/components/option-card.tsx index e23ae222041..4bb860aba6d 100644 --- a/web/app/components/workflow/nodes/_base/components/option-card.tsx +++ b/web/app/components/workflow/nodes/_base/components/option-card.tsx @@ -20,7 +20,7 @@ const variants = cva([], { }, }) -type Props = { +type Props = Readonly<{ className?: string title: string onSelect: () => void @@ -28,7 +28,7 @@ type Props = { disabled?: boolean align?: 'left' | 'center' | 'right' tooltip?: string -} & VariantProps +}> & VariantProps const OptionCard: FC = ({ className, diff --git a/web/app/components/workflow/nodes/_base/components/output-vars.tsx b/web/app/components/workflow/nodes/_base/components/output-vars.tsx index 9eef21f9777..c5c5cd9a8db 100644 --- a/web/app/components/workflow/nodes/_base/components/output-vars.tsx +++ b/web/app/components/workflow/nodes/_base/components/output-vars.tsx @@ -6,14 +6,14 @@ import { useTranslation } from 'react-i18next' import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse' import TreeIndentLine from './variable/object-child-tree-panel/tree-indent-line' -type Props = { +type Props = Readonly<{ className?: string title?: string children: ReactNode operations?: ReactNode collapsed?: boolean onCollapse?: (collapsed: boolean) => void -} +}> const OutputVars: FC = ({ title, diff --git a/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx b/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx index 880765a9acd..7d136889898 100644 --- a/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx +++ b/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx @@ -39,7 +39,7 @@ import { CodeLanguage } from '../../../code/types' import PromptGeneratorBtn from '../../../llm/components/prompt-generator-btn' import Wrap from '../editor/wrap' -type Props = { +type Props = Readonly<{ className?: string headerClassName?: string instanceId?: string @@ -81,7 +81,7 @@ type Props = { placeholderClassName?: string titleClassName?: string required?: boolean -} +}> const Editor: FC = ({ className, diff --git a/web/app/components/workflow/nodes/_base/components/readonly-input-with-select-var.tsx b/web/app/components/workflow/nodes/_base/components/readonly-input-with-select-var.tsx index fb6e0e42119..9581f22fd4b 100644 --- a/web/app/components/workflow/nodes/_base/components/readonly-input-with-select-var.tsx +++ b/web/app/components/workflow/nodes/_base/components/readonly-input-with-select-var.tsx @@ -9,11 +9,11 @@ import { useWorkflow } from '../../../hooks' import { BlockEnum } from '../../../types' import { getNodeInfoById, isSystemVar } from './variable/utils' -type Props = { +type Props = Readonly<{ nodeId: string value: string className?: string -} +}> const VAR_PLACEHOLDER = '@#!@#!' diff --git a/web/app/components/workflow/nodes/_base/components/remove-button.tsx b/web/app/components/workflow/nodes/_base/components/remove-button.tsx index 03ca00fad49..0a3b756c2bc 100644 --- a/web/app/components/workflow/nodes/_base/components/remove-button.tsx +++ b/web/app/components/workflow/nodes/_base/components/remove-button.tsx @@ -4,10 +4,10 @@ import { RiDeleteBinLine } from '@remixicon/react' import * as React from 'react' import ActionButton from '@/app/components/base/action-button' -type Props = { +type Props = Readonly<{ className?: string onClick: (e: React.MouseEvent) => void -} +}> const Remove: FC = ({ onClick, diff --git a/web/app/components/workflow/nodes/_base/components/remove-effect-var-confirm.tsx b/web/app/components/workflow/nodes/_base/components/remove-effect-var-confirm.tsx index 6ae65da1ae6..2f1a0fed6dc 100644 --- a/web/app/components/workflow/nodes/_base/components/remove-effect-var-confirm.tsx +++ b/web/app/components/workflow/nodes/_base/components/remove-effect-var-confirm.tsx @@ -12,11 +12,11 @@ import { import * as React from 'react' import { useTranslation } from 'react-i18next' -type Props = { +type Props = Readonly<{ isShow: boolean onConfirm: () => void onCancel: () => void -} +}> const i18nPrefix = 'common.effectVarConfirm' const RemoveVarConfirm: FC = ({ diff --git a/web/app/components/workflow/nodes/_base/components/selector.tsx b/web/app/components/workflow/nodes/_base/components/selector.tsx index db4883c0512..03a7b1c2ae7 100644 --- a/web/app/components/workflow/nodes/_base/components/selector.tsx +++ b/web/app/components/workflow/nodes/_base/components/selector.tsx @@ -10,7 +10,7 @@ type Item = { value: string label: string } -type Props = { +type Props = Readonly<{ className?: string trigger?: React.JSX.Element DropDownIcon?: any @@ -26,7 +26,7 @@ type Props = { itemClassName?: string readonly?: boolean showChecked?: boolean -} +}> const TypeSelector: FC = ({ className, diff --git a/web/app/components/workflow/nodes/_base/components/split.tsx b/web/app/components/workflow/nodes/_base/components/split.tsx index 65c5e4ad69a..847e709e788 100644 --- a/web/app/components/workflow/nodes/_base/components/split.tsx +++ b/web/app/components/workflow/nodes/_base/components/split.tsx @@ -3,9 +3,9 @@ import type { FC } from 'react' import { cn } from '@langgenius/dify-ui/cn' import * as React from 'react' -type Props = { +type Props = Readonly<{ className?: string -} +}> const Split: FC = ({ className, diff --git a/web/app/components/workflow/nodes/_base/components/support-var-input/index.tsx b/web/app/components/workflow/nodes/_base/components/support-var-input/index.tsx index 1e80b3a3664..74c2516f46f 100644 --- a/web/app/components/workflow/nodes/_base/components/support-var-input/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/support-var-input/index.tsx @@ -4,7 +4,7 @@ import { cn } from '@langgenius/dify-ui/cn' import * as React from 'react' import VarHighlight from '@/app/components/app/configuration/base/var-highlight' -type Props = { +type Props = Readonly<{ isFocus?: boolean onFocus?: () => void value: string @@ -12,7 +12,7 @@ type Props = { wrapClassName?: string textClassName?: string readonly?: boolean -} +}> const SupportVarInput: FC = ({ isFocus, diff --git a/web/app/components/workflow/nodes/_base/components/toggle-expand-btn.tsx b/web/app/components/workflow/nodes/_base/components/toggle-expand-btn.tsx index 8429d2a411a..3a0bda6578e 100644 --- a/web/app/components/workflow/nodes/_base/components/toggle-expand-btn.tsx +++ b/web/app/components/workflow/nodes/_base/components/toggle-expand-btn.tsx @@ -8,10 +8,10 @@ import * as React from 'react' import { useCallback } from 'react' import ActionButton from '@/app/components/base/action-button' -type Props = { +type Props = Readonly<{ isExpand: boolean onExpandChange: (isExpand: boolean) => void -} +}> const ExpandBtn: FC = ({ isExpand, diff --git a/web/app/components/workflow/nodes/_base/components/variable/assigned-var-reference-popup.tsx b/web/app/components/workflow/nodes/_base/components/variable/assigned-var-reference-popup.tsx index 0ea72881472..b62006f5daf 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/assigned-var-reference-popup.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/assigned-var-reference-popup.tsx @@ -6,11 +6,11 @@ import { useTranslation } from 'react-i18next' import ListEmpty from '@/app/components/base/list-empty' import VarReferenceVars from './var-reference-vars' -type Props = { +type Props = Readonly<{ vars: NodeOutPutVar[] onChange: (value: ValueSelector, varDetail: Var) => void itemWidth?: number -} +}> const AssignedVarReferencePopup: FC = ({ vars, onChange, diff --git a/web/app/components/workflow/nodes/_base/components/variable/constant-field.tsx b/web/app/components/workflow/nodes/_base/components/variable/constant-field.tsx index e793b5922a8..aca17258330 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/constant-field.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/constant-field.tsx @@ -9,14 +9,14 @@ import { FormTypeEnum } from '@/app/components/header/account-setting/model-prov import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' -type Props = { +type Props = Readonly<{ schema: Partial readonly: boolean value: string onChange: (value: string | number, varKindType: VarKindType, varInfo?: Var) => void onOpenChange?: (open: boolean) => void isLoading?: boolean -} +}> const DEFAULT_SCHEMA = {} as CredentialFormSchema diff --git a/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/field.tsx b/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/field.tsx index dc0d6f75956..2954be21370 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/field.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/field.tsx @@ -13,14 +13,14 @@ import TreeIndentLine from '../tree-indent-line' const MAX_DEPTH = 10 -type Props = { +type Props = Readonly<{ valueSelector: ValueSelector name: string payload: FieldType depth?: number readonly?: boolean onSelect?: (valueSelector: ValueSelector) => void -} +}> const Field: FC = ({ valueSelector, diff --git a/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/index.tsx b/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/index.tsx index c9ba9465d95..00ca3ca1420 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/index.tsx @@ -8,14 +8,14 @@ import * as React from 'react' import { useRef } from 'react' import Field from './field' -type Props = { +type Props = Readonly<{ className?: string root: { nodeId?: string, nodeName?: string, attrName: string, attrAlias?: string } payload: StructuredOutput readonly?: boolean onSelect?: (valueSelector: ValueSelector) => void onHovering?: (value: boolean) => void -} +}> export const PickerPanelMain: FC = ({ className, diff --git a/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show/field.tsx b/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show/field.tsx index 863ec66eaba..807398a6a55 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show/field.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show/field.tsx @@ -10,13 +10,13 @@ import { Type } from '../../../../../llm/types' import { getFieldType } from '../../../../../llm/utils' import TreeIndentLine from '../tree-indent-line' -type Props = { +type Props = Readonly<{ name: string payload: FieldType required: boolean depth?: number rootClassName?: string -} +}> const Field: FC = ({ name, diff --git a/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show/index.tsx b/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show/index.tsx index ba03c3b17a7..268fc9e96f5 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show/index.tsx @@ -5,10 +5,10 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import Field from './field' -type Props = { +type Props = Readonly<{ payload: StructuredOutput rootClassName?: string -} +}> const ShowPanel: FC = ({ payload, diff --git a/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/tree-indent-line.tsx b/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/tree-indent-line.tsx index 0396a2e8717..5b6e865a2f9 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/tree-indent-line.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/tree-indent-line.tsx @@ -3,10 +3,10 @@ import type { FC } from 'react' import { cn } from '@langgenius/dify-ui/cn' import * as React from 'react' -type Props = { +type Props = Readonly<{ depth?: number className?: string -} +}> const TreeIndentLine: FC = ({ depth = 1, diff --git a/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx b/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx index a4bbe9a6eb2..5ec99bbce9f 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx @@ -13,13 +13,13 @@ import { checkKeys, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var import RemoveButton from '../remove-button' import VarTypePicker from './var-type-picker' -type Props = { +type Props = Readonly<{ readonly: boolean outputs: OutputVar outputKeyOrders: string[] onChange: (payload: OutputVar, changedIndex?: number, newKey?: string) => void onRemove: (index: number) => void -} +}> const OutputVarList: FC = ({ readonly, diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-full-path-panel.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-full-path-panel.tsx index 803164547d6..74425897b15 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-full-path-panel.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-full-path-panel.tsx @@ -7,12 +7,12 @@ import { PickerPanelMain as Panel } from '@/app/components/workflow/nodes/_base/ import { BlockEnum } from '@/app/components/workflow/types' import { Type } from '../../../llm/types' -type Props = { +type Props = Readonly<{ nodeName: string path: string[] varType: TypeWithArray nodeType?: BlockEnum -} +}> const VarFullPathPanel: FC = ({ nodeName, diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx index dc5b48946f0..eefb73b1e45 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx @@ -17,7 +17,7 @@ import { checkKeys, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var import RemoveButton from '../remove-button' import VarReferencePicker from './var-reference-picker' -type Props = { +type Props = Readonly<{ nodeId: string readonly: boolean list: Variable[] @@ -27,7 +27,7 @@ type Props = { onlyLeafNodeVar?: boolean filterVar?: (payload: Var, valueSelector: ValueSelector) => boolean isSupportFileVar?: boolean -} +}> const VarList: FC = ({ nodeId, diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.trigger.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.trigger.tsx index d10874761e8..a9ef140eeb9 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.trigger.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.trigger.tsx @@ -25,7 +25,7 @@ export type HoverPopup = | { kind: 'full-path', panel: ReactElement } | { kind: 'invalid-variable', message: string } -type Props = { +type Props = Readonly<{ className?: string controlFocus: number currentProvider?: ToolWithProvider | TriggerWithProvider @@ -70,7 +70,7 @@ type Props = { varKindTypes: Array<{ label: string, value: VarKindType }> varName: string variableCategory: string -} +}> const VarReferencePickerTrigger: FC = ({ className, diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx index 3ff9b4a1cbd..297fd3528f9 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx @@ -55,7 +55,7 @@ import VarReferencePopup from './var-reference-popup' const TRIGGER_DEFAULT_WIDTH = 227 -type Props = { +type Props = Readonly<{ className?: string nodeId: string isShowNodeName?: boolean @@ -85,7 +85,7 @@ type Props = { currentTool?: Tool currentProvider?: ToolWithProvider | TriggerWithProvider preferSchemaType?: boolean -} +}> const DEFAULT_VALUE_SELECTOR: Props['value'] = [] 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 4aa1688a122..b098e5d849b 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 @@ -8,14 +8,14 @@ import ListEmpty from '@/app/components/base/list-empty' import { useStore } from '@/app/components/workflow/store' import VarReferenceVars from './var-reference-vars' -type Props = { +type Props = Readonly<{ vars: NodeOutPutVar[] popupFor?: 'assigned' | 'toAssigned' onChange: (value: ValueSelector, varDetail: Var) => void itemWidth?: number isSupportFileVar?: boolean preferSchemaType?: boolean -} +}> const VarReferencePopup: FC = ({ vars, popupFor, diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx index 77c1f6ae711..6990750a180 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx @@ -279,7 +279,7 @@ const Item: FC = ({ ) } -type Props = { +type Props = Readonly<{ hideSearch?: boolean searchText?: string searchBoxClassName?: string @@ -295,7 +295,7 @@ type Props = { onManageInputField?: () => void autoFocus?: boolean preferSchemaType?: boolean -} +}> const VarReferenceVars: FC = ({ hideSearch, searchText, diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-type-picker.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-type-picker.tsx index 14a88b44323..c1a5bab70c9 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-type-picker.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-type-picker.tsx @@ -12,12 +12,12 @@ import { import * as React from 'react' import { VarType } from '@/app/components/workflow/types' -type Props = { +type Props = Readonly<{ className?: string readonly: boolean value: string onChange: (value: string) => void -} +}> const TYPES = [VarType.string, VarType.number, VarType.boolean, VarType.arrayNumber, VarType.arrayString, VarType.arrayBoolean, VarType.arrayObject, VarType.object] const VarReferencePicker: FC = ({ diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/index.tsx index 31a7e3b9fdd..23d405b587e 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/index.tsx @@ -12,7 +12,7 @@ import { useLastRun } from '@/service/use-workflow' import { FlowType } from '@/types/common' import NoData from './no-data' -type Props = { +type Props = Readonly<{ appId: string nodeId: string canSingleRun: boolean @@ -23,7 +23,7 @@ type Props = { onSingleRunClicked: () => void singleRunResult?: NodeTracing isPaused?: boolean -} & Partial +}> & Partial const LastRun: FC = ({ appId: _appId, diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/no-data.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/no-data.tsx index 2a2de60d2dd..44c8b181b12 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/no-data.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/no-data.tsx @@ -6,10 +6,10 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { ClockPlay } from '@/app/components/base/icons/src/vender/line/time' -type Props = { +type Props = Readonly<{ canSingleRun: boolean onSingleRun: () => void -} +}> const NoData: FC = ({ canSingleRun, diff --git a/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx b/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx index 08d4b8935da..bf5d738a4dc 100644 --- a/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx +++ b/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx @@ -20,7 +20,7 @@ import { VarType } from '@/app/components/workflow/types' import { AssignerNodeInputType, WriteMode } from '../../types' import OperationSelector from '../operation-selector' -type Props = { +type Props = Readonly<{ readonly: boolean nodeId: string list: AssignerNodeOperation[] @@ -33,7 +33,7 @@ type Props = { writeModeTypes?: WriteMode[] writeModeTypesArr?: WriteMode[] writeModeTypesNum?: WriteMode[] -} +}> const VarList: FC = ({ readonly, diff --git a/web/app/components/workflow/nodes/code/dependency-picker.tsx b/web/app/components/workflow/nodes/code/dependency-picker.tsx index da6f324bae1..8df81f146de 100644 --- a/web/app/components/workflow/nodes/code/dependency-picker.tsx +++ b/web/app/components/workflow/nodes/code/dependency-picker.tsx @@ -14,11 +14,11 @@ import { useCallback, useState } from 'react' import { Check } from '@/app/components/base/icons/src/vender/line/general' import Input from '@/app/components/base/input' -type Props = { +type Props = Readonly<{ value: CodeDependency available_dependencies: CodeDependency[] onChange: (dependency: CodeDependency) => void -} +}> const DependencyPicker: FC = ({ available_dependencies, diff --git a/web/app/components/workflow/nodes/http/components/api-input.tsx b/web/app/components/workflow/nodes/http/components/api-input.tsx index 340b892b09c..f102d815997 100644 --- a/web/app/components/workflow/nodes/http/components/api-input.tsx +++ b/web/app/components/workflow/nodes/http/components/api-input.tsx @@ -20,14 +20,14 @@ const MethodOptions = [ { label: 'PUT', value: Method.put }, { label: 'DELETE', value: Method.delete }, ] -type Props = { +type Props = Readonly<{ nodeId: string readonly: boolean method: Method onMethodChange: (method: Method) => void url: string onUrlChange: (url: string) => void -} +}> const ApiInput: FC = ({ nodeId, diff --git a/web/app/components/workflow/nodes/http/components/authorization/index.tsx b/web/app/components/workflow/nodes/http/components/authorization/index.tsx index a3d8fc7331f..a0dda3b45ab 100644 --- a/web/app/components/workflow/nodes/http/components/authorization/index.tsx +++ b/web/app/components/workflow/nodes/http/components/authorization/index.tsx @@ -18,13 +18,13 @@ import RadioGroup from './radio-group' const i18nPrefix = 'nodes.http.authorization' -type Props = { +type Props = Readonly<{ nodeId: string payload: AuthorizationPayloadType onChange: (payload: AuthorizationPayloadType) => void isShow: boolean onHide: () => void -} +}> const Field = ({ title, isRequired, children }: { title: string, isRequired?: boolean, children: React.JSX.Element }) => { return ( diff --git a/web/app/components/workflow/nodes/http/components/authorization/radio-group.tsx b/web/app/components/workflow/nodes/http/components/authorization/radio-group.tsx index 6af508ead2f..fff1fe79d6c 100644 --- a/web/app/components/workflow/nodes/http/components/authorization/radio-group.tsx +++ b/web/app/components/workflow/nodes/http/components/authorization/radio-group.tsx @@ -33,11 +33,11 @@ const Item: FC = ({ ) } -type Props = { +type Props = Readonly<{ options: Option[] value: string onChange: (value: string) => void -} +}> const RadioGroup: FC = ({ options, diff --git a/web/app/components/workflow/nodes/http/components/curl-panel.tsx b/web/app/components/workflow/nodes/http/components/curl-panel.tsx index 6c847dc6139..57af7cf827b 100644 --- a/web/app/components/workflow/nodes/http/components/curl-panel.tsx +++ b/web/app/components/workflow/nodes/http/components/curl-panel.tsx @@ -11,12 +11,12 @@ import { useTranslation } from 'react-i18next' import { useNodesInteractions } from '@/app/components/workflow/hooks' import { parseCurl } from './curl-parser' -type Props = { +type Props = Readonly<{ nodeId: string isShow: boolean onHide: () => void handleCurlImport: (node: HttpNodeType) => void -} +}> const CurlPanel: FC = ({ nodeId, isShow, onHide, handleCurlImport }) => { const [inputString, setInputString] = useState('') diff --git a/web/app/components/workflow/nodes/http/components/edit-body/index.tsx b/web/app/components/workflow/nodes/http/components/edit-body/index.tsx index 76be3ba08ab..88d9cec2941 100644 --- a/web/app/components/workflow/nodes/http/components/edit-body/index.tsx +++ b/web/app/components/workflow/nodes/http/components/edit-body/index.tsx @@ -17,12 +17,12 @@ import { isSupportedHttpBodyVariable } from './supported-body-vars' const UNIQUE_ID_PREFIX = 'key-value-' -type Props = { +type Props = Readonly<{ readonly: boolean nodeId: string payload: Body onChange: (payload: Body) => void -} +}> const allTypes = [ BodyType.none, diff --git a/web/app/components/workflow/nodes/http/components/key-value/bulk-edit/index.tsx b/web/app/components/workflow/nodes/http/components/key-value/bulk-edit/index.tsx index 1a9a37fa7b8..df993078605 100644 --- a/web/app/components/workflow/nodes/http/components/key-value/bulk-edit/index.tsx +++ b/web/app/components/workflow/nodes/http/components/key-value/bulk-edit/index.tsx @@ -8,11 +8,11 @@ import TextEditor from '@/app/components/workflow/nodes/_base/components/editor/ const i18nPrefix = 'nodes.http' -type Props = { +type Props = Readonly<{ value: string onChange: (value: string) => void onSwitchToKeyValueEdit: () => void -} +}> const BulkEdit: FC = ({ value, diff --git a/web/app/components/workflow/nodes/http/components/key-value/index.tsx b/web/app/components/workflow/nodes/http/components/key-value/index.tsx index 02ba7c641dc..5c1eeca2c0c 100644 --- a/web/app/components/workflow/nodes/http/components/key-value/index.tsx +++ b/web/app/components/workflow/nodes/http/components/key-value/index.tsx @@ -4,7 +4,7 @@ import type { KeyValue } from '../../types' import * as React from 'react' import KeyValueEdit from './key-value-edit' -type Props = { +type Props = Readonly<{ readonly: boolean nodeId: string list: KeyValue[] @@ -12,7 +12,7 @@ type Props = { onAdd: () => void isSupportFile?: boolean // toggleKeyValueEdit: () => void -} +}> const KeyValueList: FC = ({ readonly, diff --git a/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/index.tsx b/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/index.tsx index 2a4b6b23b68..857405d149b 100644 --- a/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/index.tsx +++ b/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/index.tsx @@ -10,7 +10,7 @@ import KeyValueItem from './item' const i18nPrefix = 'nodes.http' -type Props = { +type Props = Readonly<{ readonly: boolean nodeId: string list: KeyValue[] @@ -20,7 +20,7 @@ type Props = { // onSwitchToBulkEdit: () => void keyNotSupportVar?: boolean insertVarTipToLeft?: boolean -} +}> const KeyValueList: FC = ({ readonly, diff --git a/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/input-item.tsx b/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/input-item.tsx index 6fa83dac522..bea36104b8b 100644 --- a/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/input-item.tsx +++ b/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/input-item.tsx @@ -10,7 +10,7 @@ import RemoveButton from '@/app/components/workflow/nodes/_base/components/remov import { VarType } from '@/app/components/workflow/types' import useAvailableVarList from '../../../../_base/hooks/use-available-var-list' -type Props = { +type Props = Readonly<{ className?: string instanceId?: string nodeId: string @@ -22,7 +22,7 @@ type Props = { readOnly?: boolean isSupportFile?: boolean insertVarTipToLeft?: boolean -} +}> const InputItem: FC = ({ className, diff --git a/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/item.tsx b/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/item.tsx index b349e8d8b22..28b33ba94e9 100644 --- a/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/item.tsx +++ b/web/app/components/workflow/nodes/http/components/key-value/key-value-edit/item.tsx @@ -23,7 +23,7 @@ import InputItem from './input-item' const i18nPrefix = 'nodes.http' -type Props = { +type Props = Readonly<{ instanceId: string className?: string nodeId: string @@ -37,7 +37,7 @@ type Props = { isSupportFile?: boolean keyNotSupportVar?: boolean insertVarTipToLeft?: boolean -} +}> const KeyValueItem: FC = ({ instanceId, diff --git a/web/app/components/workflow/nodes/http/components/timeout/index.tsx b/web/app/components/workflow/nodes/http/components/timeout/index.tsx index 66a640ed146..027fdf284f7 100644 --- a/web/app/components/workflow/nodes/http/components/timeout/index.tsx +++ b/web/app/components/workflow/nodes/http/components/timeout/index.tsx @@ -8,12 +8,12 @@ import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/ import { useStore } from '@/app/components/workflow/store' import { BlockEnum } from '@/app/components/workflow/types' -type Props = { +type Props = Readonly<{ readonly: boolean nodeId: string payload: TimeoutPayloadType onChange: (payload: TimeoutPayloadType) => void -} +}> const i18nPrefix = 'nodes.http' diff --git a/web/app/components/workflow/nodes/human-input/components/add-input-field.tsx b/web/app/components/workflow/nodes/human-input/components/add-input-field.tsx index b8fe4ebf9ad..3a5603cff7b 100644 --- a/web/app/components/workflow/nodes/human-input/components/add-input-field.tsx +++ b/web/app/components/workflow/nodes/human-input/components/add-input-field.tsx @@ -4,12 +4,12 @@ import type { FormInputItem } from '../types' import * as React from 'react' import InputField from '@/app/components/base/prompt-editor/plugins/hitl-input-block/input-field' -type Props = { +type Props = Readonly<{ nodeId: string unavailableVariableNames?: string[] onSave: (newPayload: FormInputItem) => void onCancel: () => void -} +}> const AddInputField: FC = ({ nodeId, diff --git a/web/app/components/workflow/nodes/human-input/components/button-style-dropdown.tsx b/web/app/components/workflow/nodes/human-input/components/button-style-dropdown.tsx index b3149f6b667..b3f7a548a61 100644 --- a/web/app/components/workflow/nodes/human-input/components/button-style-dropdown.tsx +++ b/web/app/components/workflow/nodes/human-input/components/button-style-dropdown.tsx @@ -16,12 +16,12 @@ import { UserActionButtonType } from '../types' const i18nPrefix = 'nodes.humanInput' -type Props = { +type Props = Readonly<{ text: string data: UserActionButtonType onChange: (state: UserActionButtonType) => void readonly?: boolean -} +}> const ButtonStyleDropdown: FC = ({ text = 'Button Text', diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/index.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/index.tsx index 50c2bf333aa..47a1bc8ce95 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/index.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/index.tsx @@ -14,7 +14,7 @@ import { UpgradeModal } from './upgrade-modal' const i18nPrefix = 'nodes.humanInput' -type Props = { +type Props = Readonly<{ nodeId: string value: DeliveryMethod[] nodesOutputVars?: NodeOutPutVar[] @@ -23,7 +23,7 @@ type Props = { formInputs?: FormInputItem[] onChange: (value: DeliveryMethod[]) => void readonly?: boolean -} +}> const DeliveryMethodForm: React.FC = ({ nodeId, diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/email-input.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/email-input.tsx index 017b5228859..fe260edee4b 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/email-input.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/email-input.tsx @@ -13,7 +13,7 @@ import MemberList from './member-list' const i18nPrefix = 'nodes.humanInput' -type Props = { +type Props = Readonly<{ email: string value: RecipientItem[] list: Member[] @@ -21,7 +21,7 @@ type Props = { onSelect: (value: string) => void onAdd: (email: string) => void disabled?: boolean -} +}> const EmailInput = ({ email, diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/email-item.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/email-item.tsx index 91f381b55bb..537e71839b1 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/email-item.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/email-item.tsx @@ -6,13 +6,13 @@ import { RiCloseCircleFill, RiErrorWarningFill } from '@remixicon/react' import * as React from 'react' import { useTranslation } from 'react-i18next' -type Props = { +type Props = Readonly<{ email: string data: Member disabled?: boolean onDelete: (recipient: RecipientItem) => void isError: boolean -} +}> const EmailItem = ({ email, diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/index.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/index.tsx index d2ccd2ed608..eb741a89eb8 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/index.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/index.tsx @@ -12,10 +12,10 @@ import MemberSelector from './member-selector' const i18nPrefix = 'nodes.humanInput' -type Props = { +type Props = Readonly<{ data: RecipientData onChange: (data: RecipientData) => void -} +}> const Recipient = ({ data, diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/member-list.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/member-list.tsx index 0e262e839e0..0d8569031d7 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/member-list.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/member-list.tsx @@ -10,7 +10,7 @@ import Input from '@/app/components/base/input' const i18nPrefix = 'nodes.humanInput' -type Props = { +type Props = Readonly<{ value: Recipient[] searchValue: string onSearchChange: (value: string) => void @@ -18,7 +18,7 @@ type Props = { onSelect: (value: string) => void email: string hideSearch?: boolean -} +}> const MemberList: FC = ({ searchValue, list, value, onSearchChange, onSelect, email, hideSearch }) => { const { t } = useTranslation() diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/member-selector.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/member-selector.tsx index 1e2de58247d..4c4ee547754 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/member-selector.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/member-selector.tsx @@ -17,12 +17,12 @@ import MemberList from './member-list' const i18nPrefix = 'nodes.humanInput' -type Props = { +type Props = Readonly<{ value: Recipient[] email: string onSelect: (value: string) => void list: Member[] -} +}> const MemberSelector: FC = ({ value, diff --git a/web/app/components/workflow/nodes/human-input/components/single-run-form.tsx b/web/app/components/workflow/nodes/human-input/components/single-run-form.tsx index c0d27bfb6d4..280884389b2 100644 --- a/web/app/components/workflow/nodes/human-input/components/single-run-form.tsx +++ b/web/app/components/workflow/nodes/human-input/components/single-run-form.tsx @@ -12,13 +12,13 @@ import { useTranslation } from 'react-i18next' import ContentItem from '@/app/components/base/chat/chat/answer/human-input-content/content-item' import { getButtonStyle, getRenderedFormInputs, hasInvalidSelectOrFileInput, initializeInputs, splitByOutputVar } from '@/app/components/base/chat/chat/answer/human-input-content/utils' -type Props = { +type Props = Readonly<{ nodeName: string data: HumanInputFormData showBackButton?: boolean handleBack?: () => void onSubmit?: ({ inputs, action }: { inputs: Record, action: string }) => Promise -} +}> const FormContent = ({ nodeName, diff --git a/web/app/components/workflow/nodes/human-input/components/timeout.tsx b/web/app/components/workflow/nodes/human-input/components/timeout.tsx index 682a56a002e..a8cfa121fb6 100644 --- a/web/app/components/workflow/nodes/human-input/components/timeout.tsx +++ b/web/app/components/workflow/nodes/human-input/components/timeout.tsx @@ -6,12 +6,12 @@ import Input from '@/app/components/base/input' const i18nPrefix = 'nodes.humanInput' -type Props = { +type Props = Readonly<{ timeout: number unit: 'day' | 'hour' onChange: (state: { timeout: number, unit: 'day' | 'hour' }) => void readonly?: boolean -} +}> const TimeoutInput: FC = ({ timeout, diff --git a/web/app/components/workflow/nodes/if-else/components/condition-wrap.tsx b/web/app/components/workflow/nodes/if-else/components/condition-wrap.tsx index 889025191fb..4ca54041f4c 100644 --- a/web/app/components/workflow/nodes/if-else/components/condition-wrap.tsx +++ b/web/app/components/workflow/nodes/if-else/components/condition-wrap.tsx @@ -21,7 +21,7 @@ import { useGetAvailableVars } from '../../variable-assigner/hooks' import ConditionAdd from './condition-add' import ConditionList from './condition-list' -type Props = { +type Props = Readonly<{ isSubVariable?: boolean caseId?: string conditionId?: string @@ -42,7 +42,7 @@ type Props = { availableNodes: Node[] varsIsVarFileAttribute?: Record filterVar: (varPayload: Var) => boolean -} +}> const ConditionWrap: FC = ({ isSubVariable, diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/components/add-dataset.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/components/add-dataset.tsx index 096ddbf4f11..f2537f94e16 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/components/add-dataset.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/components/add-dataset.tsx @@ -7,10 +7,10 @@ import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import SelectDataset from '@/app/components/app/configuration/dataset-config/select-dataset' -type Props = { +type Props = Readonly<{ selectedIds: string[] onChange: (dataSets: DataSet[]) => void -} +}> const AddDataset: FC = ({ selectedIds, diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/components/dataset-item.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/components/dataset-item.tsx index 778e0069042..c2a1aff4912 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/components/dataset-item.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/components/dataset-item.tsx @@ -27,13 +27,13 @@ import FeatureIcon from '@/app/components/header/account-setting/model-provider- import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { useKnowledge } from '@/hooks/use-knowledge' -type Props = { +type Props = Readonly<{ payload: DataSet onRemove: () => void onChange: (dataSet: DataSet) => void readonly?: boolean editable?: boolean -} +}> const DatasetItem: FC = ({ payload, diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/components/dataset-list.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/components/dataset-list.tsx index 9c1dd854ba0..1c14b6dbea1 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/components/dataset-list.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/components/dataset-list.tsx @@ -9,11 +9,11 @@ import { useSelector as useAppContextSelector } from '@/context/app-context' import { hasEditPermissionForDataset } from '@/utils/permission' import Item from './dataset-item' -type Props = { +type Props = Readonly<{ list: DataSet[] onChange: (list: DataSet[]) => void readonly?: boolean -} +}> const DatasetList: FC = ({ list, diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/components/retrieval-config.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/components/retrieval-config.tsx index 8aa49c11376..987ee789147 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/components/retrieval-config.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/components/retrieval-config.tsx @@ -20,7 +20,7 @@ import ConfigRetrievalContent from '@/app/components/app/configuration/dataset-c import { DATASET_DEFAULT } from '@/config' import { RETRIEVE_TYPE } from '@/types/app' -type Props = { +type Props = Readonly<{ payload: { retrieval_mode: RETRIEVE_TYPE multiple_retrieval_config?: MultipleRetrievalConfig @@ -35,7 +35,7 @@ type Props = { rerankModalOpen: boolean onRerankModelOpenChange: (open: boolean) => void selectedDatasets: DataSet[] -} +}> const RetrievalConfig: FC = ({ payload, diff --git a/web/app/components/workflow/nodes/list-operator/components/extract-input.tsx b/web/app/components/workflow/nodes/list-operator/components/extract-input.tsx index a5b91ec1995..023fcefe39e 100644 --- a/web/app/components/workflow/nodes/list-operator/components/extract-input.tsx +++ b/web/app/components/workflow/nodes/list-operator/components/extract-input.tsx @@ -9,12 +9,12 @@ import Input from '@/app/components/workflow/nodes/_base/components/input-suppor import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list' import { VarType } from '../../../types' -type Props = { +type Props = Readonly<{ nodeId: string readOnly: boolean value: string onChange: (value: string) => void -} +}> const ExtractInput: FC = ({ nodeId, diff --git a/web/app/components/workflow/nodes/list-operator/components/filter-condition.tsx b/web/app/components/workflow/nodes/list-operator/components/filter-condition.tsx index 6d92b8123ce..8a159ae62cf 100644 --- a/web/app/components/workflow/nodes/list-operator/components/filter-condition.tsx +++ b/web/app/components/workflow/nodes/list-operator/components/filter-condition.tsx @@ -30,14 +30,14 @@ const VAR_INPUT_SUPPORTED_KEYS: Record = { size: VarType.number, } -type Props = { +type Props = Readonly<{ condition: Condition varType: VarType onChange: (condition: Condition) => void hasSubVariable: boolean readOnly: boolean nodeId: string -} +}> const getExpectedVarType = (condition: Condition, varType: VarType) => { return condition.key ? VAR_INPUT_SUPPORTED_KEYS[condition.key] : varType diff --git a/web/app/components/workflow/nodes/list-operator/components/limit-config.tsx b/web/app/components/workflow/nodes/list-operator/components/limit-config.tsx index 223d24d96c4..95e27904c05 100644 --- a/web/app/components/workflow/nodes/list-operator/components/limit-config.tsx +++ b/web/app/components/workflow/nodes/list-operator/components/limit-config.tsx @@ -14,13 +14,13 @@ const LIMIT_SIZE_MIN = 1 const LIMIT_SIZE_MAX = 20 const LIMIT_SIZE_DEFAULT = 10 -type Props = { +type Props = Readonly<{ className?: string readonly: boolean config: Limit onChange: (limit: Limit) => void canSetRoleName?: boolean -} +}> const LIMIT_DEFAULT: Limit = { enabled: false, diff --git a/web/app/components/workflow/nodes/list-operator/components/sub-variable-picker.tsx b/web/app/components/workflow/nodes/list-operator/components/sub-variable-picker.tsx index 53eb583bd4f..c6e374cb8d5 100644 --- a/web/app/components/workflow/nodes/list-operator/components/sub-variable-picker.tsx +++ b/web/app/components/workflow/nodes/list-operator/components/sub-variable-picker.tsx @@ -8,11 +8,11 @@ import { useTranslation } from 'react-i18next' import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' import { SUB_VARIABLES } from '../../constants' -type Props = { +type Props = Readonly<{ value: string onChange: (value: string) => void className?: string -} +}> type SubVariableOption = { value: string diff --git a/web/app/components/workflow/nodes/llm/components/config-prompt-item.tsx b/web/app/components/workflow/nodes/llm/components/config-prompt-item.tsx index c9a55d42ef9..275aad377b8 100644 --- a/web/app/components/workflow/nodes/llm/components/config-prompt-item.tsx +++ b/web/app/components/workflow/nodes/llm/components/config-prompt-item.tsx @@ -13,7 +13,7 @@ import { EditionType } from '../../../types' const i18nPrefix = 'nodes.llm' -type Props = { +type Props = Readonly<{ instanceId: string className?: string headerClassName?: string @@ -40,7 +40,7 @@ type Props = { varList: Variable[] handleAddVariable: (payload: Variable) => void modelConfig?: ModelConfig -} +}> const roleOptions = [ { diff --git a/web/app/components/workflow/nodes/llm/components/config-prompt.tsx b/web/app/components/workflow/nodes/llm/components/config-prompt.tsx index 8665966e3d6..ee0407cb0c5 100644 --- a/web/app/components/workflow/nodes/llm/components/config-prompt.tsx +++ b/web/app/components/workflow/nodes/llm/components/config-prompt.tsx @@ -18,7 +18,7 @@ import ConfigPromptItem from './config-prompt-item' const i18nPrefix = 'nodes.llm' -type Props = { +type Props = Readonly<{ readOnly: boolean nodeId: string filterVar: (payload: Var, selector: ValueSelector) => boolean @@ -35,7 +35,7 @@ type Props = { varList?: Variable[] handleAddVariable: (payload: any) => void modelConfig: ModelConfig -} +}> const ConfigPrompt: FC = ({ readOnly, diff --git a/web/app/components/workflow/nodes/llm/components/panel-memory-section.tsx b/web/app/components/workflow/nodes/llm/components/panel-memory-section.tsx index 81d53ec488d..d867e969662 100644 --- a/web/app/components/workflow/nodes/llm/components/panel-memory-section.tsx +++ b/web/app/components/workflow/nodes/llm/components/panel-memory-section.tsx @@ -8,7 +8,7 @@ import MemoryConfig from '@/app/components/workflow/nodes/_base/components/memor import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor' import { FlowType } from '@/types/common' -type Props = { +type Props = Readonly<{ readOnly: boolean isChatMode: boolean isChatModel: boolean @@ -24,7 +24,7 @@ type Props = { flowType?: FlowType handleSyeQueryChange: (query: string) => void handleMemoryChange: (memory?: Memory) => void -} +}> const i18nPrefix = 'nodes.llm' const DEFAULT_MEMORY: Memory = { diff --git a/web/app/components/workflow/nodes/llm/components/panel-output-section.tsx b/web/app/components/workflow/nodes/llm/components/panel-output-section.tsx index 82b0a24aefc..c5da2c68d52 100644 --- a/web/app/components/workflow/nodes/llm/components/panel-output-section.tsx +++ b/web/app/components/workflow/nodes/llm/components/panel-output-section.tsx @@ -9,7 +9,7 @@ import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/compo import Split from '@/app/components/workflow/nodes/_base/components/split' import { StructureOutput } from './structure-output' -type Props = { +type Props = Readonly<{ readOnly: boolean inputs: LLMNodeType isModelSupportStructuredOutput: boolean | undefined @@ -17,7 +17,7 @@ type Props = { setStructuredOutputCollapsed: (collapsed: boolean) => void handleStructureOutputEnableChange: (enabled: boolean) => void handleStructureOutputChange: (newOutput: StructuredOutput) => void -} +}> const i18nPrefix = 'nodes.llm' diff --git a/web/app/components/workflow/nodes/llm/components/prompt-generator-btn.tsx b/web/app/components/workflow/nodes/llm/components/prompt-generator-btn.tsx index 0bd2cd798ec..ea4328a56da 100644 --- a/web/app/components/workflow/nodes/llm/components/prompt-generator-btn.tsx +++ b/web/app/components/workflow/nodes/llm/components/prompt-generator-btn.tsx @@ -12,14 +12,14 @@ import { Generator } from '@/app/components/base/icons/src/vender/other' import { AppModeEnum } from '@/types/app' import { useHooksStore } from '../../../hooks-store' -type Props = { +type Props = Readonly<{ className?: string onGenerated?: (prompt: string) => void modelConfig?: ModelConfig nodeId: string editorId?: string currentPrompt?: string -} +}> const PromptGeneratorBtn: FC = ({ className, diff --git a/web/app/components/workflow/nodes/llm/components/resolution-picker.tsx b/web/app/components/workflow/nodes/llm/components/resolution-picker.tsx index 8a1d06deb54..6380e5d6d1b 100644 --- a/web/app/components/workflow/nodes/llm/components/resolution-picker.tsx +++ b/web/app/components/workflow/nodes/llm/components/resolution-picker.tsx @@ -8,10 +8,10 @@ import { Resolution } from '@/types/app' const i18nPrefix = 'nodes.llm' -type Props = { +type Props = Readonly<{ value: Resolution onChange: (value: Resolution) => void -} +}> const ResolutionPicker: FC = ({ value, diff --git a/web/app/components/workflow/nodes/llm/components/structure-output.tsx b/web/app/components/workflow/nodes/llm/components/structure-output.tsx index c7dadab2693..56a0f40fd70 100644 --- a/web/app/components/workflow/nodes/llm/components/structure-output.tsx +++ b/web/app/components/workflow/nodes/llm/components/structure-output.tsx @@ -8,11 +8,11 @@ import ShowPanel from '@/app/components/workflow/nodes/_base/components/variable import { Type } from '../types' import { JsonSchemaConfigModal } from './json-schema-config-modal' -type Props = { +type Props = Readonly<{ className?: string value?: StructuredOutput onChange: (value: StructuredOutput) => void -} +}> export function StructureOutput({ className, diff --git a/web/app/components/workflow/nodes/loop/components/condition-wrap.tsx b/web/app/components/workflow/nodes/loop/components/condition-wrap.tsx index 3e1c70e3196..09d4869c770 100644 --- a/web/app/components/workflow/nodes/loop/components/condition-wrap.tsx +++ b/web/app/components/workflow/nodes/loop/components/condition-wrap.tsx @@ -17,7 +17,7 @@ import { SUB_VARIABLES } from './../default' import ConditionAdd from './condition-add' import ConditionList from './condition-list' -type Props = { +type Props = Readonly<{ isSubVariable?: boolean conditionId?: string conditions: Condition[] @@ -34,7 +34,7 @@ type Props = { nodeId: string availableNodes: Node[] availableVars: NodeOutPutVar[] -} +}> const ConditionWrap: FC = ({ isSubVariable, diff --git a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/import-from-tool.tsx b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/import-from-tool.tsx index 3770d442fcd..48240858a17 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/import-from-tool.tsx +++ b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/import-from-tool.tsx @@ -25,9 +25,9 @@ import BlockSelector from '../../../../block-selector' const i18nPrefix = 'nodes.parameterExtractor' -type Props = { +type Props = Readonly<{ onImport: (params: Param[]) => void -} +}> function toParmExactParams(toolParams: ToolParameter[], lan: string): Param[] { return toolParams.map((item) => { diff --git a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/item.tsx b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/item.tsx index b8a38bca8ea..b47f73b8da5 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/item.tsx +++ b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/item.tsx @@ -11,11 +11,11 @@ import { Variable02 } from '@/app/components/base/icons/src/vender/solid/develop const i18nPrefix = 'nodes.parameterExtractor' -type Props = { +type Props = Readonly<{ payload: Param onEdit: () => void onDelete: () => void -} +}> const Item: FC = ({ payload, diff --git a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/list.tsx b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/list.tsx index 01d16f64c80..853ade7300c 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/list.tsx +++ b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/list.tsx @@ -12,11 +12,11 @@ import EditParam from './update' const i18nPrefix = 'nodes.parameterExtractor' -type Props = { +type Props = Readonly<{ readonly: boolean list: Param[] onChange: (list: Param[], moreInfo?: MoreInfo) => void -} +}> const List: FC = ({ list, diff --git a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/update.tsx b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/update.tsx index 5b28a8fcabd..7d9a37ae686 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/update.tsx +++ b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/update.tsx @@ -29,12 +29,12 @@ const DEFAULT_PARAM: Param = { required: false, } -type Props = { +type Props = Readonly<{ type: 'add' | 'edit' payload?: Param onSave: (payload: Param, moreInfo?: MoreInfo) => void onCancel?: () => void -} +}> const TYPES = [ParamType.string, ParamType.number, ParamType.bool, ParamType.arrayString, ParamType.arrayNumber, ParamType.arrayObject, ParamType.arrayBool] diff --git a/web/app/components/workflow/nodes/parameter-extractor/components/reasoning-mode-picker.tsx b/web/app/components/workflow/nodes/parameter-extractor/components/reasoning-mode-picker.tsx index c4b8809ffe2..f2f9a800190 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/components/reasoning-mode-picker.tsx +++ b/web/app/components/workflow/nodes/parameter-extractor/components/reasoning-mode-picker.tsx @@ -9,10 +9,10 @@ import { ReasoningModeType } from '../types' const i18nPrefix = 'nodes.parameterExtractor' -type Props = { +type Props = Readonly<{ type: ReasoningModeType onChange: (type: ReasoningModeType) => void -} +}> const ReasoningModePicker: FC = ({ type, diff --git a/web/app/components/workflow/nodes/question-classifier/components/advanced-setting.tsx b/web/app/components/workflow/nodes/question-classifier/components/advanced-setting.tsx index 21a925f7bc5..752a73270bd 100644 --- a/web/app/components/workflow/nodes/question-classifier/components/advanced-setting.tsx +++ b/web/app/components/workflow/nodes/question-classifier/components/advanced-setting.tsx @@ -9,7 +9,7 @@ import MemoryConfig from '../../_base/components/memory-config' const i18nPrefix = 'nodes.questionClassifiers' -type Props = { +type Props = Readonly<{ instruction: string onInstructionChange: (instruction: string) => void hideMemorySetting: boolean @@ -25,7 +25,7 @@ type Props = { } nodesOutputVars: NodeOutPutVar[] availableNodes: Node[] -} +}> const AdvancedSetting: FC = ({ instruction, diff --git a/web/app/components/workflow/nodes/question-classifier/components/class-item.tsx b/web/app/components/workflow/nodes/question-classifier/components/class-item.tsx index 9894e29cc11..38e42d6fe14 100644 --- a/web/app/components/workflow/nodes/question-classifier/components/class-item.tsx +++ b/web/app/components/workflow/nodes/question-classifier/components/class-item.tsx @@ -12,7 +12,7 @@ import { getCanonicalClassLabel, getDisplayClassLabel } from './class-label-util const i18nPrefix = 'nodes.questionClassifiers' -type Props = { +type Props = Readonly<{ className?: string headerClassName?: string nodeId: string @@ -23,7 +23,7 @@ type Props = { readonly?: boolean filterVar: (payload: Var, valueSelector: ValueSelector) => boolean onLabelEditStart?: () => void -} +}> const ClassItem: FC = ({ className, diff --git a/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx b/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx index ef7764a106c..e8a809b5606 100644 --- a/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx +++ b/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx @@ -20,14 +20,14 @@ import { getDefaultClassLabel, isDefaultClassLabel } from './class-label-utils' const i18nPrefix = 'nodes.questionClassifiers' const INLINE_LABEL_HINT_STORAGE_KEY = 'question-classifier-inline-label-hint-dismissed' -type Props = { +type Props = Readonly<{ nodeId: string list: Topic[] onChange: (list: Topic[]) => void readonly?: boolean filterVar: (payload: Var, valueSelector: ValueSelector) => boolean handleSortTopic?: (newTopics: (Topic & { id: string })[]) => void -} +}> const ClassList: FC = ({ nodeId, diff --git a/web/app/components/workflow/nodes/start/components/var-item.tsx b/web/app/components/workflow/nodes/start/components/var-item.tsx index 2820f209d65..d3568960a0b 100644 --- a/web/app/components/workflow/nodes/start/components/var-item.tsx +++ b/web/app/components/workflow/nodes/start/components/var-item.tsx @@ -16,7 +16,7 @@ import { Variable02 } from '@/app/components/base/icons/src/vender/solid/develop import { Edit03 } from '@/app/components/base/icons/src/vender/solid/general' import InputVarTypeIcon from '../../_base/components/input-var-type-icon' -type Props = { +type Props = Readonly<{ className?: string readonly: boolean payload: InputVar @@ -26,7 +26,7 @@ type Props = { varKeys?: string[] showLegacyBadge?: boolean canDrag?: boolean -} +}> const VarItem: FC = ({ className, diff --git a/web/app/components/workflow/nodes/start/components/var-list.tsx b/web/app/components/workflow/nodes/start/components/var-list.tsx index 6e5caf6a834..c606644f29d 100644 --- a/web/app/components/workflow/nodes/start/components/var-list.tsx +++ b/web/app/components/workflow/nodes/start/components/var-list.tsx @@ -13,11 +13,11 @@ import { ChangeType } from '@/app/components/workflow/types' import { hasDuplicateStr } from '@/utils/var' import VarItem from './var-item' -type Props = { +type Props = Readonly<{ readonly: boolean list: InputVar[] onChange: (list: InputVar[], moreInfo?: { index: number, payload: MoreInfo }) => void -} +}> const VarList: FC = ({ readonly, diff --git a/web/app/components/workflow/nodes/tool/components/copy-id.tsx b/web/app/components/workflow/nodes/tool/components/copy-id.tsx index 2c55b009a48..ff0b7249512 100644 --- a/web/app/components/workflow/nodes/tool/components/copy-id.tsx +++ b/web/app/components/workflow/nodes/tool/components/copy-id.tsx @@ -6,9 +6,9 @@ import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' -type Props = { +type Props = Readonly<{ content: string -} +}> const prefixEmbedded = 'overview.appInfo.embedded' diff --git a/web/app/components/workflow/nodes/tool/components/input-var-list.tsx b/web/app/components/workflow/nodes/tool/components/input-var-list.tsx index 6765c92033f..7dbcf1f8300 100644 --- a/web/app/components/workflow/nodes/tool/components/input-var-list.tsx +++ b/web/app/components/workflow/nodes/tool/components/input-var-list.tsx @@ -20,7 +20,7 @@ import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use import { VarType } from '@/app/components/workflow/types' import { VarType as VarKindType } from '../types' -type Props = { +type Props = Readonly<{ readOnly: boolean nodeId: string schema: CredentialFormSchema[] @@ -31,7 +31,7 @@ type Props = { filterVar?: (payload: Var, valueSelector: ValueSelector) => boolean currentTool?: Tool currentProvider?: ToolWithProvider -} +}> const InputVarList: FC = ({ readOnly, diff --git a/web/app/components/workflow/nodes/tool/components/tool-form/index.tsx b/web/app/components/workflow/nodes/tool/components/tool-form/index.tsx index c8d60be7895..f858e5dc401 100644 --- a/web/app/components/workflow/nodes/tool/components/tool-form/index.tsx +++ b/web/app/components/workflow/nodes/tool/components/tool-form/index.tsx @@ -6,7 +6,7 @@ import type { Tool } from '@/app/components/tools/types' import type { ToolWithProvider } from '@/app/components/workflow/types' import ToolFormItem from './item' -type Props = { +type Props = Readonly<{ readOnly: boolean nodeId: string schema: CredentialFormSchema[] @@ -19,7 +19,7 @@ type Props = { showManageInputField?: boolean onManageInputField?: () => void extraParams?: Record -} +}> const ToolForm: FC = ({ readOnly, diff --git a/web/app/components/workflow/nodes/tool/components/tool-form/item.tsx b/web/app/components/workflow/nodes/tool/components/tool-form/item.tsx index fa7c1b867d2..6680c5ad325 100644 --- a/web/app/components/workflow/nodes/tool/components/tool-form/item.tsx +++ b/web/app/components/workflow/nodes/tool/components/tool-form/item.tsx @@ -54,7 +54,7 @@ const renderDescriptionWithLinks = (description: string): ReactNode => { return parts } -type Props = { +type Props = Readonly<{ readOnly: boolean nodeId: string schema: CredentialFormSchema @@ -67,7 +67,7 @@ type Props = { onManageInputField?: () => void extraParams?: Record providerType?: 'tool' | 'trigger' -} +}> const ToolFormItem: FC = ({ readOnly, diff --git a/web/app/components/workflow/nodes/trigger-plugin/components/trigger-form/index.tsx b/web/app/components/workflow/nodes/trigger-plugin/components/trigger-form/index.tsx index 7ed42ea0195..d3282f7d94a 100644 --- a/web/app/components/workflow/nodes/trigger-plugin/components/trigger-form/index.tsx +++ b/web/app/components/workflow/nodes/trigger-plugin/components/trigger-form/index.tsx @@ -6,7 +6,7 @@ import type { TriggerWithProvider } from '@/app/components/workflow/block-select import type { PluginTriggerVarInputs } from '@/app/components/workflow/nodes/trigger-plugin/types' import TriggerFormItem from './item' -type Props = { +type Props = Readonly<{ readOnly: boolean nodeId: string schema: CredentialFormSchema[] @@ -18,7 +18,7 @@ type Props = { currentProvider?: TriggerWithProvider extraParams?: Record disableVariableInsertion?: boolean -} +}> const TriggerForm: FC = ({ readOnly, diff --git a/web/app/components/workflow/nodes/trigger-plugin/components/trigger-form/item.tsx b/web/app/components/workflow/nodes/trigger-plugin/components/trigger-form/item.tsx index 40119674d46..9427c8857f2 100644 --- a/web/app/components/workflow/nodes/trigger-plugin/components/trigger-form/item.tsx +++ b/web/app/components/workflow/nodes/trigger-plugin/components/trigger-form/item.tsx @@ -15,7 +15,7 @@ import { useLanguage } from '@/app/components/header/account-setting/model-provi import { SchemaModal } from '@/app/components/plugins/plugin-detail-panel/tool-selector/components' import FormInputItem from '@/app/components/workflow/nodes/_base/components/form-input-item' -type Props = { +type Props = Readonly<{ readOnly: boolean nodeId: string schema: CredentialFormSchema @@ -26,7 +26,7 @@ type Props = { currentProvider?: TriggerWithProvider extraParams?: Record disableVariableInsertion?: boolean -} +}> const TriggerFormItem: FC = ({ readOnly, diff --git a/web/app/components/workflow/nodes/variable-assigner/components/var-group-item.tsx b/web/app/components/workflow/nodes/variable-assigner/components/var-group-item.tsx index 0f23c0c8913..3d8943515db 100644 --- a/web/app/components/workflow/nodes/variable-assigner/components/var-group-item.tsx +++ b/web/app/components/workflow/nodes/variable-assigner/components/var-group-item.tsx @@ -25,7 +25,7 @@ type Payload = VarGroupItemType & { group_name?: string } -type Props = { +type Props = Readonly<{ readOnly: boolean nodeId: string payload: Payload @@ -35,7 +35,7 @@ type Props = { canRemove?: boolean onRemove?: () => void availableVars: NodeOutPutVar[] -} +}> const VarGroupItem: FC = ({ readOnly, diff --git a/web/app/components/workflow/nodes/variable-assigner/components/var-list/index.tsx b/web/app/components/workflow/nodes/variable-assigner/components/var-list/index.tsx index 1f45a8a0e1d..c4f324272e3 100644 --- a/web/app/components/workflow/nodes/variable-assigner/components/var-list/index.tsx +++ b/web/app/components/workflow/nodes/variable-assigner/components/var-list/index.tsx @@ -11,14 +11,14 @@ import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/typ import ListNoDataPlaceholder from '../../../_base/components/list-no-data-placeholder' import RemoveButton from '../../../_base/components/remove-button' -type Props = { +type Props = Readonly<{ readonly: boolean nodeId: string list: ValueSelector[] onChange: (list: ValueSelector[], value?: ValueSelector) => void onOpen?: (index: number) => void filterVar?: (payload: Var, valueSelector: ValueSelector) => boolean -} +}> const VarList: FC = ({ readonly, diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/array-bool-list.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/array-bool-list.tsx index 759c5cf8a23..da778851050 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/array-bool-list.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/array-bool-list.tsx @@ -10,11 +10,11 @@ import { useTranslation } from 'react-i18next' import RemoveButton from '@/app/components/workflow/nodes/_base/components/remove-button' import BoolValue from './bool-value' -type Props = { +type Props = Readonly<{ className?: string list: boolean[] onChange: (list: boolean[]) => void -} +}> const ArrayValueList: FC = ({ className, diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/array-value-list.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/array-value-list.tsx index 03e476d981f..32f28732bd8 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/array-value-list.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/array-value-list.tsx @@ -9,11 +9,11 @@ import { useTranslation } from 'react-i18next' import Input from '@/app/components/base/input' import RemoveButton from '@/app/components/workflow/nodes/_base/components/remove-button' -type Props = { +type Props = Readonly<{ isString: boolean list: any[] onChange: (list: any[]) => void -} +}> const ArrayValueList: FC = ({ isString = true, diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/bool-value.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/bool-value.tsx index 859a8bc7287..e75d48a8262 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/bool-value.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/bool-value.tsx @@ -4,10 +4,10 @@ import * as React from 'react' import { useCallback } from 'react' import OptionCard from '../../../nodes/_base/components/option-card' -type Props = { +type Props = Readonly<{ value: boolean onChange: (value: boolean) => void -} +}> const BoolValue: FC = ({ value, diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/object-value-item.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/object-value-item.tsx index a7d801ddcef..fa90a1c012d 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/object-value-item.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/object-value-item.tsx @@ -9,11 +9,11 @@ import ActionButton from '@/app/components/base/action-button' import VariableTypeSelector from '@/app/components/workflow/panel/chat-variable-panel/components/variable-type-select' import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type' -type Props = { +type Props = Readonly<{ index: number list: any[] onChange: (list: any[]) => void -} +}> const typeList = [ ChatVarType.String, diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/object-value-list.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/object-value-list.tsx index 1db0a559c37..58b7537dddb 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/object-value-list.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/object-value-list.tsx @@ -4,10 +4,10 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import ObjectValueItem from '@/app/components/workflow/panel/chat-variable-panel/components/object-value-item' -type Props = { +type Props = Readonly<{ list: any[] onChange: (list: any[]) => void -} +}> const ObjectValueList: FC = ({ list, diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal-trigger.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal-trigger.tsx index 4a0f79d2763..5544b6c3513 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal-trigger.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal-trigger.tsx @@ -7,14 +7,14 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import VariableModal from '@/app/components/workflow/panel/chat-variable-panel/components/variable-modal' -type Props = { +type Props = Readonly<{ open: boolean setOpen: (value: React.SetStateAction) => void showTip: boolean chatVar?: ConversationVariable onClose: () => void onSave: (env: ConversationVariable) => void -} +}> const VariableModalTrigger = ({ open, diff --git a/web/app/components/workflow/panel/debug-and-preview/conversation-variable-modal.tsx b/web/app/components/workflow/panel/debug-and-preview/conversation-variable-modal.tsx index 7000f1d64de..75a14e6c88c 100644 --- a/web/app/components/workflow/panel/debug-and-preview/conversation-variable-modal.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/conversation-variable-modal.tsx @@ -23,10 +23,10 @@ import { useStore } from '@/app/components/workflow/store' import useTimestamp from '@/hooks/use-timestamp' import { fetchCurrentValueOfConversationVariable } from '@/service/workflow' -type Props = { +type Props = Readonly<{ conversationID: string onHide: () => void -} +}> const ConversationVariableModal = ({ conversationID, diff --git a/web/app/components/workflow/panel/env-panel/variable-trigger.tsx b/web/app/components/workflow/panel/env-panel/variable-trigger.tsx index 600a86e09c6..162facc262d 100644 --- a/web/app/components/workflow/panel/env-panel/variable-trigger.tsx +++ b/web/app/components/workflow/panel/env-panel/variable-trigger.tsx @@ -7,13 +7,13 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import VariableModal from '@/app/components/workflow/panel/env-panel/variable-modal' -type Props = { +type Props = Readonly<{ open: boolean setOpen: (value: React.SetStateAction) => void env?: EnvironmentVariable onClose: () => void onSave: (env: EnvironmentVariable) => void -} +}> const VariableTrigger = ({ open, diff --git a/web/app/components/workflow/panel/global-variable-panel/item.tsx b/web/app/components/workflow/panel/global-variable-panel/item.tsx index 46cd160620b..e803570df95 100644 --- a/web/app/components/workflow/panel/global-variable-panel/item.tsx +++ b/web/app/components/workflow/panel/global-variable-panel/item.tsx @@ -5,9 +5,9 @@ import { capitalize } from 'es-toolkit/string' import { memo } from 'react' import { GlobalVariable as GlobalVariableIcon } from '@/app/components/base/icons/src/vender/line/others' -type Props = { +type Props = Readonly<{ payload: GlobalVariable -} +}> const Item = ({ payload, diff --git a/web/app/components/workflow/panel/inputs-panel.tsx b/web/app/components/workflow/panel/inputs-panel.tsx index 10004236dd8..6a40a755396 100644 --- a/web/app/components/workflow/panel/inputs-panel.tsx +++ b/web/app/components/workflow/panel/inputs-panel.tsx @@ -25,9 +25,9 @@ import { WorkflowRunningStatus, } from '../types' -type Props = { +type Props = Readonly<{ onRun: () => void -} +}> const InputsPanel = ({ onRun }: Props) => { const { t } = useTranslation() diff --git a/web/app/components/workflow/workflow-generator/example-prompts.tsx b/web/app/components/workflow/workflow-generator/example-prompts.tsx index e105f989115..ebe3bbb452b 100644 --- a/web/app/components/workflow/workflow-generator/example-prompts.tsx +++ b/web/app/components/workflow/workflow-generator/example-prompts.tsx @@ -3,10 +3,10 @@ import type { WorkflowGeneratorMode } from './types' import { memo, useMemo } from 'react' import { useTranslation } from 'react-i18next' -type Props = { +type Props = Readonly<{ mode: WorkflowGeneratorMode onSelect: (prompt: string) => void -} +}> /** * "Try one of these" chips that sit below the instruction textarea. diff --git a/web/app/components/workflow/workflow-generator/generation-phases.tsx b/web/app/components/workflow/workflow-generator/generation-phases.tsx index f2dd7c0c99e..6ff5b72e27a 100644 --- a/web/app/components/workflow/workflow-generator/generation-phases.tsx +++ b/web/app/components/workflow/workflow-generator/generation-phases.tsx @@ -17,7 +17,7 @@ import Loading from '@/app/components/base/loading' const PLANNING_MS = 3500 const BUILDING_MS = 12000 -type Props = { +type Props = Readonly<{ /** * Per-attempt nonce — typically ``Date.now()`` of when Generate was * clicked. The component resets ``phaseIndex`` whenever this changes so a @@ -25,7 +25,7 @@ type Props = { * resuming wherever the previous attempt left off. */ startedAt: number -} +}> const GenerationPhases = ({ startedAt }: Props) => { const { t } = useTranslation('workflow') From 08f1bf20abd193b130409f4f1d4c608e325096cd Mon Sep 17 00:00:00 2001 From: Rohit Gahlawat <283466839+Rohit-Gahlawat@users.noreply.github.com> Date: Thu, 11 Jun 2026 05:49:51 +0530 Subject: [PATCH 011/122] refactor(web): mark Props of app/annotation components as read-only (#25219) (#37299) --- .../app/annotation/add-annotation-modal/edit-item/index.tsx | 4 ++-- .../components/app/annotation/add-annotation-modal/index.tsx | 4 ++-- .../annotation/batch-add-annotation-modal/csv-uploader.tsx | 4 ++-- .../annotation/clear-all-annotations-confirm-modal/index.tsx | 4 ++-- .../app/annotation/edit-annotation-modal/edit-item/index.tsx | 4 ++-- .../components/app/annotation/edit-annotation-modal/index.tsx | 4 ++-- web/app/components/app/annotation/header-opts/index.tsx | 4 ++-- web/app/components/app/annotation/index.tsx | 4 ++-- web/app/components/app/annotation/list.tsx | 4 ++-- .../app/annotation/remove-annotation-confirm-modal/index.tsx | 4 ++-- .../components/app/annotation/view-annotation-modal/index.tsx | 4 ++-- 11 files changed, 22 insertions(+), 22 deletions(-) diff --git a/web/app/components/app/annotation/add-annotation-modal/edit-item/index.tsx b/web/app/components/app/annotation/add-annotation-modal/edit-item/index.tsx index 0b416ca644e..cabd8a0972d 100644 --- a/web/app/components/app/annotation/add-annotation-modal/edit-item/index.tsx +++ b/web/app/components/app/annotation/add-annotation-modal/edit-item/index.tsx @@ -9,11 +9,11 @@ export enum EditItemType { Query = 'query', Answer = 'answer', } -type Props = { +type Props = Readonly<{ type: EditItemType content: string onChange: (content: string) => void -} +}> const EditItem: FC = ({ type, diff --git a/web/app/components/app/annotation/add-annotation-modal/index.tsx b/web/app/components/app/annotation/add-annotation-modal/index.tsx index 4be9cc2146b..ac572707954 100644 --- a/web/app/components/app/annotation/add-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/add-annotation-modal/index.tsx @@ -21,11 +21,11 @@ import AnnotationFull from '@/app/components/billing/annotation-full' import { useProviderContext } from '@/context/provider-context' import EditItem, { EditItemType } from './edit-item' -type Props = { +type Props = Readonly<{ isShow: boolean onHide: () => void onAdd: (payload: AnnotationItemBasic) => void -} +}> const AddAnnotationModal: FC = ({ isShow, diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx index fc65c0dc19d..991ca8a98b8 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx @@ -9,10 +9,10 @@ import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { Csv as CSVIcon } from '@/app/components/base/icons/src/public/files' -export type Props = { +export type Props = Readonly<{ file: File | undefined updateFile: (file?: File) => void -} +}> const CSVUploader: FC = ({ file, diff --git a/web/app/components/app/annotation/clear-all-annotations-confirm-modal/index.tsx b/web/app/components/app/annotation/clear-all-annotations-confirm-modal/index.tsx index 6b0ef19e7b5..5d054bbfcc4 100644 --- a/web/app/components/app/annotation/clear-all-annotations-confirm-modal/index.tsx +++ b/web/app/components/app/annotation/clear-all-annotations-confirm-modal/index.tsx @@ -12,11 +12,11 @@ import { import * as React from 'react' import { useTranslation } from 'react-i18next' -type Props = { +type Props = Readonly<{ isShow: boolean onHide: () => void onConfirm: () => void -} +}> const ClearAllAnnotationsConfirmModal: FC = ({ isShow, diff --git a/web/app/components/app/annotation/edit-annotation-modal/edit-item/index.tsx b/web/app/components/app/annotation/edit-annotation-modal/edit-item/index.tsx index eb66c246d93..2797ad2aadb 100644 --- a/web/app/components/app/annotation/edit-annotation-modal/edit-item/index.tsx +++ b/web/app/components/app/annotation/edit-annotation-modal/edit-item/index.tsx @@ -13,12 +13,12 @@ export enum EditItemType { Query = 'query', Answer = 'answer', } -type Props = { +type Props = Readonly<{ type: EditItemType content: string readonly?: boolean onSave: (content: string) => Promise -} +}> export const EditTitle: FC<{ className?: string, title: string }> = ({ className, title }) => (
diff --git a/web/app/components/app/annotation/edit-annotation-modal/index.tsx b/web/app/components/app/annotation/edit-annotation-modal/index.tsx index a7df64722fb..7ba47312316 100644 --- a/web/app/components/app/annotation/edit-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/edit-annotation-modal/index.tsx @@ -29,7 +29,7 @@ import useTimestamp from '@/hooks/use-timestamp' import { addAnnotation, editAnnotation } from '@/service/annotation' import EditItem, { EditItemType } from './edit-item' -type Props = { +type Props = Readonly<{ isShow: boolean onHide: () => void appId: string @@ -42,7 +42,7 @@ type Props = { createdAt?: number onRemove: () => void onlyEditResponse?: boolean -} +}> const EditAnnotationModal: FC = ({ isShow, diff --git a/web/app/components/app/annotation/header-opts/index.tsx b/web/app/components/app/annotation/header-opts/index.tsx index ab1ed04ce0b..8cbc1048be3 100644 --- a/web/app/components/app/annotation/header-opts/index.tsx +++ b/web/app/components/app/annotation/header-opts/index.tsx @@ -29,12 +29,12 @@ import ClearAllAnnotationsConfirmModal from '../clear-all-annotations-confirm-mo const CSV_HEADER_QA_EN = ['Question', 'Answer'] const CSV_HEADER_QA_CN = ['问题', '答案'] -type Props = { +type Props = Readonly<{ appId: string onAdd: (payload: AnnotationItemBasic) => void onAdded: () => void controlUpdateList: number -} +}> type OperationsMenuProps = { list: AnnotationItemBasic[] diff --git a/web/app/components/app/annotation/index.tsx b/web/app/components/app/annotation/index.tsx index a8601d0a75d..d59b8eee465 100644 --- a/web/app/components/app/annotation/index.tsx +++ b/web/app/components/app/annotation/index.tsx @@ -30,9 +30,9 @@ import { List } from './list' import { AnnotationEnableStatus, JobStatus } from './type' import ViewAnnotationModal from './view-annotation-modal' -type Props = { +type Props = Readonly<{ appDetail: App -} +}> const Annotation: FC = (props) => { const { appDetail } = props diff --git a/web/app/components/app/annotation/list.tsx b/web/app/components/app/annotation/list.tsx index 96267158a68..b80c9ac4f76 100644 --- a/web/app/components/app/annotation/list.tsx +++ b/web/app/components/app/annotation/list.tsx @@ -9,14 +9,14 @@ import useTimestamp from '@/hooks/use-timestamp' import BatchAction from './batch-action' import RemoveAnnotationConfirmModal from './remove-annotation-confirm-modal' -type Props = { +type Props = Readonly<{ list: AnnotationItem[] onView: (item: AnnotationItem) => void onRemove: (id: string) => void selectedIds: string[] onSelectedIdsChange: (selectedIds: string[]) => void onBatchDelete: () => Promise -} +}> type AnnotationTableRowProps = { item: AnnotationItem diff --git a/web/app/components/app/annotation/remove-annotation-confirm-modal/index.tsx b/web/app/components/app/annotation/remove-annotation-confirm-modal/index.tsx index 864e098ca5c..3fc8bc4d6fd 100644 --- a/web/app/components/app/annotation/remove-annotation-confirm-modal/index.tsx +++ b/web/app/components/app/annotation/remove-annotation-confirm-modal/index.tsx @@ -11,11 +11,11 @@ import { import * as React from 'react' import { useTranslation } from 'react-i18next' -type Props = { +type Props = Readonly<{ isShow: boolean onHide: () => void onRemove: () => void -} +}> const RemoveAnnotationConfirmModal: FC = ({ isShow, diff --git a/web/app/components/app/annotation/view-annotation-modal/index.tsx b/web/app/components/app/annotation/view-annotation-modal/index.tsx index 839f4331015..1214f108e07 100644 --- a/web/app/components/app/annotation/view-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/view-annotation-modal/index.tsx @@ -33,14 +33,14 @@ import { fetchHitHistoryList } from '@/service/annotation' import EditItem, { EditItemType } from '../edit-annotation-modal/edit-item' import HitHistoryNoData from './hit-history-no-data' -type Props = { +type Props = Readonly<{ appId: string isShow: boolean onHide: () => void item: AnnotationItem onSave: (editedQuery: string, editedAnswer: string) => Promise onRemove: () => void -} +}> enum TabType { annotation = 'annotation', From 5ed663e7fd58ac548078cc2d468acc0e64b73a9b Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Thu, 11 Jun 2026 09:05:08 +0800 Subject: [PATCH 012/122] refactor: use foxact package for copied hooks (#37308) --- pnpm-lock.yaml | 39 ++- pnpm-workspace.yaml | 2 +- web/AGENTS.md | 2 +- .../education-verification-flow.test.tsx | 2 +- .../[appId]/overview/card-view.tsx | 2 +- .../[datasetId]/layout-main.tsx | 2 +- .../education-verify-action-recorder.spec.tsx | 2 +- .../app-info/use-app-info-actions.ts | 2 +- .../code-generator/get-code-generator-res.tsx | 2 +- .../app/create-app-dialog/app-list/index.tsx | 2 +- .../components/app/create-app-modal/index.tsx | 2 +- .../app/create-from-dsl-modal/index.tsx | 2 +- .../components/app/switch-app-modal/index.tsx | 2 +- web/app/components/apps/app-card.tsx | 2 +- web/app/components/apps/list.tsx | 2 +- .../base/chat/chat-with-history/hooks.tsx | 2 +- .../base/chat/embedded-chatbot/hooks.tsx | 2 +- .../copy-feedback/__tests__/index.spec.tsx | 2 +- .../components/base/copy-feedback/index.tsx | 2 +- .../base/copy-icon/__tests__/index.spec.tsx | 2 +- web/app/components/base/copy-icon/index.tsx | 2 +- .../input-with-copy/__tests__/index.spec.tsx | 2 +- .../components/base/input-with-copy/index.tsx | 2 +- .../billing/plan/__tests__/index.spec.tsx | 2 +- web/app/components/billing/plan/index.tsx | 2 +- .../education-verify-action-recorder.tsx | 2 +- .../header/account-dropdown/index.tsx | 2 +- web/app/components/header/header-wrapper.tsx | 2 +- .../components/header/maintenance-notice.tsx | 2 +- .../block-selector/featured-tools.tsx | 2 +- .../block-selector/featured-triggers.tsx | 2 +- .../rag-tool-recommendations/index.tsx | 2 +- .../_base/components/workflow-panel/index.tsx | 2 +- .../components/class-list.tsx | 2 +- .../components/workflow/note-node/hooks.ts | 2 +- web/app/components/workflow/operator/hooks.ts | 2 +- .../panel/debug-and-preview/index.tsx | 2 +- .../persistence/local-storage-bridge.tsx | 2 +- .../workflow/variable-inspect/index.tsx | 2 +- .../workflow/workflow-generator/index.tsx | 2 +- .../education-apply/education-apply-page.tsx | 2 +- web/app/education-apply/hooks.ts | 2 +- .../signin/components/mail-and-code-auth.tsx | 2 +- web/context/modal-context-provider.tsx | 2 +- web/context/modal-context.test.tsx | 2 +- web/context/provider-context-provider.tsx | 2 +- web/eslint.config.mjs | 9 +- web/hooks/noop.ts | 7 - web/hooks/use-clipboard.ts | 74 ----- web/hooks/use-import-dsl.ts | 2 +- .../__tests__/index.spec.tsx | 95 ------- web/hooks/use-local-storage/index.ts | 260 ------------------ ...what-you-are-doing-or-you-will-be-fired.ts | 44 --- web/hooks/use-typescript-happy-callback.ts | 10 - web/package.json | 2 +- web/vitest.setup.ts | 2 +- 56 files changed, 84 insertions(+), 550 deletions(-) delete mode 100644 web/hooks/noop.ts delete mode 100644 web/hooks/use-clipboard.ts delete mode 100644 web/hooks/use-local-storage/__tests__/index.spec.tsx delete mode 100644 web/hooks/use-local-storage/index.ts delete mode 100644 web/hooks/use-stable-handler-only-when-you-know-what-you-are-doing-or-you-will-be-fired.ts delete mode 100644 web/hooks/use-typescript-happy-callback.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 338fd6b2e7c..173a983eca6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -264,9 +264,6 @@ catalogs: cli-table3: specifier: 0.6.5 version: 0.6.5 - client-only: - specifier: 0.0.1 - version: 0.0.1 clsx: specifier: 2.1.1 version: 2.1.1 @@ -348,6 +345,9 @@ catalogs: fast-deep-equal: specifier: 3.1.3 version: 3.1.3 + foxact: + specifier: 0.3.4 + version: 0.3.4 fuse.js: specifier: 7.4.2 version: 7.4.2 @@ -1089,9 +1089,6 @@ importers: class-variance-authority: specifier: 'catalog:' version: 0.7.1 - client-only: - specifier: 'catalog:' - version: 0.0.1 cmdk: specifier: 'catalog:' version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -1134,6 +1131,9 @@ importers: fast-deep-equal: specifier: 'catalog:' version: 3.1.3 + foxact: + specifier: 'catalog:' + version: 0.3.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7) fuse.js: specifier: 'catalog:' version: 7.4.2 @@ -6358,6 +6358,9 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + event-target-bus@1.0.0: + resolution: {integrity: sha512-uPcWKbj/BJU3Tbw9XqhHqET4/LBOhvv3/SJWr7NksxA6TC5YqBpaZgawE9R+WpYFCBFSAE4Vun+xQS6w4ABdlA==} + eventsource-parser@3.1.0: resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} engines: {node: '>=18.0.0'} @@ -6463,6 +6466,17 @@ packages: engines: {node: '>=18.3.0'} hasBin: true + foxact@0.3.4: + resolution: {integrity: sha512-6ENWStzEp/VczVgwBdn8scZUGccko2kLGho8Qg1VyUTG6tpSW1vz2xX/dRtAUjPEFbu618DKsjj/YL7iMU8mlA==} + peerDependencies: + react: '*' + react-dom: '*' + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -14740,6 +14754,8 @@ snapshots: esutils@2.0.3: {} + event-target-bus@1.0.0: {} + eventsource-parser@3.1.0: {} expand-template@2.0.3: @@ -14843,6 +14859,15 @@ snapshots: dependencies: fd-package-json: 2.0.0 + foxact@0.3.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + client-only: 0.0.1 + event-target-bus: 1.0.0 + server-only: 0.0.1 + optionalDependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + fs-constants@1.0.0: optional: true @@ -18169,7 +18194,6 @@ time: chokidar@5.0.0: '2025-11-25T23:28:06.854Z' class-variance-authority@0.7.1: '2024-11-26T08:20:34.604Z' cli-table3@0.6.5: '2024-05-12T16:36:50.079Z' - client-only@0.0.1: '2022-09-03T01:07:11.981Z' clsx@2.1.1: '2024-04-23T05:26:04.645Z' cmdk@1.1.1: '2025-03-14T19:21:16.194Z' code-inspector-plugin@1.5.2: '2026-06-07T02:32:04.545Z' @@ -18197,6 +18221,7 @@ time: eslint@10.4.1: '2026-05-29T20:32:32.193Z' eventsource-parser@3.1.0: '2026-05-27T20:55:51.466Z' fast-deep-equal@3.1.3: '2020-06-08T07:27:28.474Z' + foxact@0.3.4: '2026-05-15T12:55:52.584Z' fuse.js@7.4.2: '2026-06-05T22:22:52.388Z' happy-dom@20.10.2: '2026-06-06T15:03:11.325Z' hast-util-to-jsx-runtime@2.3.6: '2025-03-05T11:30:29.166Z' diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4a9100a24e5..8bd3056bf23 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -131,7 +131,6 @@ catalog: chokidar: 5.0.0 class-variance-authority: 0.7.1 cli-table3: 0.6.5 - client-only: 0.0.1 clsx: 2.1.1 cmdk: 1.1.1 code-inspector-plugin: 1.5.2 @@ -159,6 +158,7 @@ catalog: eslint-plugin-storybook: 10.4.2 eventsource-parser: 3.1.0 fast-deep-equal: 3.1.3 + foxact: 0.3.4 fuse.js: 7.4.2 happy-dom: 20.10.2 hast-util-to-jsx-runtime: 2.3.6 diff --git a/web/AGENTS.md b/web/AGENTS.md index 8dc91be1a2d..21def787b9e 100644 --- a/web/AGENTS.md +++ b/web/AGENTS.md @@ -33,7 +33,7 @@ - Use local component state for state owned by one component. - Use feature-level Jotai atoms for simple client state shared across components in the same feature, especially when components need a shared source of truth, derived values, or shared actions. - Use existing feature stores for complex or high-frequency interaction state such as workflow canvas, drag, resize, and panel runtime state. -- Use `@/hooks/use-local-storage` only for low-frequency, client-only persistence such as user preferences, dismissed notices, and UI defaults. Do not use localStorage as the live source of truth for app state. +- Use `foxact/use-local-storage` only for low-frequency, client-only persistence such as user preferences, dismissed notices, and UI defaults. Do not use localStorage as the live source of truth for app state. - For high-frequency interactions, update the feature state during interaction and persist storage only on commit or settled updates. - Do not access `localStorage`, `window.localStorage`, or `globalThis.localStorage` directly in app code; use the storage hook boundary and preserve existing raw/custom storage formats. - Do not add ad hoc global event listeners for shared state. Prefer atoms, existing stores, or a shared subscription hook so listeners are centralized and deduplicated. diff --git a/web/__tests__/billing/education-verification-flow.test.tsx b/web/__tests__/billing/education-verification-flow.test.tsx index 6c7f05c4df7..83885b9f9f9 100644 --- a/web/__tests__/billing/education-verification-flow.test.tsx +++ b/web/__tests__/billing/education-verification-flow.test.tsx @@ -77,7 +77,7 @@ vi.mock('@/hooks/use-async-window-open', () => ({ useAsyncWindowOpen: () => vi.fn(), })) -vi.mock('@/hooks/use-local-storage', () => ({ +vi.mock('foxact/use-local-storage', () => ({ useSetLocalStorage: () => mockSetEducationVerifying, })) 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 3e73e210eb4..06465e3551a 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx @@ -6,6 +6,7 @@ import type { UpdateAppSiteCodeResponse } from '@/models/app' import type { App } from '@/types/app' import type { I18nKeysByPrefix } from '@/types/i18n' import { toast } from '@langgenius/dify-ui/toast' +import { useSetLocalStorage } from 'foxact/use-local-storage' import * as React from 'react' import { useCallback, useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -18,7 +19,6 @@ import { collaborationManager } from '@/app/components/workflow/collaboration/co import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager' import { isTriggerNode } from '@/app/components/workflow/types' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { fetchAppDetail, updateAppSiteAccessToken, diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx index c455cf7ab3d..e725ac5e5f2 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx @@ -11,6 +11,7 @@ import { RiFocus2Fill, RiFocus2Line, } from '@remixicon/react' +import { useLocalStorage } from 'foxact/use-local-storage' import * as React from 'react' import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -24,7 +25,6 @@ import DatasetDetailContext from '@/context/dataset-detail' import { useEventEmitterContextContext } from '@/context/event-emitter' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useDocumentTitle from '@/hooks/use-document-title' -import { useLocalStorage } from '@/hooks/use-local-storage' import { usePathname, useRouter } from '@/next/navigation' import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset' diff --git a/web/app/components/__tests__/education-verify-action-recorder.spec.tsx b/web/app/components/__tests__/education-verify-action-recorder.spec.tsx index 742fb9c2493..d93dc90c2ac 100644 --- a/web/app/components/__tests__/education-verify-action-recorder.spec.tsx +++ b/web/app/components/__tests__/education-verify-action-recorder.spec.tsx @@ -12,7 +12,7 @@ vi.mock('@/next/navigation', () => ({ useSearchParams: vi.fn(), })) -vi.mock('@/hooks/use-local-storage', () => ({ +vi.mock('foxact/use-local-storage', () => ({ useSetLocalStorage: () => setEducationVerifyingMock, })) diff --git a/web/app/components/app-sidebar/app-info/use-app-info-actions.ts b/web/app/components/app-sidebar/app-info/use-app-info-actions.ts index 289af673712..6162869e1e7 100644 --- a/web/app/components/app-sidebar/app-info/use-app-info-actions.ts +++ b/web/app/components/app-sidebar/app-info/use-app-info-actions.ts @@ -3,12 +3,12 @@ import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-moda import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal' import type { EnvironmentVariable } from '@/app/components/workflow/types' import { toast } from '@langgenius/dify-ui/toast' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useStore as useAppStore } from '@/app/components/app/store' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useProviderContext } from '@/context/provider-context' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { useRouter } from '@/next/navigation' import { copyApp, deleteApp, exportAppConfig, fetchAppDetail, updateAppInfo } from '@/service/apps' import { useInvalidateAppList } from '@/service/use-apps' diff --git a/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx b/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx index ccc2bd4ead3..dd7fd42dc8d 100644 --- a/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx +++ b/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx @@ -19,6 +19,7 @@ import { useBoolean, useSessionStorageState, } from 'ahooks' +import { useLocalStorage } from 'foxact/use-local-storage' import * as React from 'react' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -27,7 +28,6 @@ import Loading from '@/app/components/base/loading' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' -import { useLocalStorage } from '@/hooks/use-local-storage' import { generateRule } from '@/service/debug' import { useGenerateRuleTemplate } from '@/service/use-apps' import { languageMap } from '../../../../workflow/nodes/_base/components/editor/code-editor/index' diff --git a/web/app/components/app/create-app-dialog/app-list/index.tsx b/web/app/components/app/create-app-dialog/app-list/index.tsx index 51ce7c076fc..ba0ba8e1b49 100644 --- a/web/app/components/app/create-app-dialog/app-list/index.tsx +++ b/web/app/components/app/create-app-dialog/app-list/index.tsx @@ -6,6 +6,7 @@ import { cn } from '@langgenius/dify-ui/cn' import { toast } from '@langgenius/dify-ui/toast' import { RiRobot2Line } from '@remixicon/react' import { useDebounceFn } from 'ahooks' +import { useSetLocalStorage } from 'foxact/use-local-storage' import * as React from 'react' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -17,7 +18,6 @@ import CreateAppModal from '@/app/components/explore/create-app-modal' import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useAppContext } from '@/context/app-context' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { DSLImportMode } from '@/models/app' import { useRouter } from '@/next/navigation' import { importDSL } from '@/service/apps' diff --git a/web/app/components/app/create-app-modal/index.tsx b/web/app/components/app/create-app-modal/index.tsx index 0d29c4d5992..0628e66e166 100644 --- a/web/app/components/app/create-app-modal/index.tsx +++ b/web/app/components/app/create-app-modal/index.tsx @@ -10,6 +10,7 @@ import { toast } from '@langgenius/dify-ui/toast' import { RiArrowRightLine, RiArrowRightSLine, RiExchange2Fill } from '@remixicon/react' import { formatForDisplay, useHotkey } from '@tanstack/react-hotkeys' import { useDebounceFn } from 'ahooks' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' @@ -20,7 +21,6 @@ import AppsFull from '@/app/components/billing/apps-full-in-dialog' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useAppContext } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import useTheme from '@/hooks/use-theme' import { useRouter } from '@/next/navigation' import { createApp } from '@/service/apps' diff --git a/web/app/components/app/create-from-dsl-modal/index.tsx b/web/app/components/app/create-from-dsl-modal/index.tsx index 72eac7ef797..bbe4e114d01 100644 --- a/web/app/components/app/create-from-dsl-modal/index.tsx +++ b/web/app/components/app/create-from-dsl-modal/index.tsx @@ -8,6 +8,7 @@ import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd' import { toast } from '@langgenius/dify-ui/toast' import { formatForDisplay, useHotkey } from '@tanstack/react-hotkeys' import { useDebounceFn } from 'ahooks' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import Input from '@/app/components/base/input' @@ -16,7 +17,6 @@ import { usePluginDependencies } from '@/app/components/workflow/plugin-dependen import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useAppContext } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { DSLImportMode, DSLImportStatus, diff --git a/web/app/components/app/switch-app-modal/index.tsx b/web/app/components/app/switch-app-modal/index.tsx index f94f6735bff..0c794025062 100644 --- a/web/app/components/app/switch-app-modal/index.tsx +++ b/web/app/components/app/switch-app-modal/index.tsx @@ -16,6 +16,7 @@ import { cn } from '@langgenius/dify-ui/cn' import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' import { toast } from '@langgenius/dify-ui/toast' import { RiCloseLine } from '@remixicon/react' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useStore as useAppStore } from '@/app/components/app/store' @@ -26,7 +27,6 @@ import AppsFull from '@/app/components/billing/apps-full-in-dialog' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useAppContext } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { useRouter } from '@/next/navigation' import { deleteApp, switchApp } from '@/service/apps' import { AppModeEnum } from '@/types/app' diff --git a/web/app/components/apps/app-card.tsx b/web/app/components/apps/app-card.tsx index 7fe19ac4e13..c6e5ecb7330 100644 --- a/web/app/components/apps/app-card.tsx +++ b/web/app/components/apps/app-card.tsx @@ -30,6 +30,7 @@ import { TooltipTrigger, } from '@langgenius/dify-ui/tooltip' import { useSuspenseQuery } from '@tanstack/react-query' +import { useSetLocalStorage } from 'foxact/use-local-storage' import * as React from 'react' import { useCallback, useMemo, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' @@ -42,7 +43,6 @@ import { useProviderContext } from '@/context/provider-context' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { AppCardTags } from '@/features/tag-management/components/app-card-tags' import { useAsyncWindowOpen } from '@/hooks/use-async-window-open' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { AccessMode } from '@/models/access-control' import dynamic from '@/next/dynamic' import { useRouter } from '@/next/navigation' diff --git a/web/app/components/apps/list.tsx b/web/app/components/apps/list.tsx index 321c104462e..84be0873339 100644 --- a/web/app/components/apps/list.tsx +++ b/web/app/components/apps/list.tsx @@ -4,6 +4,7 @@ import type { AppListQuery } from '@/contract/console/apps' import { cn } from '@langgenius/dify-ui/cn' import { keepPreviousData, useInfiniteQuery, useSuspenseQuery } from '@tanstack/react-query' import { useDebounce } from 'ahooks' +import { useLocalStorage } from 'foxact/use-local-storage' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { SearchInput } from '@/app/components/base/search-input' @@ -11,7 +12,6 @@ import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useAppContext } from '@/context/app-context' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { TagFilter } from '@/features/tag-management/components/tag-filter' -import { useLocalStorage } from '@/hooks/use-local-storage' import { CheckModal } from '@/hooks/use-pay' import dynamic from '@/next/dynamic' import Link from '@/next/link' diff --git a/web/app/components/base/chat/chat-with-history/hooks.tsx b/web/app/components/base/chat/chat-with-history/hooks.tsx index 765a32ba69d..c2936a71ee5 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -5,6 +5,7 @@ import type { AppData, ConversationItem } from '@/models/share' import type { HumanInputFilledFormData, HumanInputFormData } from '@/types/workflow' import { toast } from '@langgenius/dify-ui/toast' import { noop } from 'es-toolkit/function' +import { useLocalStorage } from 'foxact/use-local-storage' import { produce } from 'immer' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -12,7 +13,6 @@ import { getProcessedFilesFromResponse } from '@/app/components/base/file-upload import { InputVarType } from '@/app/components/workflow/types' import { useWebAppStore } from '@/context/web-app-context' import { useAppFavicon } from '@/hooks/use-app-favicon' -import { useLocalStorage } from '@/hooks/use-local-storage' import { changeLanguage } from '@/i18n-config/client' import { AppSourceType, delConversation, pinConversation, renameConversation, unpinConversation, updateFeedback } from '@/service/share' import { useInvalidateShareConversations, useShareChatList, useShareConversationName, useShareConversations } from '@/service/use-share' diff --git a/web/app/components/base/chat/embedded-chatbot/hooks.tsx b/web/app/components/base/chat/embedded-chatbot/hooks.tsx index 948be4ae3f4..b9c2df19b3d 100644 --- a/web/app/components/base/chat/embedded-chatbot/hooks.tsx +++ b/web/app/components/base/chat/embedded-chatbot/hooks.tsx @@ -5,13 +5,13 @@ import type { Locale } from '@/i18n-config' import type { AppData, ConversationItem } from '@/models/share' import { toast } from '@langgenius/dify-ui/toast' import { noop } from 'es-toolkit/function' +import { useLocalStorage } from 'foxact/use-local-storage' import { produce } from 'immer' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils' import { InputVarType } from '@/app/components/workflow/types' import { useWebAppStore } from '@/context/web-app-context' -import { useLocalStorage } from '@/hooks/use-local-storage' import { changeLanguage } from '@/i18n-config/client' import { AppSourceType, updateFeedback } from '@/service/share' import { useInvalidateShareConversations, useShareChatList, useShareConversationName, useShareConversations } from '@/service/use-share' diff --git a/web/app/components/base/copy-feedback/__tests__/index.spec.tsx b/web/app/components/base/copy-feedback/__tests__/index.spec.tsx index b2484031dbf..f92925ae89d 100644 --- a/web/app/components/base/copy-feedback/__tests__/index.spec.tsx +++ b/web/app/components/base/copy-feedback/__tests__/index.spec.tsx @@ -5,7 +5,7 @@ const mockCopy = vi.fn() const mockReset = vi.fn() let mockCopied = false -vi.mock('@/hooks/use-clipboard', () => ({ +vi.mock('foxact/use-clipboard', () => ({ useClipboard: () => ({ copy: mockCopy, reset: mockReset, diff --git a/web/app/components/base/copy-feedback/index.tsx b/web/app/components/base/copy-feedback/index.tsx index 12b70b3c0e2..8415d0c433e 100644 --- a/web/app/components/base/copy-feedback/index.tsx +++ b/web/app/components/base/copy-feedback/index.tsx @@ -4,10 +4,10 @@ import { RiClipboardFill, RiClipboardLine, } from '@remixicon/react' +import { useClipboard } from 'foxact/use-clipboard' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' -import { useClipboard } from '@/hooks/use-clipboard' import copyStyle from './style.module.css' type Props = Readonly<{ diff --git a/web/app/components/base/copy-icon/__tests__/index.spec.tsx b/web/app/components/base/copy-icon/__tests__/index.spec.tsx index 28044a20d03..568d6c92843 100644 --- a/web/app/components/base/copy-icon/__tests__/index.spec.tsx +++ b/web/app/components/base/copy-icon/__tests__/index.spec.tsx @@ -5,7 +5,7 @@ const copy = vi.fn() const reset = vi.fn() let copied = false -vi.mock('@/hooks/use-clipboard', () => ({ +vi.mock('foxact/use-clipboard', () => ({ useClipboard: () => ({ copy, reset, diff --git a/web/app/components/base/copy-icon/index.tsx b/web/app/components/base/copy-icon/index.tsx index 6c6fa987a0e..5ed40bc1fdd 100644 --- a/web/app/components/base/copy-icon/index.tsx +++ b/web/app/components/base/copy-icon/index.tsx @@ -1,8 +1,8 @@ 'use client' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' +import { useClipboard } from 'foxact/use-clipboard' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' -import { useClipboard } from '@/hooks/use-clipboard' type Props = Readonly<{ content: string diff --git a/web/app/components/base/input-with-copy/__tests__/index.spec.tsx b/web/app/components/base/input-with-copy/__tests__/index.spec.tsx index b985a0f018f..2b9d64a39ac 100644 --- a/web/app/components/base/input-with-copy/__tests__/index.spec.tsx +++ b/web/app/components/base/input-with-copy/__tests__/index.spec.tsx @@ -6,7 +6,7 @@ const mockCopy = vi.fn() let mockCopied = false const mockReset = vi.fn() -vi.mock('@/hooks/use-clipboard', () => ({ +vi.mock('foxact/use-clipboard', () => ({ useClipboard: () => ({ copy: mockCopy, copied: mockCopied, diff --git a/web/app/components/base/input-with-copy/index.tsx b/web/app/components/base/input-with-copy/index.tsx index e401b7bdb08..6f6682bcee3 100644 --- a/web/app/components/base/input-with-copy/index.tsx +++ b/web/app/components/base/input-with-copy/index.tsx @@ -2,9 +2,9 @@ import type { InputProps } from '../input' import { cn } from '@langgenius/dify-ui/cn' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' +import { useClipboard } from 'foxact/use-clipboard' import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useClipboard } from '@/hooks/use-clipboard' import ActionButton from '../action-button' type InputWithCopyProps = { diff --git a/web/app/components/billing/plan/__tests__/index.spec.tsx b/web/app/components/billing/plan/__tests__/index.spec.tsx index b433d5df274..5038f57f882 100644 --- a/web/app/components/billing/plan/__tests__/index.spec.tsx +++ b/web/app/components/billing/plan/__tests__/index.spec.tsx @@ -36,7 +36,7 @@ vi.mock('@/context/app-context', () => ({ }), })) -vi.mock('@/hooks/use-local-storage', () => ({ +vi.mock('foxact/use-local-storage', () => ({ useSetLocalStorage: () => setEducationVerifyingMock, })) diff --git a/web/app/components/billing/plan/index.tsx b/web/app/components/billing/plan/index.tsx index 55d4a714d2e..e76925abbf9 100644 --- a/web/app/components/billing/plan/index.tsx +++ b/web/app/components/billing/plan/index.tsx @@ -8,6 +8,7 @@ import { RiGroupLine, } from '@remixicon/react' import { useUnmountedRef } from 'ahooks' +import { useSetLocalStorage } from 'foxact/use-local-storage' import * as React from 'react' import { useEffect } from 'react' import { useTranslation } from 'react-i18next' @@ -18,7 +19,6 @@ import VerifyStateModal from '@/app/education-apply/verify-state-modal' import { useAppContext } from '@/context/app-context' import { useModalContextSelector } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { usePathname, useRouter } from '@/next/navigation' import { useEducationVerify } from '@/service/use-education' import { getDaysUntilEndOfMonth } from '@/utils/time' diff --git a/web/app/components/education-verify-action-recorder.tsx b/web/app/components/education-verify-action-recorder.tsx index 3e26e55dc77..ece0569c7e2 100644 --- a/web/app/components/education-verify-action-recorder.tsx +++ b/web/app/components/education-verify-action-recorder.tsx @@ -1,11 +1,11 @@ 'use client' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { useEffect } from 'react' import { EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION, EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, } from '@/app/education-apply/constants' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { useSearchParams } from '@/next/navigation' export function EducationVerifyActionRecorder() { diff --git a/web/app/components/header/account-dropdown/index.tsx b/web/app/components/header/account-dropdown/index.tsx index 896f7416ce4..0c2148ed884 100644 --- a/web/app/components/header/account-dropdown/index.tsx +++ b/web/app/components/header/account-dropdown/index.tsx @@ -5,6 +5,7 @@ import { Avatar } from '@langgenius/dify-ui/avatar' import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLinkItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@langgenius/dify-ui/dropdown-menu' import { StatusDot } from '@langgenius/dify-ui/status-dot' import { useSuspenseQuery } from '@tanstack/react-query' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { resetUser } from '@/app/components/base/amplitude/utils' @@ -18,7 +19,6 @@ import { useModalContext } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' import { env } from '@/env' import { systemFeaturesQueryOptions } from '@/features/system-features/client' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import Link from '@/next/link' import { useRouter } from '@/next/navigation' import { useLogout } from '@/service/use-common' diff --git a/web/app/components/header/header-wrapper.tsx b/web/app/components/header/header-wrapper.tsx index 4c5c8e657e9..5f35a95524a 100644 --- a/web/app/components/header/header-wrapper.tsx +++ b/web/app/components/header/header-wrapper.tsx @@ -1,10 +1,10 @@ 'use client' import type { EventEmitterValue } from '@/context/event-emitter' import { cn } from '@langgenius/dify-ui/cn' +import { useLocalStorage } from 'foxact/use-local-storage' import * as React from 'react' import { useState } from 'react' import { useEventEmitterContextContext } from '@/context/event-emitter' -import { useLocalStorage } from '@/hooks/use-local-storage' import { usePathname } from '@/next/navigation' import s from './index.module.css' diff --git a/web/app/components/header/maintenance-notice.tsx b/web/app/components/header/maintenance-notice.tsx index 559402aa12f..d3a6481f1a5 100644 --- a/web/app/components/header/maintenance-notice.tsx +++ b/web/app/components/header/maintenance-notice.tsx @@ -1,8 +1,8 @@ +import { useLocalStorage } from 'foxact/use-local-storage' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { X } from '@/app/components/base/icons/src/vender/line/general' import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' -import { useLocalStorage } from '@/hooks/use-local-storage' import { NOTICE_I18N } from '@/i18n-config/language' const MaintenanceNotice = () => { diff --git a/web/app/components/workflow/block-selector/featured-tools.tsx b/web/app/components/workflow/block-selector/featured-tools.tsx index 82a8637cd20..8a0463e915f 100644 --- a/web/app/components/workflow/block-selector/featured-tools.tsx +++ b/web/app/components/workflow/block-selector/featured-tools.tsx @@ -6,6 +6,7 @@ import type { Plugin } from '@/app/components/plugins/types' import type { Locale } from '@/i18n-config' import { createPreviewCardHandle, PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card' import { RiMoreLine } from '@remixicon/react' +import { useLocalStorage } from 'foxact/use-local-storage' import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { ArrowDownDoubleLine, ArrowDownRoundFill, ArrowUpDoubleLine } from '@/app/components/base/icons/src/vender/solid/arrows' @@ -13,7 +14,6 @@ import Loading from '@/app/components/base/loading' import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace' import Action from '@/app/components/workflow/block-selector/market-place-plugin/action' import { useGetLanguage } from '@/context/i18n' -import { useLocalStorage } from '@/hooks/use-local-storage' import Link from '@/next/link' import { formatNumber } from '@/utils/format' import { getMarketplaceUrl } from '@/utils/var' diff --git a/web/app/components/workflow/block-selector/featured-triggers.tsx b/web/app/components/workflow/block-selector/featured-triggers.tsx index 8dec59d8cc8..13efb38c8f2 100644 --- a/web/app/components/workflow/block-selector/featured-triggers.tsx +++ b/web/app/components/workflow/block-selector/featured-triggers.tsx @@ -6,6 +6,7 @@ import type { Plugin } from '@/app/components/plugins/types' import type { Locale } from '@/i18n-config' import { createPreviewCardHandle, PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card' import { RiMoreLine } from '@remixicon/react' +import { useLocalStorage } from 'foxact/use-local-storage' import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { ArrowDownDoubleLine, ArrowDownRoundFill, ArrowUpDoubleLine } from '@/app/components/base/icons/src/vender/solid/arrows' @@ -13,7 +14,6 @@ import Loading from '@/app/components/base/loading' import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace' import Action from '@/app/components/workflow/block-selector/market-place-plugin/action' import { useGetLanguage } from '@/context/i18n' -import { useLocalStorage } from '@/hooks/use-local-storage' import Link from '@/next/link' import { formatNumber } from '@/utils/format' import { getMarketplaceUrl } from '@/utils/var' diff --git a/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx b/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx index 2bf0b51dba9..f7def41acd5 100644 --- a/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx +++ b/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx @@ -3,13 +3,13 @@ import type { Dispatch, SetStateAction } from 'react' import type { ViewType } from '@/app/components/workflow/block-selector/view-type-select' import type { OnSelectBlock } from '@/app/components/workflow/types' import { RiMoreLine } from '@remixicon/react' +import { useLocalStorage } from 'foxact/use-local-storage' import * as React from 'react' import { useCallback, useMemo } from 'react' import { Trans, useTranslation } from 'react-i18next' import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/arrows' import Loading from '@/app/components/base/loading' import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils' -import { useLocalStorage } from '@/hooks/use-local-storage' import Link from '@/next/link' import { useRAGRecommendedPlugins } from '@/service/use-tools' import { getMarketplaceUrl } from '@/utils/var' diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx index 6abec226ce2..c73361e35c3 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -13,6 +13,7 @@ import { RiPlayLargeLine, } from '@remixicon/react' import { debounce } from 'es-toolkit/compat' +import { useSetLocalStorage } from 'foxact/use-local-storage' import * as React from 'react' import { cloneElement, @@ -68,7 +69,6 @@ import { } from '@/app/components/workflow/utils' import { useAppContext } from '@/context/app-context' import { useModalContext } from '@/context/modal-context' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { useAllBuiltInTools } from '@/service/use-tools' import { useAllTriggerPlugins } from '@/service/use-triggers' import { FlowType } from '@/types/common' diff --git a/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx b/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx index e8a809b5606..672536aabed 100644 --- a/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx +++ b/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx @@ -5,13 +5,13 @@ import type { ValueSelector, Var } from '@/app/components/workflow/types' import { cn } from '@langgenius/dify-ui/cn' import { RiDraggable } from '@remixicon/react' import { noop } from 'es-toolkit/function' +import { useLocalStorage } from 'foxact/use-local-storage' import { produce } from 'immer' import * as React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { ReactSortable } from 'react-sortablejs' import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general' -import { useLocalStorage } from '@/hooks/use-local-storage' import { useEdgesInteractions } from '../../../hooks' import AddButton from '../../_base/components/add-button' import Item from './class-item' diff --git a/web/app/components/workflow/note-node/hooks.ts b/web/app/components/workflow/note-node/hooks.ts index 82d13e09bca..8a9e3573eba 100644 --- a/web/app/components/workflow/note-node/hooks.ts +++ b/web/app/components/workflow/note-node/hooks.ts @@ -1,7 +1,7 @@ import type { EditorState } from 'lexical' import type { NoteTheme } from './types' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { useCallback } from 'react' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { useNodeDataUpdate, useWorkflowHistory, WorkflowHistoryEvent } from '../hooks' import { NOTE_SHOW_AUTHOR_STORAGE_KEY } from './constants' diff --git a/web/app/components/workflow/operator/hooks.ts b/web/app/components/workflow/operator/hooks.ts index adf2befd800..4330920d501 100644 --- a/web/app/components/workflow/operator/hooks.ts +++ b/web/app/components/workflow/operator/hooks.ts @@ -1,7 +1,7 @@ import type { NoteNodeType } from '../note-node/types' +import { useLocalStorage } from 'foxact/use-local-storage' import { useCallback } from 'react' import { useAppContext } from '@/context/app-context' -import { useLocalStorage } from '@/hooks/use-local-storage' import { CUSTOM_NOTE_NODE, NOTE_SHOW_AUTHOR_STORAGE_KEY, diff --git a/web/app/components/workflow/panel/debug-and-preview/index.tsx b/web/app/components/workflow/panel/debug-and-preview/index.tsx index 6e246df9e11..3f0576d8703 100644 --- a/web/app/components/workflow/panel/debug-and-preview/index.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/index.tsx @@ -5,6 +5,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/too import { RiCloseLine, RiEqualizer2Line } from '@remixicon/react' import { debounce } from 'es-toolkit/compat' import { noop } from 'es-toolkit/function' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { memo, useCallback, @@ -19,7 +20,6 @@ import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows import { useEdgesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-edges-interactions-without-sync' import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync' import { useStore } from '@/app/components/workflow/store' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { useWorkflowInteractions, } from '../../hooks' diff --git a/web/app/components/workflow/persistence/local-storage-bridge.tsx b/web/app/components/workflow/persistence/local-storage-bridge.tsx index 07eac23396d..437917162e1 100644 --- a/web/app/components/workflow/persistence/local-storage-bridge.tsx +++ b/web/app/components/workflow/persistence/local-storage-bridge.tsx @@ -1,5 +1,5 @@ +import { useLocalStorage, useSetLocalStorage } from 'foxact/use-local-storage' import { useEffect, useLayoutEffect as useLayoutEffectFromReact } from 'react' -import { useLocalStorage, useSetLocalStorage } from '@/hooks/use-local-storage' import { useStore, useWorkflowStore } from '../store' import { isControlMode, diff --git a/web/app/components/workflow/variable-inspect/index.tsx b/web/app/components/workflow/variable-inspect/index.tsx index 828c992d9d7..924ad7d07c0 100644 --- a/web/app/components/workflow/variable-inspect/index.tsx +++ b/web/app/components/workflow/variable-inspect/index.tsx @@ -1,11 +1,11 @@ import type { FC } from 'react' import { cn } from '@langgenius/dify-ui/cn' import { debounce } from 'es-toolkit/compat' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { useCallback, useMemo, } from 'react' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { useResizePanel } from '../nodes/_base/hooks/use-resize-panel' import { useStore } from '../store' import Panel from './panel' diff --git a/web/app/components/workflow/workflow-generator/index.tsx b/web/app/components/workflow/workflow-generator/index.tsx index b7c2bd95f3c..cd0128c2797 100644 --- a/web/app/components/workflow/workflow-generator/index.tsx +++ b/web/app/components/workflow/workflow-generator/index.tsx @@ -16,6 +16,7 @@ import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' import { Textarea } from '@langgenius/dify-ui/textarea' import { toast } from '@langgenius/dify-ui/toast' import { useBoolean } from 'ahooks' +import { useLocalStorage } from 'foxact/use-local-storage' import * as React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -26,7 +27,6 @@ import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/com import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' import WorkflowPreview from '@/app/components/workflow/workflow-preview' import { useAppContext } from '@/context/app-context' -import { useLocalStorage } from '@/hooks/use-local-storage' import { useRouter } from '@/next/navigation' import { generateWorkflow } from '@/service/debug' import { fetchWorkflowDraft } from '@/service/workflow' diff --git a/web/app/education-apply/education-apply-page.tsx b/web/app/education-apply/education-apply-page.tsx index 4e5aedc68c5..4c5c310fac3 100644 --- a/web/app/education-apply/education-apply-page.tsx +++ b/web/app/education-apply/education-apply-page.tsx @@ -8,6 +8,7 @@ import { Checkbox } from '@langgenius/dify-ui/checkbox' import { toast } from '@langgenius/dify-ui/toast' import { useQueryClient } from '@tanstack/react-query' import { noop } from 'es-toolkit/function' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import { useEducationDiscount } from '@/app/components/billing/hooks/use-education-discount' @@ -19,7 +20,6 @@ import { useProviderContext } from '@/context/provider-context' import { useWorkspacesContext } from '@/context/workspace-context' import { WorkspaceProvider } from '@/context/workspace-context-provider' import { useAsyncWindowOpen } from '@/hooks/use-async-window-open' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { useRouter, useSearchParams, diff --git a/web/app/education-apply/hooks.ts b/web/app/education-apply/hooks.ts index 764ab1a0904..ae216feebc2 100644 --- a/web/app/education-apply/hooks.ts +++ b/web/app/education-apply/hooks.ts @@ -4,6 +4,7 @@ import { useDebounceFn } from 'ahooks' import dayjs from 'dayjs' import timezone from 'dayjs/plugin/timezone' import utc from 'dayjs/plugin/utc' +import { useLocalStorage } from 'foxact/use-local-storage' import { useCallback, useEffect, @@ -13,7 +14,6 @@ import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/con import { useModalContextSelector } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' import { userProfileQueryOptions } from '@/features/account-profile/client' -import { useLocalStorage } from '@/hooks/use-local-storage' import { useRouter, useSearchParams } from '@/next/navigation' import { useEducationAutocomplete, useEducationVerify } from '@/service/use-education' import { diff --git a/web/app/signin/components/mail-and-code-auth.tsx b/web/app/signin/components/mail-and-code-auth.tsx index eacf1e8acd2..2c013b868bc 100644 --- a/web/app/signin/components/mail-and-code-auth.tsx +++ b/web/app/signin/components/mail-and-code-auth.tsx @@ -2,12 +2,12 @@ import { Button } from '@langgenius/dify-ui/button' import { FieldControl, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field' import { Form } from '@langgenius/dify-ui/form' import { toast } from '@langgenius/dify-ui/toast' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown' import { emailRegex } from '@/config' import { useLocale } from '@/context/i18n' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { useRouter, useSearchParams } from '@/next/navigation' import { sendEMailLoginCode } from '@/service/common' diff --git a/web/context/modal-context-provider.tsx b/web/context/modal-context-provider.tsx index 4adc1c25d28..0f6392115c3 100644 --- a/web/context/modal-context-provider.tsx +++ b/web/context/modal-context-provider.tsx @@ -11,6 +11,7 @@ import type { InputVar } from '@/app/components/workflow/types' import type { ExpireNoticeModalPayloadProps } from '@/app/education-apply/expire-notice-modal' import type { ExternalDataTool } from '@/models/common' import type { ModerationConfig, PromptVariable } from '@/models/debug' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { useCallback, useEffect, useRef, useState } from 'react' import { @@ -22,7 +23,6 @@ import { } from '@/app/education-apply/constants' import { useAppContext } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { useAccountSettingModal, usePricingModal, diff --git a/web/context/modal-context.test.tsx b/web/context/modal-context.test.tsx index 62ce95ef978..d31c3f1a6ac 100644 --- a/web/context/modal-context.test.tsx +++ b/web/context/modal-context.test.tsx @@ -32,7 +32,7 @@ vi.mock('@/app/components/header/account-setting', () => ({ ), })) -vi.mock('@/hooks/use-local-storage', () => ({ +vi.mock('foxact/use-local-storage', () => ({ useSetLocalStorage: () => mockSetEducationVerifying, })) diff --git a/web/context/provider-context-provider.tsx b/web/context/provider-context-provider.tsx index 90a47451bfb..e95570bae1f 100644 --- a/web/context/provider-context-provider.tsx +++ b/web/context/provider-context-provider.tsx @@ -4,6 +4,7 @@ import type { ReactNode } from 'react' import { toast } from '@langgenius/dify-ui/toast' import { useQueryClient } from '@tanstack/react-query' import dayjs from 'dayjs' +import { useLocalStorage } from 'foxact/use-local-storage' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils' @@ -15,7 +16,6 @@ import { ModelTypeEnum, } from '@/app/components/header/account-setting/model-provider-page/declarations' import { ZENDESK_FIELD_IDS } from '@/config' -import { useLocalStorage } from '@/hooks/use-local-storage' import { fetchCurrentPlanInfo } from '@/service/billing' import { useModelListByType, diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index 3e5b6213687..7c096cc84f9 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -186,14 +186,13 @@ export default antfu( ...GLOB_TESTS, 'vitest.setup.ts', 'instrumentation-client.ts', - 'hooks/use-local-storage/index.ts', ], rules: { 'no-restricted-globals': [ 'error', { name: 'localStorage', - message: 'Do not use localStorage directly. Use @/hooks/use-local-storage instead.', + message: 'Do not use localStorage directly. Use foxact/use-local-storage instead.', }, ], 'no-restricted-properties': [ @@ -201,19 +200,19 @@ export default antfu( { object: 'window', property: 'localStorage', - message: 'Do not use window.localStorage directly. Use @/hooks/use-local-storage instead.', + message: 'Do not use window.localStorage directly. Use foxact/use-local-storage instead.', }, { object: 'globalThis', property: 'localStorage', - message: 'Do not use globalThis.localStorage directly. Use @/hooks/use-local-storage instead.', + message: 'Do not use globalThis.localStorage directly. Use foxact/use-local-storage instead.', }, ], 'no-restricted-syntax': [ 'error', { selector: 'ImportDeclaration[source.value="ahooks"] ImportSpecifier[imported.name="useLocalStorageState"]', - message: 'Do not use ahooks useLocalStorageState. Use @/hooks/use-local-storage instead.', + message: 'Do not use ahooks useLocalStorageState. Use foxact/use-local-storage instead.', }, ], }, diff --git a/web/hooks/noop.ts b/web/hooks/noop.ts deleted file mode 100644 index 9cf6f968dcb..00000000000 --- a/web/hooks/noop.ts +++ /dev/null @@ -1,7 +0,0 @@ -type Noop = { - // eslint-disable-next-line ts/no-explicit-any - (...args: any[]): any -} - -/** @see https://foxact.skk.moe/noop */ -export const noop: Noop = () => { /* noop */ } diff --git a/web/hooks/use-clipboard.ts b/web/hooks/use-clipboard.ts deleted file mode 100644 index 60ceeb4f188..00000000000 --- a/web/hooks/use-clipboard.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { useRef, useState } from 'react' -import { writeTextToClipboard } from '@/utils/clipboard' -import { noop } from './noop' -import { useStableHandler } from './use-stable-handler-only-when-you-know-what-you-are-doing-or-you-will-be-fired' -import { useCallback } from './use-typescript-happy-callback' -import 'client-only' - -type UseClipboardOption = { - timeout?: number - usePromptAsFallback?: boolean - promptFallbackText?: string - onCopyError?: (error: Error) => void -} - -/** @see https://foxact.skk.moe/use-clipboard */ -export function useClipboard({ - timeout = 1000, - usePromptAsFallback = false, - promptFallbackText = 'Failed to copy to clipboard automatically, please manually copy the text below.', - onCopyError, -}: UseClipboardOption = {}) { - const [error, setError] = useState(null) - const [copied, setCopied] = useState(false) - const copyTimeoutRef = useRef(null) - - const stablizedOnCopyError = useStableHandler<[e: Error], void>(onCopyError || noop) - - const handleCopyResult = useCallback((isCopied: boolean) => { - if (copyTimeoutRef.current) { - clearTimeout(copyTimeoutRef.current) - } - if (isCopied) { - copyTimeoutRef.current = window.setTimeout(() => setCopied(false), timeout) - } - setCopied(isCopied) - }, [timeout]) - - const handleCopyError = useCallback((e: Error) => { - setError(e) - stablizedOnCopyError(e) - }, [stablizedOnCopyError]) - - const copy = useCallback(async (valueToCopy: string) => { - try { - await writeTextToClipboard(valueToCopy) - handleCopyResult(true) - } - catch (e) { - if (usePromptAsFallback) { - try { - // eslint-disable-next-line no-alert -- prompt as fallback in case of copy error - window.prompt(promptFallbackText, valueToCopy) - handleCopyResult(true) - } - catch (e2) { - handleCopyError(e2 as Error) - } - } - else { - handleCopyError(e as Error) - } - } - }, [handleCopyResult, promptFallbackText, handleCopyError, usePromptAsFallback]) - - const reset = useCallback(() => { - setCopied(false) - setError(null) - if (copyTimeoutRef.current) { - clearTimeout(copyTimeoutRef.current) - } - }, []) - - return { copy, reset, error, copied } -} diff --git a/web/hooks/use-import-dsl.ts b/web/hooks/use-import-dsl.ts index bfcb8dc1943..6fe157bcfa5 100644 --- a/web/hooks/use-import-dsl.ts +++ b/web/hooks/use-import-dsl.ts @@ -4,6 +4,7 @@ import type { } from '@/models/app' import type { AppIconType } from '@/types/app' import { toast } from '@langgenius/dify-ui/toast' +import { useSetLocalStorage } from 'foxact/use-local-storage' import { useCallback, useRef, @@ -13,7 +14,6 @@ import { useTranslation } from 'react-i18next' import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useSelector } from '@/context/app-context' -import { useSetLocalStorage } from '@/hooks/use-local-storage' import { DSLImportStatus } from '@/models/app' import { useRouter } from '@/next/navigation' import { diff --git a/web/hooks/use-local-storage/__tests__/index.spec.tsx b/web/hooks/use-local-storage/__tests__/index.spec.tsx deleted file mode 100644 index 1819f46f262..00000000000 --- a/web/hooks/use-local-storage/__tests__/index.spec.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { act, renderHook } from '@testing-library/react' -import { renderToString } from 'react-dom/server' -import { useLocalStorage } from '../index' - -describe('useLocalStorage', () => { - beforeEach(() => { - vi.clearAllMocks() - window.localStorage.clear() - }) - - it('should return server value and persist it when storage is empty', () => { - const { result } = renderHook(() => useLocalStorage('shape', 'circle')) - - expect(result.current[0]).toBe('circle') - expect(window.localStorage.getItem('shape')).toBe(JSON.stringify('circle')) - }) - - it('should prefer stored value over server value', () => { - window.localStorage.setItem('shape', JSON.stringify('square')) - - const { result } = renderHook(() => useLocalStorage('shape', 'circle')) - - expect(result.current[0]).toBe('square') - expect(window.localStorage.getItem('shape')).toBe(JSON.stringify('square')) - }) - - it('should update storage and subscribers from setter calls', () => { - const { result } = renderHook(() => useLocalStorage('shape', 'circle')) - - act(() => { - result.current[1]('triangle') - }) - - expect(result.current[0]).toBe('triangle') - expect(window.localStorage.getItem('shape')).toBe(JSON.stringify('triangle')) - }) - - it('should support updater functions and null removal', () => { - const { result } = renderHook(() => useLocalStorage('count', 1)) - - act(() => { - result.current[1](current => (current ?? 0) + 1) - }) - - expect(result.current[0]).toBe(2) - expect(window.localStorage.getItem('count')).toBe(JSON.stringify(2)) - - act(() => { - result.current[1](null) - }) - - expect(result.current[0]).toBe(1) - expect(window.localStorage.getItem('count')).toBeNull() - }) - - it('should update from cross-tab storage events', () => { - const { result } = renderHook(() => useLocalStorage('shape', 'circle')) - - window.localStorage.setItem('shape', JSON.stringify('square')) - act(() => { - window.dispatchEvent(new StorageEvent('storage', { key: 'shape' })) - }) - - expect(result.current[0]).toBe('square') - }) - - it('should support raw string values', () => { - const { result } = renderHook(() => useLocalStorage('raw-shape', 'circle', { raw: true })) - - act(() => { - result.current[1]('square') - }) - - expect(result.current[0]).toBe('square') - expect(window.localStorage.getItem('raw-shape')).toBe('square') - }) - - it('should render with server value during server rendering', () => { - const Component = () => { - const [value] = useLocalStorage('shape', 'circle') - return
{value}
- } - - expect(renderToString()).toContain('circle') - }) - - it('should throw a recoverable no-SSR error during server rendering without server value', () => { - const Component = () => { - const [value] = useLocalStorage('shape') - return
{value}
- } - - expect(() => renderToString()).toThrow('[foxact/use-local-storage] cannot be used on the server without a serverValue') - }) -}) diff --git a/web/hooks/use-local-storage/index.ts b/web/hooks/use-local-storage/index.ts deleted file mode 100644 index da98e5feb4f..00000000000 --- a/web/hooks/use-local-storage/index.ts +++ /dev/null @@ -1,260 +0,0 @@ -import { useCallback, useEffect, useLayoutEffect as useLayoutEffectFromReact, useMemo, useSyncExternalStore } from 'react' -import { noop } from '../noop' -import 'client-only' - -type NotUndefined = T extends undefined ? never : T -type StateHookTuple = readonly [T, React.Dispatch>] - -type Serializer = (value: T) => string -type Deserializer = (value: string) => T - -export type UseLocalStorageRawOption = { - raw: true -} - -export type UseLocalStorageParserOption = { - raw?: false - serializer: Serializer - deserializer: Deserializer -} - -const FOXACT_LOCAL_STORAGE_EVENT_KEY = 'foxact-use-local-storage' -const HOOK_NAME = 'foxact/use-local-storage' - -const useLayoutEffect = typeof window === 'undefined' - ? useEffect - : useLayoutEffectFromReact - -type ErrorConstructorWithStackTraceLimit = ErrorConstructor & { - stackTraceLimit?: number -} - -const errorConstructor = Error as ErrorConstructorWithStackTraceLimit -const stackTraceLimitProperty = Object.getOwnPropertyDescriptor(errorConstructor, 'stackTraceLimit') -const hasWritableStackTraceLimit = stackTraceLimitProperty?.writable && typeof stackTraceLimitProperty.value === 'number' - -function createStacklessError(errorFactory: () => T): T { - const originalStackTraceLimit = errorConstructor.stackTraceLimit - - if (hasWritableStackTraceLimit) - errorConstructor.stackTraceLimit = 0 - - const error = errorFactory() - - if (hasWritableStackTraceLimit) - errorConstructor.stackTraceLimit = originalStackTraceLimit - - return error -} - -function noSSRError(errorMessage?: string, nextjsDigest = 'BAILOUT_TO_CLIENT_SIDE_RENDERING') { - const error = createStacklessError(() => new Error(errorMessage)) as Error & { - digest?: string - recoverableError?: string - } - - error.digest = nextjsDigest - error.recoverableError = 'NO_SSR' - - return error -} - -function getServerSnapshotWithoutServerValue(): never { - throw noSSRError(`[${HOOK_NAME}] cannot be used on the server without a serverValue`) -} - -function rawSerializer(value: T): string { - return value as string -} - -function rawDeserializer(value: string): T { - return value as T -} - -function isStorageSetter( - value: React.SetStateAction, -): value is (previousState: T | null) => T | null { - return typeof value === 'function' -} - -const dispatchStorageEvent = typeof window === 'undefined' - ? noop - : (key: string) => { - window.dispatchEvent(new CustomEvent(FOXACT_LOCAL_STORAGE_EVENT_KEY, { detail: key })) - } - -const setStorageItem = typeof window === 'undefined' - ? noop - : (key: string, value: string) => { - try { - window.localStorage.setItem(key, value) - } - catch { - console.warn(`[${HOOK_NAME}] Failed to set value to localStorage, it might be blocked`) - } - finally { - dispatchStorageEvent(key) - } - } - -const removeStorageItem = typeof window === 'undefined' - ? noop - : (key: string) => { - try { - window.localStorage.removeItem(key) - } - catch { - console.warn(`[${HOOK_NAME}] Failed to remove value from localStorage, it might be blocked`) - } - finally { - dispatchStorageEvent(key) - } - } - -function getStorageItem(key: string) { - if (typeof window === 'undefined') - return null - - try { - return window.localStorage.getItem(key) - } - catch { - console.warn(`[${HOOK_NAME}] Failed to get value from localStorage, it might be blocked`) - return null - } -} - -function getStorageOptions( - options: UseLocalStorageRawOption | UseLocalStorageParserOption, -) { - return options.raw - ? { - serializer: rawSerializer, - deserializer: rawDeserializer, - } - : { - serializer: options.serializer, - deserializer: options.deserializer, - } -} - -const defaultStorageOptions = { - raw: false, - serializer: JSON.stringify, - deserializer: JSON.parse, -} satisfies UseLocalStorageParserOption - -/** @see https://foxact.skk.moe/use-local-storage */ -export const useSetLocalStorage = ( - key: string, - options: UseLocalStorageRawOption | UseLocalStorageParserOption = defaultStorageOptions as UseLocalStorageParserOption, -) => { - const { serializer, deserializer } = getStorageOptions(options) - - return useCallback((value: React.SetStateAction) => { - try { - let nextState: T | null - if (isStorageSetter(value)) { - const currentRaw = getStorageItem(key) - const currentState = currentRaw === null ? null : deserializer(currentRaw) - nextState = value(currentState) - } - else { - nextState = value - } - - if (nextState === null) - removeStorageItem(key) - else - setStorageItem(key, serializer(nextState)) - } - catch (error) { - console.warn(error) - } - }, [key, serializer, deserializer]) -} - -function useLocalStorageValue( - key: string, - serverValue: NotUndefined, - options?: UseLocalStorageRawOption | UseLocalStorageParserOption, -): T -function useLocalStorageValue( - key: string, - serverValue?: undefined, - options?: UseLocalStorageRawOption | UseLocalStorageParserOption, -): T | null -function useLocalStorageValue( - key: string, - serverValue?: NotUndefined, - options: UseLocalStorageRawOption | UseLocalStorageParserOption = defaultStorageOptions as UseLocalStorageParserOption, -): T | null { - const subscribeToSpecificKeyOfLocalStorage = useCallback((callback: () => void) => { - if (typeof window === 'undefined') - return noop - - const handleStorageEvent = (event: StorageEvent) => { - if (!('key' in event) || event.key === key) - callback() - } - const handleCustomStorageEvent: EventListener = (event) => { - if (event instanceof CustomEvent && event.detail === key) - callback() - } - - window.addEventListener('storage', handleStorageEvent) - window.addEventListener(FOXACT_LOCAL_STORAGE_EVENT_KEY, handleCustomStorageEvent) - - return () => { - window.removeEventListener('storage', handleStorageEvent) - window.removeEventListener(FOXACT_LOCAL_STORAGE_EVENT_KEY, handleCustomStorageEvent) - } - }, [key]) - - const { serializer, deserializer } = getStorageOptions(options) - const getClientSnapshot = () => getStorageItem(key) - const getServerSnapshot = serverValue === undefined - ? getServerSnapshotWithoutServerValue - : () => serializer(serverValue) - - const store = useSyncExternalStore( - subscribeToSpecificKeyOfLocalStorage, - getClientSnapshot, - getServerSnapshot, - ) - - const deserialized = useMemo(() => (store === null ? null : deserializer(store)), [store, deserializer]) - - useLayoutEffect(() => { - if (getStorageItem(key) === null && serverValue !== undefined) - setStorageItem(key, serializer(serverValue)) - }, [key, serializer, serverValue]) - - return deserialized === null - ? (serverValue === undefined ? null : serverValue) - : deserialized -} - -function useLocalStorage( - key: string, - serverValue: NotUndefined, - options?: UseLocalStorageRawOption | UseLocalStorageParserOption, -): StateHookTuple -function useLocalStorage( - key: string, - serverValue?: undefined, - options?: UseLocalStorageRawOption | UseLocalStorageParserOption, -): StateHookTuple -/** @see https://foxact.skk.moe/use-local-storage */ -function useLocalStorage( - key: string, - serverValue?: NotUndefined, - options: UseLocalStorageRawOption | UseLocalStorageParserOption = defaultStorageOptions as UseLocalStorageParserOption, -): StateHookTuple | StateHookTuple { - const value = useLocalStorageValue(key, serverValue!, options) - const setState = useSetLocalStorage(key, options) - - return [value, setState] as const -} - -export { useLocalStorage } diff --git a/web/hooks/use-stable-handler-only-when-you-know-what-you-are-doing-or-you-will-be-fired.ts b/web/hooks/use-stable-handler-only-when-you-know-what-you-are-doing-or-you-will-be-fired.ts deleted file mode 100644 index 227f4fd1fb4..00000000000 --- a/web/hooks/use-stable-handler-only-when-you-know-what-you-are-doing-or-you-will-be-fired.ts +++ /dev/null @@ -1,44 +0,0 @@ -import * as reactExports from 'react' -import { useCallback, useEffect, useLayoutEffect, useRef } from 'react' - -// useIsomorphicInsertionEffect -const useInsertionEffect - = typeof window === 'undefined' - // useInsertionEffect is only available in React 18+ - - ? useEffect - : reactExports.useInsertionEffect || useLayoutEffect - -/** - * @see https://foxact.skk.moe/use-stable-handler-only-when-you-know-what-you-are-doing-or-you-will-be-fired - * Similar to useCallback, with a few subtle differences: - * - The returned function is a stable reference, and will always be the same between renders - * - No dependency lists required - * - Properties or state accessed within the callback will always be "current" - */ -// eslint-disable-next-line ts/no-explicit-any -export function useStableHandler( - callback: (...args: Args) => Result, -): typeof callback { - // Keep track of the latest callback: - // eslint-disable-next-line ts/no-explicit-any - const latestRef = useRef(shouldNotBeInvokedBeforeMount as any) - useInsertionEffect(() => { - latestRef.current = callback - }, [callback]) - - return useCallback((...args) => { - const fn = latestRef.current - return fn(...args) - }, []) -} - -/** - * Render methods should be pure, especially when concurrency is used, - * so we will throw this error if the callback is called while rendering. - */ -function shouldNotBeInvokedBeforeMount() { - throw new Error( - 'foxact: the stablized handler cannot be invoked before the component has mounted.', - ) -} diff --git a/web/hooks/use-typescript-happy-callback.ts b/web/hooks/use-typescript-happy-callback.ts deleted file mode 100644 index db3ba372c01..00000000000 --- a/web/hooks/use-typescript-happy-callback.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useCallback as useCallbackFromReact } from 'react' - -/** @see https://foxact.skk.moe/use-typescript-happy-callback */ -const useTypeScriptHappyCallback: ( - fn: (...args: Args) => R, - deps: React.DependencyList, -) => (...args: Args) => R = useCallbackFromReact - -/** @see https://foxact.skk.moe/use-typescript-happy-callback */ -export const useCallback = useTypeScriptHappyCallback diff --git a/web/package.json b/web/package.json index f6130ce2b37..73910ba17cf 100644 --- a/web/package.json +++ b/web/package.json @@ -80,7 +80,6 @@ "abcjs": "catalog:", "ahooks": "catalog:", "class-variance-authority": "catalog:", - "client-only": "catalog:", "cmdk": "catalog:", "copy-to-clipboard": "catalog:", "cron-parser": "catalog:", @@ -95,6 +94,7 @@ "emoji-mart": "catalog:", "es-toolkit": "catalog:", "fast-deep-equal": "catalog:", + "foxact": "catalog:", "fuse.js": "catalog:", "hast-util-to-jsx-runtime": "catalog:", "html-entities": "catalog:", diff --git a/web/vitest.setup.ts b/web/vitest.setup.ts index 99d264113e8..60e8d7c2495 100644 --- a/web/vitest.setup.ts +++ b/web/vitest.setup.ts @@ -76,7 +76,7 @@ afterEach(async () => { }) // mock custom clipboard hook - wraps writeTextToClipboard with fallback -vi.mock('@/hooks/use-clipboard', () => ({ +vi.mock('foxact/use-clipboard', () => ({ useClipboard: () => ({ copy: vi.fn(), copied: false, From 2a46a7d91d957c2a08756f551689a381c0fab720 Mon Sep 17 00:00:00 2001 From: chariri Date: Thu, 11 Jun 2026 10:30:31 +0900 Subject: [PATCH 013/122] refactor(api): migrate remaining console APIs to use injected user/tenant (#37288) --- .../console/datasets/hit_testing.py | 14 +- .../console/datasets/hit_testing_base.py | 17 +- api/controllers/console/datasets/metadata.py | 15 +- .../datasets/rag_pipeline/rag_pipeline.py | 29 +- .../rag_pipeline/rag_pipeline_workflow.py | 13 +- api/controllers/console/explore/completion.py | 13 +- .../console/explore/conversation.py | 29 +- api/controllers/console/explore/trial.py | 35 +- api/controllers/console/feature.py | 20 +- .../console/workspace/trigger_providers.py | 150 +++----- api/libs/login.py | 48 +++ api/services/metadata_service.py | 34 +- .../built_in/built_in_retrieval.py | 3 +- .../customized/customized_retrieval.py | 6 +- .../database/database_retrieval.py | 3 +- .../pipeline_template_base.py | 2 +- .../remote/remote_retrieval.py | 3 +- api/services/rag_pipeline/rag_pipeline.py | 44 ++- .../rag_pipeline/test_rag_pipeline.py | 24 +- .../test_rag_pipeline_workflow.py | 125 ++++-- .../console/explore/test_conversation.py | 133 ++++--- .../workspace/test_trigger_providers.py | 158 ++++---- .../test_rag_pipeline_service_db.py | 110 +++--- .../services/test_metadata_partial_update.py | 44 ++- .../services/test_metadata_service.py | 357 ++++++++---------- .../rag_pipeline/test_rag_pipeline.py | 100 +++-- .../console/datasets/test_hit_testing.py | 36 +- .../console/datasets/test_hit_testing_base.py | 76 ++-- .../console/datasets/test_metadata.py | 19 +- .../console/explore/test_completion.py | 76 ++-- .../controllers/console/explore/test_trial.py | 352 +++++++---------- .../controllers/console/test_feature.py | 77 ++-- .../controllers/console/test_wraps.py | 3 + .../service_api/dataset/test_hit_testing.py | 52 ++- api/tests/unit_tests/libs/test_login.py | 98 ++++- .../test_customized_retrieval.py | 6 +- .../rag_pipeline/test_rag_pipeline_service.py | 254 ++++++++----- .../services/test_metadata_bug_complete.py | 80 ++-- .../services/test_metadata_nullable_bug.py | 55 ++- 39 files changed, 1448 insertions(+), 1265 deletions(-) diff --git a/api/controllers/console/datasets/hit_testing.py b/api/controllers/console/datasets/hit_testing.py index 37640138eb3..c08ed2fe9f0 100644 --- a/api/controllers/console/datasets/hit_testing.py +++ b/api/controllers/console/datasets/hit_testing.py @@ -8,6 +8,7 @@ from controllers.common.schema import register_response_schema_models, register_ from fields.hit_testing_fields import HitTestingResponse from libs.helper import dump_response from libs.login import login_required +from models import Account from .. import console_ns from ..datasets.hit_testing_base import DatasetsHitTestingBase, HitTestingPayload @@ -15,6 +16,8 @@ from ..wraps import ( account_initialization_required, cloud_edition_billing_rate_limit_check, setup_required, + with_current_tenant_id, + with_current_user, ) register_schema_models(console_ns, HitTestingPayload) @@ -38,11 +41,16 @@ class HitTestingApi(Resource, DatasetsHitTestingBase): @login_required @account_initialization_required @cloud_edition_billing_rate_limit_check("knowledge") - def post(self, dataset_id: UUID) -> dict[str, object]: + @with_current_tenant_id + @with_current_user + def post(self, current_user: Account, current_tenant_id: str, dataset_id: UUID) -> dict[str, object]: dataset_id_str = str(dataset_id) - dataset = self.get_and_validate_dataset(dataset_id_str) + dataset = self.get_and_validate_dataset(dataset_id_str, current_user, current_tenant_id) args = self.parse_args(console_ns.payload) self.hit_testing_args_check(args) - return dump_response(HitTestingResponse, self.perform_hit_testing(dataset, args)) + return dump_response( + HitTestingResponse, + self.perform_hit_testing(dataset, args, current_user, current_tenant_id), + ) diff --git a/api/controllers/console/datasets/hit_testing_base.py b/api/controllers/console/datasets/hit_testing_base.py index 4be91e0e54d..6141d2d1d58 100644 --- a/api/controllers/console/datasets/hit_testing_base.py +++ b/api/controllers/console/datasets/hit_testing_base.py @@ -19,7 +19,7 @@ from core.errors.error import ( QuotaExceededError, ) from graphon.model_runtime.errors.invoke import InvokeError -from libs.login import current_user +from libs.login import resolve_account_fallback from models.account import Account from models.dataset import Dataset from services.dataset_service import DatasetService @@ -71,8 +71,10 @@ class DatasetsHitTestingBase: return normalized_records @staticmethod - def get_and_validate_dataset(dataset_id: str) -> Dataset: - assert isinstance(current_user, Account) + def get_and_validate_dataset( + dataset_id: str, current_user: Account | None = None, current_tenant_id: str | None = None + ) -> Dataset: + current_user, _ = resolve_account_fallback(current_user, current_tenant_id) dataset = DatasetService.get_dataset(dataset_id) if dataset is None: raise NotFound("Dataset not found.") @@ -95,9 +97,14 @@ class DatasetsHitTestingBase: return hit_testing_payload.model_dump(exclude_none=True) @staticmethod - def perform_hit_testing(dataset: Dataset, args: dict[str, Any]) -> dict[str, Any]: - assert isinstance(current_user, Account) + def perform_hit_testing( + dataset: Dataset, + args: dict[str, Any], + current_user: Account | None = None, + current_tenant_id: str | None = None, + ) -> dict[str, Any]: try: + current_user, _ = resolve_account_fallback(current_user, current_tenant_id) response = HitTestingService.retrieve( dataset=dataset, query=cast(str, args.get("query")), diff --git a/api/controllers/console/datasets/metadata.py b/api/controllers/console/datasets/metadata.py index 5b53c40ae97..ec4c5bedb61 100644 --- a/api/controllers/console/datasets/metadata.py +++ b/api/controllers/console/datasets/metadata.py @@ -11,6 +11,7 @@ from controllers.console.wraps import ( account_initialization_required, enterprise_license_required, setup_required, + with_current_tenant_id, with_current_user, ) from fields.dataset_fields import ( @@ -50,7 +51,8 @@ class DatasetMetadataCreateApi(Resource): @console_ns.response(201, "Metadata created successfully", console_ns.models[DatasetMetadataResponse.__name__]) @console_ns.expect(console_ns.models[MetadataArgs.__name__]) @with_current_user - def post(self, current_user: Account, dataset_id: UUID): + @with_current_tenant_id + def post(self, current_tenant_id: str, current_user: Account, dataset_id: UUID): metadata_args = MetadataArgs.model_validate(console_ns.payload or {}) dataset_id_str = str(dataset_id) @@ -59,7 +61,7 @@ class DatasetMetadataCreateApi(Resource): raise NotFound("Dataset not found.") DatasetService.check_dataset_permission(dataset, current_user) - metadata = MetadataService.create_metadata(dataset_id_str, metadata_args) + metadata = MetadataService.create_metadata(dataset_id_str, metadata_args, current_user, current_tenant_id) return dump_response(DatasetMetadataResponse, metadata), 201 @setup_required @@ -87,7 +89,8 @@ class DatasetMetadataApi(Resource): @console_ns.response(200, "Metadata updated successfully", console_ns.models[DatasetMetadataResponse.__name__]) @console_ns.expect(console_ns.models[MetadataUpdatePayload.__name__]) @with_current_user - def patch(self, current_user: Account, dataset_id: UUID, metadata_id: UUID): + @with_current_tenant_id + def patch(self, current_tenant_id: str, current_user: Account, dataset_id: UUID, metadata_id: UUID): payload = MetadataUpdatePayload.model_validate(console_ns.payload or {}) name = payload.name @@ -98,7 +101,9 @@ class DatasetMetadataApi(Resource): raise NotFound("Dataset not found.") DatasetService.check_dataset_permission(dataset, current_user) - metadata = MetadataService.update_metadata_name(dataset_id_str, metadata_id_str, name) + metadata = MetadataService.update_metadata_name( + dataset_id_str, metadata_id_str, name, current_user, current_tenant_id + ) return dump_response(DatasetMetadataResponse, metadata), 200 @setup_required @@ -181,7 +186,7 @@ class DocumentMetadataEditApi(Resource): metadata_args = MetadataOperationData.model_validate(console_ns.payload or {}) - MetadataService.update_documents_metadata(dataset, metadata_args) + MetadataService.update_documents_metadata(dataset, metadata_args, current_user) # Frontend callers only await success and invalidate caches; no response body is consumed. return "", 204 diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline.py index d21800d53c1..ca41573cb85 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline.py @@ -21,11 +21,14 @@ from controllers.console.wraps import ( enterprise_license_required, knowledge_pipeline_publish_enabled, setup_required, + with_current_tenant_id, + with_current_user, ) from extensions.ext_database import db from fields.base import ResponseModel from libs.helper import dump_response from libs.login import login_required +from models.account import Account from models.dataset import PipelineCustomizedTemplate from services.entities.knowledge_entities.rag_pipeline_entities import IconInfo, PipelineTemplateInfoEntity from services.rag_pipeline.rag_pipeline import RagPipelineService @@ -96,10 +99,11 @@ class PipelineTemplateListApi(Resource): @login_required @account_initialization_required @enterprise_license_required - def get(self) -> JsonResponseWithStatus: + @with_current_tenant_id + def get(self, current_tenant_id: str) -> JsonResponseWithStatus: query = PipelineTemplateListQuery.model_validate(request.args.to_dict(flat=True)) # get pipeline templates - pipeline_templates = RagPipelineService.get_pipeline_templates(query.type, query.language) + pipeline_templates = RagPipelineService.get_pipeline_templates(query.type, query.language, current_tenant_id) return dump_response(PipelineTemplateListResponse, pipeline_templates), 200 @@ -128,10 +132,14 @@ class CustomizedPipelineTemplateApi(Resource): @login_required @account_initialization_required @enterprise_license_required - def patch(self, template_id: str) -> tuple[str, int]: + @with_current_user + @with_current_tenant_id + def patch(self, current_tenant_id: str, current_user: Account, template_id: str) -> tuple[str, int]: payload = CustomizedPipelineTemplatePayload.model_validate(console_ns.payload or {}) pipeline_template_info = PipelineTemplateInfoEntity.model_validate(payload.model_dump()) - RagPipelineService.update_customized_pipeline_template(template_id, pipeline_template_info) + RagPipelineService.update_customized_pipeline_template( + template_id, pipeline_template_info, current_user, current_tenant_id + ) return "", 204 @console_ns.response(204, "Pipeline template deleted") @@ -139,8 +147,9 @@ class CustomizedPipelineTemplateApi(Resource): @login_required @account_initialization_required @enterprise_license_required - def delete(self, template_id: str) -> tuple[str, int]: - RagPipelineService.delete_customized_pipeline_template(template_id) + @with_current_tenant_id + def delete(self, current_tenant_id: str, template_id: str) -> tuple[str, int]: + RagPipelineService.delete_customized_pipeline_template(template_id, current_tenant_id) return "", 204 @setup_required @@ -168,8 +177,12 @@ class PublishCustomizedPipelineTemplateApi(Resource): @account_initialization_required @enterprise_license_required @knowledge_pipeline_publish_enabled - def post(self, pipeline_id: str) -> tuple[str, int]: + @with_current_user + @with_current_tenant_id + def post(self, current_tenant_id: str, current_user: Account, pipeline_id: str) -> tuple[str, int]: payload = CustomizedPipelineTemplatePayload.model_validate(console_ns.payload or {}) rag_pipeline_service = RagPipelineService() - rag_pipeline_service.publish_customized_pipeline_template(pipeline_id, payload.model_dump()) + rag_pipeline_service.publish_customized_pipeline_template( + pipeline_id, payload.model_dump(), current_user, current_tenant_id + ) return "", 204 diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py index ebc3b92dd63..53e0d0c2931 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py @@ -48,7 +48,7 @@ from fields.workflow_run_fields import ( from graphon.model_runtime.utils.encoders import jsonable_encoder from libs import helper from libs.helper import TimestampField, UUIDStrOrEmpty, dump_response -from libs.login import current_user, login_required +from libs.login import login_required from models import Account from models.dataset import Pipeline from models.model import EndUser @@ -835,7 +835,7 @@ class RagPipelineWorkflowRunListApi(Resource): } ) args = { - "last_id": str(query.last_id) if query.last_id else None, + "last_id": query.last_id or None, "limit": query.limit, } @@ -881,7 +881,8 @@ class RagPipelineWorkflowRunNodeExecutionListApi(Resource): @login_required @account_initialization_required @get_rag_pipeline - def get(self, pipeline: Pipeline, run_id: UUID): + @with_current_user + def get(self, current_user: Account, pipeline: Pipeline, run_id: UUID): """ Get workflow run node execution list """ @@ -988,9 +989,11 @@ class RagPipelineRecommendedPluginApi(Resource): @setup_required @login_required @account_initialization_required - def get(self): + @with_current_user + @with_current_tenant_id + def get(self, current_tenant_id: str, current_user: Account): query = RagPipelineRecommendedPluginQuery.model_validate(request.args.to_dict()) rag_pipeline_service = RagPipelineService() - recommended_plugins = rag_pipeline_service.get_recommended_plugins(query.type) + recommended_plugins = rag_pipeline_service.get_recommended_plugins(query.type, current_user, current_tenant_id) return recommended_plugins diff --git a/api/controllers/console/explore/completion.py b/api/controllers/console/explore/completion.py index d1ae6526c68..1db177a29dd 100644 --- a/api/controllers/console/explore/completion.py +++ b/api/controllers/console/explore/completion.py @@ -18,7 +18,7 @@ from controllers.console.app.error import ( ) from controllers.console.explore.error import NotChatAppError, NotCompletionAppError from controllers.console.explore.wraps import InstalledAppResource -from controllers.console.wraps import with_current_user_id +from controllers.console.wraps import with_current_user, with_current_user_id from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError from core.app.entities.app_invoke_entities import InvokeFrom from core.errors.error import ( @@ -30,7 +30,6 @@ from extensions.ext_database import db from graphon.model_runtime.errors.invoke import InvokeError from libs import helper from libs.datetime_utils import naive_utc_now -from libs.login import current_user from models import Account from models.model import AppMode, InstalledApp from services.app_generate_service import AppGenerateService @@ -84,7 +83,8 @@ register_response_schema_models(console_ns, SimpleResultResponse) ) class CompletionApi(InstalledAppResource): @console_ns.expect(console_ns.models[CompletionMessageExplorePayload.__name__]) - def post(self, installed_app: InstalledApp): + @with_current_user + def post(self, current_user: Account, installed_app: InstalledApp): app_model = installed_app.app if app_model is None: raise AppUnavailableError() @@ -101,8 +101,6 @@ class CompletionApi(InstalledAppResource): db.session.commit() try: - if not isinstance(current_user, Account): - raise ValueError("current_user must be an Account instance") response = AppGenerateService.generate( app_model=app_model, user=current_user, args=args, invoke_from=InvokeFrom.EXPLORE, streaming=streaming ) @@ -160,7 +158,8 @@ class CompletionStopApi(InstalledAppResource): ) class ChatApi(InstalledAppResource): @console_ns.expect(console_ns.models[ChatMessagePayload.__name__]) - def post(self, installed_app: InstalledApp): + @with_current_user + def post(self, current_user: Account, installed_app: InstalledApp): app_model = installed_app.app if app_model is None: raise AppUnavailableError() @@ -177,8 +176,6 @@ class ChatApi(InstalledAppResource): db.session.commit() try: - if not isinstance(current_user, Account): - raise ValueError("current_user must be an Account instance") response = AppGenerateService.generate( app_model=app_model, user=current_user, args=args, invoke_from=InvokeFrom.EXPLORE, streaming=True ) diff --git a/api/controllers/console/explore/conversation.py b/api/controllers/console/explore/conversation.py index 68e18a0207b..9cebba496b5 100644 --- a/api/controllers/console/explore/conversation.py +++ b/api/controllers/console/explore/conversation.py @@ -11,6 +11,7 @@ from controllers.common.schema import register_response_schema_models, register_ from controllers.console.app.error import AppUnavailableError from controllers.console.explore.error import NotChatAppError from controllers.console.explore.wraps import InstalledAppResource +from controllers.console.wraps import with_current_user from core.app.entities.app_invoke_entities import InvokeFrom from extensions.ext_database import db from fields.conversation_fields import ( @@ -19,7 +20,6 @@ from fields.conversation_fields import ( SimpleConversation, ) from libs.helper import UUIDStrOrEmpty -from libs.login import current_user from models import Account from models.model import AppMode, InstalledApp from services.conversation_service import ConversationService @@ -45,7 +45,8 @@ register_response_schema_models(console_ns, ResultResponse) ) class ConversationListApi(InstalledAppResource): @console_ns.expect(console_ns.models[ConversationListQuery.__name__]) - def get(self, installed_app: InstalledApp): + @with_current_user + def get(self, current_user: Account, installed_app: InstalledApp): app_model = installed_app.app if app_model is None: raise AppUnavailableError() @@ -66,14 +67,12 @@ class ConversationListApi(InstalledAppResource): args = ConversationListQuery.model_validate(raw_args) try: - if not isinstance(current_user, Account): - raise ValueError("current_user must be an Account instance") with sessionmaker(db.engine).begin() as session: pagination = WebConversationService.pagination_by_last_id( session=session, app_model=app_model, user=current_user, - last_id=str(args.last_id) if args.last_id else None, + last_id=args.last_id or None, limit=args.limit, invoke_from=InvokeFrom.EXPLORE, pinned=args.pinned, @@ -95,7 +94,8 @@ class ConversationListApi(InstalledAppResource): ) class ConversationApi(InstalledAppResource): @console_ns.response(204, "Conversation deleted successfully") - def delete(self, installed_app: InstalledApp, c_id: UUID): + @with_current_user + def delete(self, current_user: Account, installed_app: InstalledApp, c_id: UUID): app_model = installed_app.app if app_model is None: raise AppUnavailableError() @@ -105,8 +105,6 @@ class ConversationApi(InstalledAppResource): conversation_id = str(c_id) try: - if not isinstance(current_user, Account): - raise ValueError("current_user must be an Account instance") ConversationService.delete(app_model, conversation_id, current_user) except ConversationNotExistsError: raise NotFound("Conversation Not Exists.") @@ -120,7 +118,8 @@ class ConversationApi(InstalledAppResource): ) class ConversationRenameApi(InstalledAppResource): @console_ns.expect(console_ns.models[ConversationRenamePayload.__name__]) - def post(self, installed_app: InstalledApp, c_id: UUID): + @with_current_user + def post(self, current_user: Account, installed_app: InstalledApp, c_id: UUID): app_model = installed_app.app if app_model is None: raise AppUnavailableError() @@ -133,8 +132,6 @@ class ConversationRenameApi(InstalledAppResource): payload = ConversationRenamePayload.model_validate(console_ns.payload or {}) try: - if not isinstance(current_user, Account): - raise ValueError("current_user must be an Account instance") conversation = ConversationService.rename( app_model, conversation_id, current_user, payload.name, payload.auto_generate ) @@ -153,7 +150,8 @@ class ConversationRenameApi(InstalledAppResource): ) class ConversationPinApi(InstalledAppResource): @console_ns.response(200, "Success", console_ns.models[ResultResponse.__name__]) - def patch(self, installed_app: InstalledApp, c_id: UUID): + @with_current_user + def patch(self, current_user: Account, installed_app: InstalledApp, c_id: UUID): app_model = installed_app.app if app_model is None: raise AppUnavailableError() @@ -164,8 +162,6 @@ class ConversationPinApi(InstalledAppResource): conversation_id = str(c_id) try: - if not isinstance(current_user, Account): - raise ValueError("current_user must be an Account instance") WebConversationService.pin(app_model, conversation_id, current_user) except ConversationNotExistsError: raise NotFound("Conversation Not Exists.") @@ -179,7 +175,8 @@ class ConversationPinApi(InstalledAppResource): ) class ConversationUnPinApi(InstalledAppResource): @console_ns.response(200, "Success", console_ns.models[ResultResponse.__name__]) - def patch(self, installed_app: InstalledApp, c_id: UUID): + @with_current_user + def patch(self, current_user: Account, installed_app: InstalledApp, c_id: UUID): app_model = installed_app.app if app_model is None: raise AppUnavailableError() @@ -188,8 +185,6 @@ class ConversationUnPinApi(InstalledAppResource): raise NotChatAppError() conversation_id = str(c_id) - if not isinstance(current_user, Account): - raise ValueError("current_user must be an Account instance") WebConversationService.unpin(app_model, conversation_id, current_user) return ResultResponse(result="success").model_dump(mode="json") diff --git a/api/controllers/console/explore/trial.py b/api/controllers/console/explore/trial.py index 26b48ec599a..2e7796574f1 100644 --- a/api/controllers/console/explore/trial.py +++ b/api/controllers/console/explore/trial.py @@ -33,6 +33,7 @@ from controllers.console.explore.error import ( NotWorkflowAppError, ) from controllers.console.explore.wraps import TrialAppResource, trial_feature_enable +from controllers.console.wraps import with_current_user from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError from core.app.app_config.common.parameters_mapping import get_parameters_from_feature_dict from core.app.apps.base_app_queue_manager import AppQueueManager @@ -63,7 +64,6 @@ from graphon.graph_engine.manager import GraphEngineManager from graphon.model_runtime.errors.invoke import InvokeError from libs import helper from libs.helper import uuid_value -from libs.login import current_user from models import Account from models.account import TenantStatus from models.model import AppMode, Site @@ -155,7 +155,8 @@ register_schema_models(console_ns, WorkflowRunRequest, ChatRequest, TextToSpeech class TrialAppWorkflowRunApi(TrialAppResource): @trial_feature_enable @console_ns.expect(console_ns.models[WorkflowRunRequest.__name__]) - def post(self, trial_app): + @with_current_user + def post(self, current_user: Account, trial_app): """ Run workflow """ @@ -168,7 +169,6 @@ class TrialAppWorkflowRunApi(TrialAppResource): request_data = WorkflowRunRequest.model_validate(console_ns.payload) args = request_data.model_dump() - assert current_user is not None try: app_id = app_model.id user_id = current_user.id @@ -206,7 +206,6 @@ class TrialAppWorkflowTaskStopApi(TrialAppResource): app_mode = AppMode.value_of(app_model.mode) if app_mode != AppMode.WORKFLOW: raise NotWorkflowAppError() - assert current_user is not None # Stop using both mechanisms for backward compatibility # Legacy stop flag mechanism (without user check) @@ -221,7 +220,8 @@ class TrialAppWorkflowTaskStopApi(TrialAppResource): class TrialChatApi(TrialAppResource): @console_ns.expect(console_ns.models[ChatRequest.__name__]) @trial_feature_enable - def post(self, trial_app): + @with_current_user + def post(self, current_user: Account, trial_app): app_model = trial_app app_mode = AppMode.value_of(app_model.mode) if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: @@ -239,9 +239,6 @@ class TrialChatApi(TrialAppResource): args["auto_generate_name"] = False try: - if not isinstance(current_user, Account): - raise ValueError("current_user must be an Account instance") - # Get IDs before they might be detached from session app_id = app_model.id user_id = current_user.id @@ -276,7 +273,8 @@ class TrialChatApi(TrialAppResource): class TrialMessageSuggestedQuestionApi(TrialAppResource): - def get(self, trial_app, message_id): + @with_current_user + def get(self, current_user: Account, trial_app, message_id): app_model = trial_app app_mode = AppMode.value_of(app_model.mode) if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: @@ -285,8 +283,6 @@ class TrialMessageSuggestedQuestionApi(TrialAppResource): message_id = str(message_id) try: - if not isinstance(current_user, Account): - raise ValueError("current_user must be an Account instance") questions = MessageService.get_suggested_questions_after_answer( app_model=app_model, user=current_user, message_id=message_id, invoke_from=InvokeFrom.EXPLORE ) @@ -313,15 +309,13 @@ class TrialMessageSuggestedQuestionApi(TrialAppResource): class TrialChatAudioApi(TrialAppResource): @trial_feature_enable - def post(self, trial_app): + @with_current_user + def post(self, current_user: Account, trial_app): app_model = trial_app file = request.files["file"] try: - if not isinstance(current_user, Account): - raise ValueError("current_user must be an Account instance") - # Get IDs before they might be detached from session app_id = app_model.id user_id = current_user.id @@ -358,7 +352,8 @@ class TrialChatAudioApi(TrialAppResource): class TrialChatTextApi(TrialAppResource): @console_ns.expect(console_ns.models[TextToSpeechRequest.__name__]) @trial_feature_enable - def post(self, trial_app): + @with_current_user + def post(self, current_user: Account, trial_app): app_model = trial_app try: request_data = TextToSpeechRequest.model_validate(console_ns.payload) @@ -366,8 +361,6 @@ class TrialChatTextApi(TrialAppResource): message_id = request_data.message_id text = request_data.text voice = request_data.voice - if not isinstance(current_user, Account): - raise ValueError("current_user must be an Account instance") # Get IDs before they might be detached from session app_id = app_model.id @@ -405,7 +398,8 @@ class TrialChatTextApi(TrialAppResource): class TrialCompletionApi(TrialAppResource): @console_ns.expect(console_ns.models[CompletionRequest.__name__]) @trial_feature_enable - def post(self, trial_app): + @with_current_user + def post(self, current_user: Account, trial_app): app_model = trial_app if app_model.mode != "completion": raise NotCompletionAppError() @@ -417,9 +411,6 @@ class TrialCompletionApi(TrialAppResource): args["auto_generate_name"] = False try: - if not isinstance(current_user, Account): - raise ValueError("current_user must be an Account instance") - # Get IDs before they might be detached from session app_id = app_model.id user_id = current_user.id diff --git a/api/controllers/console/feature.py b/api/controllers/console/feature.py index b221db697ba..3b1b414150a 100644 --- a/api/controllers/console/feature.py +++ b/api/controllers/console/feature.py @@ -1,10 +1,9 @@ from flask_restx import Resource -from werkzeug.exceptions import Unauthorized from controllers.common.schema import register_response_schema_models from fields.base import ResponseModel from libs.helper import dump_response -from libs.login import current_user, login_required +from libs.login import current_account_with_tenant_optional, login_required from services.feature_service import ( FeatureModel, FeatureService, @@ -13,7 +12,12 @@ from services.feature_service import ( ) from . import console_ns -from .wraps import account_initialization_required, cloud_utm_record, setup_required, with_current_tenant_id +from .wraps import ( + account_initialization_required, + cloud_utm_record, + setup_required, + with_current_tenant_id, +) class TrialModelsResponse(ResponseModel): @@ -133,12 +137,6 @@ class SystemFeatureApi(Resource): Only non-sensitive configuration data should be returned by this endpoint. """ - # NOTE(QuantumGhost): ideally we should access `current_user.is_authenticated` - # without a try-catch. However, due to the implementation of user loader (the `load_user_from_request` - # in api/extensions/ext_login.py), accessing `current_user.is_authenticated` will - # raise `Unauthorized` exception if authentication token is not provided. - try: - is_authenticated = current_user.is_authenticated - except Unauthorized: - is_authenticated = False + current_user, _ = current_account_with_tenant_optional() + is_authenticated = current_user is not None return FeatureService.get_system_features(is_authenticated=is_authenticated).model_dump() diff --git a/api/controllers/console/workspace/trigger_providers.py b/api/controllers/console/workspace/trigger_providers.py index 3805d0ff372..d862ba4ff45 100644 --- a/api/controllers/console/workspace/trigger_providers.py +++ b/api/controllers/console/workspace/trigger_providers.py @@ -17,7 +17,7 @@ from core.trigger.entities.entities import SubscriptionBuilderUpdater from core.trigger.trigger_manager import TriggerManager from extensions.ext_database import db from graphon.model_runtime.utils.encoders import jsonable_encoder -from libs.login import current_user, login_required +from libs.login import login_required from models.account import Account from models.provider_ids import TriggerProviderID from services.plugin.oauth_service import OAuthProxyService @@ -31,6 +31,8 @@ from ..wraps import ( edit_permission_required, is_admin_or_owner_required, setup_required, + with_current_tenant_id, + with_current_user, ) logger = logging.getLogger(__name__) @@ -77,12 +79,9 @@ class TriggerProviderIconApi(Resource): @setup_required @login_required @account_initialization_required - def get(self, provider: str): - user = current_user - assert isinstance(user, Account) - assert user.current_tenant_id is not None - - return TriggerManager.get_trigger_plugin_icon(tenant_id=user.current_tenant_id, provider_id=provider) + @with_current_tenant_id + def get(self, tenant_id: str, provider: str): + return TriggerManager.get_trigger_plugin_icon(tenant_id=tenant_id, provider_id=provider) @console_ns.route("/workspaces/current/triggers") @@ -90,12 +89,10 @@ class TriggerProviderListApi(Resource): @setup_required @login_required @account_initialization_required - def get(self): + @with_current_tenant_id + def get(self, tenant_id: str): """List all trigger providers for the current tenant""" - user = current_user - assert isinstance(user, Account) - assert user.current_tenant_id is not None - return jsonable_encoder(TriggerProviderService.list_trigger_providers(user.current_tenant_id)) + return jsonable_encoder(TriggerProviderService.list_trigger_providers(tenant_id)) @console_ns.route("/workspaces/current/trigger-provider//info") @@ -103,14 +100,10 @@ class TriggerProviderInfoApi(Resource): @setup_required @login_required @account_initialization_required - def get(self, provider: str): + @with_current_tenant_id + def get(self, tenant_id: str, provider: str): """Get info for a trigger provider""" - user = current_user - assert isinstance(user, Account) - assert user.current_tenant_id is not None - return jsonable_encoder( - TriggerProviderService.get_trigger_provider(user.current_tenant_id, TriggerProviderID(provider)) - ) + return jsonable_encoder(TriggerProviderService.get_trigger_provider(tenant_id, TriggerProviderID(provider))) @console_ns.route("/workspaces/current/trigger-provider//subscriptions/list") @@ -119,16 +112,14 @@ class TriggerSubscriptionListApi(Resource): @login_required @edit_permission_required @account_initialization_required - def get(self, provider: str): + @with_current_user + @with_current_tenant_id + def get(self, tenant_id: str, user: Account, provider: str): """List all trigger subscriptions for the current tenant's provider""" - user = current_user - assert isinstance(user, Account) - assert user.current_tenant_id is not None - try: return jsonable_encoder( TriggerProviderService.list_trigger_provider_subscriptions( - tenant_id=user.current_tenant_id, + tenant_id=tenant_id, provider_id=TriggerProviderID(provider), user=user, ) @@ -149,17 +140,16 @@ class TriggerSubscriptionBuilderCreateApi(Resource): @login_required @edit_permission_required @account_initialization_required - def post(self, provider: str): + @with_current_user + @with_current_tenant_id + def post(self, tenant_id: str, user: Account, provider: str): """Add a new subscription instance for a trigger provider""" - user = current_user - assert user.current_tenant_id is not None - payload = TriggerSubscriptionBuilderCreatePayload.model_validate(console_ns.payload or {}) try: credential_type = CredentialType.of(payload.credential_type) subscription_builder = TriggerSubscriptionBuilderService.create_trigger_subscription_builder( - tenant_id=user.current_tenant_id, + tenant_id=tenant_id, user_id=user.id, provider_id=TriggerProviderID(provider), credential_type=credential_type, @@ -194,17 +184,16 @@ class TriggerSubscriptionBuilderVerifyApi(Resource): @login_required @edit_permission_required @account_initialization_required - def post(self, provider: str, subscription_builder_id: str): + @with_current_user + @with_current_tenant_id + def post(self, tenant_id: str, user: Account, provider: str, subscription_builder_id: str): """Verify and update a subscription instance for a trigger provider""" - user = current_user - assert user.current_tenant_id is not None - payload = TriggerSubscriptionBuilderVerifyPayload.model_validate(console_ns.payload or {}) try: # Use atomic update_and_verify to prevent race conditions return TriggerSubscriptionBuilderService.update_and_verify_builder( - tenant_id=user.current_tenant_id, + tenant_id=tenant_id, user_id=user.id, provider_id=TriggerProviderID(provider), subscription_builder_id=subscription_builder_id, @@ -226,17 +215,14 @@ class TriggerSubscriptionBuilderUpdateApi(Resource): @login_required @edit_permission_required @account_initialization_required - def post(self, provider: str, subscription_builder_id: str): + @with_current_tenant_id + def post(self, tenant_id: str, provider: str, subscription_builder_id: str): """Update a subscription instance for a trigger provider""" - user = current_user - assert isinstance(user, Account) - assert user.current_tenant_id is not None - payload = TriggerSubscriptionBuilderUpdatePayload.model_validate(console_ns.payload or {}) try: return jsonable_encoder( TriggerSubscriptionBuilderService.update_trigger_subscription_builder( - tenant_id=user.current_tenant_id, + tenant_id=tenant_id, provider_id=TriggerProviderID(provider), subscription_builder_id=subscription_builder_id, subscription_builder_updater=SubscriptionBuilderUpdater( @@ -262,10 +248,6 @@ class TriggerSubscriptionBuilderLogsApi(Resource): @account_initialization_required def get(self, provider: str, subscription_builder_id: str): """Get the request logs for a subscription instance for a trigger provider""" - user = current_user - assert isinstance(user, Account) - assert user.current_tenant_id is not None - try: logs = TriggerSubscriptionBuilderService.list_logs(subscription_builder_id) return jsonable_encoder({"logs": [log.model_dump(mode="json") for log in logs]}) @@ -283,15 +265,15 @@ class TriggerSubscriptionBuilderBuildApi(Resource): @login_required @edit_permission_required @account_initialization_required - def post(self, provider: str, subscription_builder_id: str): + @with_current_user + @with_current_tenant_id + def post(self, tenant_id: str, user: Account, provider: str, subscription_builder_id: str): """Build a subscription instance for a trigger provider""" - user = current_user - assert user.current_tenant_id is not None payload = TriggerSubscriptionBuilderUpdatePayload.model_validate(console_ns.payload or {}) try: # Use atomic update_and_build to prevent race conditions TriggerSubscriptionBuilderService.update_and_build_builder( - tenant_id=user.current_tenant_id, + tenant_id=tenant_id, user_id=user.id, provider_id=TriggerProviderID(provider), subscription_builder_id=subscription_builder_id, @@ -316,15 +298,13 @@ class TriggerSubscriptionUpdateApi(Resource): @login_required @edit_permission_required @account_initialization_required - def post(self, subscription_id: str): + @with_current_tenant_id + def post(self, tenant_id: str, subscription_id: str): """Update a subscription instance""" - user = current_user - assert user.current_tenant_id is not None - request = TriggerSubscriptionBuilderUpdatePayload.model_validate(console_ns.payload or {}) subscription = TriggerProviderService.get_subscription_by_id( - tenant_id=user.current_tenant_id, + tenant_id=tenant_id, subscription_id=subscription_id, ) if not subscription: @@ -341,7 +321,7 @@ class TriggerSubscriptionUpdateApi(Resource): manually_created = subscription.credential_type == CredentialType.UNAUTHORIZED if rename or manually_created: TriggerProviderService.update_trigger_subscription( - tenant_id=user.current_tenant_id, + tenant_id=tenant_id, subscription_id=subscription_id, name=request.name, properties=request.properties, @@ -351,7 +331,7 @@ class TriggerSubscriptionUpdateApi(Resource): # For the rest cases(API_KEY, OAUTH2) # we need to call third party provider(e.g. GitHub) to rebuild the subscription TriggerProviderService.rebuild_trigger_subscription( - tenant_id=user.current_tenant_id, + tenant_id=tenant_id, name=request.name, provider_id=provider_id, subscription_id=subscription_id, @@ -375,23 +355,21 @@ class TriggerSubscriptionDeleteApi(Resource): @login_required @is_admin_or_owner_required @account_initialization_required - def post(self, subscription_id: str): + @with_current_tenant_id + def post(self, tenant_id: str, subscription_id: str): """Delete a subscription instance""" - user = current_user - assert user.current_tenant_id is not None - try: with sessionmaker(db.engine).begin() as session: # Delete trigger provider subscription TriggerProviderService.delete_trigger_provider( session=session, - tenant_id=user.current_tenant_id, + tenant_id=tenant_id, subscription_id=subscription_id, ) # Delete plugin triggers TriggerSubscriptionOperatorService.delete_plugin_trigger_by_subscription( session=session, - tenant_id=user.current_tenant_id, + tenant_id=tenant_id, subscription_id=subscription_id, ) return {"result": "success"} @@ -407,17 +385,14 @@ class TriggerOAuthAuthorizeApi(Resource): @setup_required @login_required @account_initialization_required - def get(self, provider: str): + @with_current_user + @with_current_tenant_id + def get(self, tenant_id: str, user: Account, provider: str): """Initiate OAuth authorization flow for a trigger provider""" - user = current_user - assert isinstance(user, Account) - assert user.current_tenant_id is not None - try: provider_id = TriggerProviderID(provider) plugin_id = provider_id.plugin_id provider_name = provider_id.provider_name - tenant_id = user.current_tenant_id # Get OAuth client configuration oauth_client_params = TriggerProviderService.get_oauth_client( @@ -557,30 +532,28 @@ class TriggerOAuthClientManageApi(Resource): @login_required @is_admin_or_owner_required @account_initialization_required - def get(self, provider: str): + @with_current_tenant_id + def get(self, tenant_id: str, provider: str): """Get OAuth client configuration for a provider""" - user = current_user - assert user.current_tenant_id is not None - try: provider_id = TriggerProviderID(provider) # Get custom OAuth client params if exists custom_params = TriggerProviderService.get_custom_oauth_client_params( - tenant_id=user.current_tenant_id, + tenant_id=tenant_id, provider_id=provider_id, ) # Check if custom client is enabled is_custom_enabled = TriggerProviderService.is_oauth_custom_client_enabled( - tenant_id=user.current_tenant_id, + tenant_id=tenant_id, provider_id=provider_id, ) system_client_exists = TriggerProviderService.is_oauth_system_client_exists( - tenant_id=user.current_tenant_id, + tenant_id=tenant_id, provider_id=provider_id, ) - provider_controller = TriggerManager.get_trigger_provider(user.current_tenant_id, provider_id) + provider_controller = TriggerManager.get_trigger_provider(tenant_id, provider_id) redirect_uri = f"{dify_config.CONSOLE_API_URL}/console/api/oauth/plugin/{provider}/trigger/callback" return jsonable_encoder( { @@ -603,17 +576,15 @@ class TriggerOAuthClientManageApi(Resource): @login_required @is_admin_or_owner_required @account_initialization_required - def post(self, provider: str): + @with_current_tenant_id + def post(self, tenant_id: str, provider: str): """Configure custom OAuth client for a provider""" - user = current_user - assert user.current_tenant_id is not None - payload = TriggerOAuthClientPayload.model_validate(console_ns.payload or {}) try: provider_id = TriggerProviderID(provider) return TriggerProviderService.save_custom_oauth_client_params( - tenant_id=user.current_tenant_id, + tenant_id=tenant_id, provider_id=provider_id, client_params=payload.client_params, enabled=payload.enabled, @@ -629,16 +600,14 @@ class TriggerOAuthClientManageApi(Resource): @login_required @is_admin_or_owner_required @account_initialization_required - def delete(self, provider: str): + @with_current_tenant_id + def delete(self, tenant_id: str, provider: str): """Remove custom OAuth client configuration""" - user = current_user - assert user.current_tenant_id is not None - try: provider_id = TriggerProviderID(provider) return TriggerProviderService.delete_custom_oauth_client_params( - tenant_id=user.current_tenant_id, + tenant_id=tenant_id, provider_id=provider_id, ) except ValueError as e: @@ -657,16 +626,15 @@ class TriggerSubscriptionVerifyApi(Resource): @login_required @edit_permission_required @account_initialization_required - def post(self, provider: str, subscription_id: str): + @with_current_user + @with_current_tenant_id + def post(self, tenant_id: str, user: Account, provider: str, subscription_id: str): """Verify credentials for an existing subscription (edit mode only)""" - user = current_user - assert user.current_tenant_id is not None - verify_request = TriggerSubscriptionBuilderVerifyPayload.model_validate(console_ns.payload or {}) try: result = TriggerProviderService.verify_subscription_credentials( - tenant_id=user.current_tenant_id, + tenant_id=tenant_id, user_id=user.id, provider_id=TriggerProviderID(provider), subscription_id=subscription_id, diff --git a/api/libs/login.py b/api/libs/login.py index 12d0f53f2d6..bbb8ba1611c 100644 --- a/api/libs/login.py +++ b/api/libs/login.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any, Concatenate, cast, overload from flask import Response, current_app, g, has_request_context, request from flask_login.config import EXEMPT_METHODS +from werkzeug.exceptions import Unauthorized from werkzeug.local import LocalProxy from configs import dify_config @@ -48,6 +49,53 @@ def current_account_with_tenant() -> tuple[Account, str]: return user, user.current_tenant_id +def current_account_with_tenant_optional() -> tuple[Account | None, str | None]: + try: + user = _resolve_current_user() + except Unauthorized: + return None, None + + if not isinstance(user, Account): + return None, None + if not bool(getattr(user, "is_authenticated", False)): + return None, None + return user, user.current_tenant_id + + +def resolve_account_fallback( + current_user: Account | None = None, + current_tenant_id: str | None = None, + *, + fallback_tenant_id: str | None = None, +) -> tuple[Account, str]: + """ + If the provided current user and tenant ID is None, fallback to current_account_with_tenant. + This is useful for those service layers whose controllers are not migrated to use DI for + resolving current user yet. + + TODO: this should be removed after all ctrls (especially service API) are migrated + """ + if current_user is not None: + tenant_id = current_tenant_id or fallback_tenant_id + if tenant_id is None: + raise ValueError("current_tenant_id is required when current_user is provided.") + return current_user, tenant_id + return current_account_with_tenant() + + +def resolve_tenant_id_fallback(current_tenant_id: str | None = None) -> str: + """ + If the provided tenant ID is None, fallback to the tenant resolved from current_account_with_tenant. + This is useful for tenant-only service paths whose controllers are not all migrated to tenant injection yet. + + TODO: this should be removed after all ctrls (especially service API) are migrated + """ + if current_tenant_id is not None: + return current_tenant_id + _, tenant_id = current_account_with_tenant() + return tenant_id + + @overload def login_required[T, **P, R]( func: Callable[Concatenate[T, P], R], diff --git a/api/services/metadata_service.py b/api/services/metadata_service.py index 672f309bac0..f9dcfd25c7f 100644 --- a/api/services/metadata_service.py +++ b/api/services/metadata_service.py @@ -7,7 +7,8 @@ from core.rag.index_processor.constant.built_in_field import BuiltInField, Metad from extensions.ext_database import db from extensions.ext_redis import redis_client from libs.datetime_utils import naive_utc_now -from libs.login import current_account_with_tenant +from libs.login import resolve_account_fallback +from models import Account from models.dataset import Dataset, DatasetMetadata, DatasetMetadataBinding from models.enums import DatasetMetadataType from services.dataset_service import DocumentService @@ -21,11 +22,16 @@ logger = logging.getLogger(__name__) class MetadataService: @staticmethod - def create_metadata(dataset_id: str, metadata_args: MetadataArgs) -> DatasetMetadata: + def create_metadata( + dataset_id: str, + metadata_args: MetadataArgs, + current_user: Account | None = None, # TODO: the service_api is not migrated yet + current_tenant_id: str | None = None, + ) -> DatasetMetadata: # check if metadata name is too long if len(metadata_args.name) > 255: raise ValueError("Metadata name cannot exceed 255 characters.") - current_user, current_tenant_id = current_account_with_tenant() + current_user, current_tenant_id = resolve_account_fallback(current_user, current_tenant_id) # check if metadata name already exists if db.session.scalar( select(DatasetMetadata) @@ -52,14 +58,20 @@ class MetadataService: return metadata @staticmethod - def update_metadata_name(dataset_id: str, metadata_id: str, name: str) -> DatasetMetadata: # type: ignore + def update_metadata_name( + dataset_id: str, + metadata_id: str, + name: str, + current_user: Account | None = None, + current_tenant_id: str | None = None, # TODO: the service_api is not migrated yet + ) -> DatasetMetadata | None: # check if metadata name is too long if len(name) > 255: raise ValueError("Metadata name cannot exceed 255 characters.") lock_key = f"dataset_metadata_lock_{dataset_id}" # check if metadata name already exists - current_user, current_tenant_id = current_account_with_tenant() + current_user, current_tenant_id = resolve_account_fallback(current_user, current_tenant_id) if db.session.scalar( select(DatasetMetadata) .where( @@ -107,6 +119,7 @@ class MetadataService: return metadata except Exception: logger.exception("Update metadata name failed") + return None finally: redis_client.delete(lock_key) @@ -217,7 +230,15 @@ class MetadataService: redis_client.delete(lock_key) @staticmethod - def update_documents_metadata(dataset: Dataset, metadata_args: MetadataOperationData): + def update_documents_metadata( + dataset: Dataset, + metadata_args: MetadataOperationData, + current_user: Account | None = None, # TODO: the service_api is not migrated yet + current_tenant_id: str | None = None, + ): + current_user, current_tenant_id = resolve_account_fallback( + current_user, current_tenant_id, fallback_tenant_id=dataset.tenant_id + ) for operation in metadata_args.operation_data: lock_key = f"document_metadata_lock_{operation.document_id}" try: @@ -248,7 +269,6 @@ class MetadataService: ) ) - current_user, current_tenant_id = current_account_with_tenant() for metadata_value in operation.metadata_list: # check if binding already exists if operation.partial_update: diff --git a/api/services/rag_pipeline/pipeline_template/built_in/built_in_retrieval.py b/api/services/rag_pipeline/pipeline_template/built_in/built_in_retrieval.py index 3ba7593be53..4e4cf2d19f5 100644 --- a/api/services/rag_pipeline/pipeline_template/built_in/built_in_retrieval.py +++ b/api/services/rag_pipeline/pipeline_template/built_in/built_in_retrieval.py @@ -21,7 +21,8 @@ class BuiltInPipelineTemplateRetrieval(PipelineTemplateRetrievalBase): return PipelineTemplateType.BUILTIN @override - def get_pipeline_templates(self, language: str) -> dict[str, Any]: + def get_pipeline_templates(self, language: str, current_tenant_id: str | None = None) -> dict[str, Any]: + del current_tenant_id result = self.fetch_pipeline_templates_from_builtin(language) return result diff --git a/api/services/rag_pipeline/pipeline_template/customized/customized_retrieval.py b/api/services/rag_pipeline/pipeline_template/customized/customized_retrieval.py index ee73b0328f5..57dfefed2e0 100644 --- a/api/services/rag_pipeline/pipeline_template/customized/customized_retrieval.py +++ b/api/services/rag_pipeline/pipeline_template/customized/customized_retrieval.py @@ -4,7 +4,7 @@ import yaml from sqlalchemy import select from extensions.ext_database import db -from libs.login import current_account_with_tenant +from libs.login import resolve_tenant_id_fallback from models.dataset import PipelineCustomizedTemplate from services.rag_pipeline.pipeline_template.pipeline_template_base import PipelineTemplateRetrievalBase from services.rag_pipeline.pipeline_template.pipeline_template_type import PipelineTemplateType @@ -40,8 +40,8 @@ class CustomizedPipelineTemplateRetrieval(PipelineTemplateRetrievalBase): """ @override - def get_pipeline_templates(self, language: str) -> dict[str, Any]: - _, current_tenant_id = current_account_with_tenant() + def get_pipeline_templates(self, language: str, current_tenant_id: str | None = None) -> dict[str, Any]: + current_tenant_id = resolve_tenant_id_fallback(current_tenant_id) return self.fetch_pipeline_templates_from_customized(tenant_id=current_tenant_id, language=language) @override diff --git a/api/services/rag_pipeline/pipeline_template/database/database_retrieval.py b/api/services/rag_pipeline/pipeline_template/database/database_retrieval.py index 9c94fdee2b0..0f6d0727c76 100644 --- a/api/services/rag_pipeline/pipeline_template/database/database_retrieval.py +++ b/api/services/rag_pipeline/pipeline_template/database/database_retrieval.py @@ -40,7 +40,8 @@ class DatabasePipelineTemplateRetrieval(PipelineTemplateRetrievalBase): """ @override - def get_pipeline_templates(self, language: str) -> dict[str, Any]: + def get_pipeline_templates(self, language: str, current_tenant_id: str | None = None) -> dict[str, Any]: + del current_tenant_id return self.fetch_pipeline_templates_from_db(language) @override diff --git a/api/services/rag_pipeline/pipeline_template/pipeline_template_base.py b/api/services/rag_pipeline/pipeline_template/pipeline_template_base.py index 9cfb8f36aa7..84d8f5674bb 100644 --- a/api/services/rag_pipeline/pipeline_template/pipeline_template_base.py +++ b/api/services/rag_pipeline/pipeline_template/pipeline_template_base.py @@ -4,7 +4,7 @@ from typing import Any, Protocol class PipelineTemplateRetrievalBase(Protocol): """Interface for pipeline template retrieval.""" - def get_pipeline_templates(self, language: str) -> dict[str, Any]: ... + def get_pipeline_templates(self, language: str, current_tenant_id: str | None = None) -> dict[str, Any]: ... def get_pipeline_template_detail(self, template_id: str) -> dict[str, Any] | None: ... diff --git a/api/services/rag_pipeline/pipeline_template/remote/remote_retrieval.py b/api/services/rag_pipeline/pipeline_template/remote/remote_retrieval.py index 1be97c2888d..5cf46915ab0 100644 --- a/api/services/rag_pipeline/pipeline_template/remote/remote_retrieval.py +++ b/api/services/rag_pipeline/pipeline_template/remote/remote_retrieval.py @@ -25,7 +25,8 @@ class RemotePipelineTemplateRetrieval(PipelineTemplateRetrievalBase): return DatabasePipelineTemplateRetrieval.fetch_pipeline_template_detail_from_db(template_id) @override - def get_pipeline_templates(self, language: str) -> dict[str, Any]: + def get_pipeline_templates(self, language: str, current_tenant_id: str | None = None) -> dict[str, Any]: + del current_tenant_id try: return self.fetch_pipeline_templates_from_dify_official(language) except Exception as e: diff --git a/api/services/rag_pipeline/rag_pipeline.py b/api/services/rag_pipeline/rag_pipeline.py index fd02a44995d..abab174b3d9 100644 --- a/api/services/rag_pipeline/rag_pipeline.py +++ b/api/services/rag_pipeline/rag_pipeline.py @@ -8,7 +8,6 @@ from datetime import UTC, datetime from typing import Any, cast from uuid import uuid4 -from flask_login import current_user from sqlalchemy import func, select from sqlalchemy.orm import Session, sessionmaker @@ -54,6 +53,7 @@ from graphon.nodes.http_request import HTTP_REQUEST_CONFIG_FILTER_KEY, build_htt from graphon.runtime import VariablePool from graphon.variables.variables import Variable, VariableBase from libs.infinite_scroll_pagination import InfiniteScrollPagination +from libs.login import resolve_account_fallback, resolve_tenant_id_fallback from models import Account from models.dataset import ( # type: ignore Dataset, @@ -104,11 +104,16 @@ class RagPipelineService: self._workflow_run_repo = DifyAPIRepositoryFactory.create_api_workflow_run_repository(session_maker) @classmethod - def get_pipeline_templates(cls, type: str = "built-in", language: str = "en-US") -> dict[str, Any]: + def get_pipeline_templates( + cls, + type: str = "built-in", + language: str = "en-US", + current_tenant_id: str | None = None, + ) -> dict[str, Any]: if type == "built-in": mode = dify_config.HOSTED_FETCH_PIPELINE_TEMPLATES_MODE retrieval_instance = PipelineTemplateRetrievalFactory.get_pipeline_template_factory(mode)() - result = retrieval_instance.get_pipeline_templates(language) + result = retrieval_instance.get_pipeline_templates(language, current_tenant_id) if not result.get("pipeline_templates") and language != "en-US": template_retrieval = PipelineTemplateRetrievalFactory.get_built_in_pipeline_template_retrieval() result = template_retrieval.fetch_pipeline_templates_from_builtin("en-US") @@ -116,7 +121,7 @@ class RagPipelineService: else: mode = "customized" retrieval_instance = PipelineTemplateRetrievalFactory.get_pipeline_template_factory(mode)() - result = retrieval_instance.get_pipeline_templates(language) + result = retrieval_instance.get_pipeline_templates(language, current_tenant_id) return result @classmethod @@ -146,17 +151,24 @@ class RagPipelineService: return customized_result @classmethod - def update_customized_pipeline_template(cls, template_id: str, template_info: PipelineTemplateInfoEntity): + def update_customized_pipeline_template( + cls, + template_id: str, + template_info: PipelineTemplateInfoEntity, + current_user: Account | None = None, + current_tenant_id: str | None = None, + ): """ Update pipeline template. :param template_id: template id :param template_info: template info """ + current_user, current_tenant_id = resolve_account_fallback(current_user, current_tenant_id) customized_template: PipelineCustomizedTemplate | None = db.session.scalar( select(PipelineCustomizedTemplate) .where( PipelineCustomizedTemplate.id == template_id, - PipelineCustomizedTemplate.tenant_id == current_user.current_tenant_id, + PipelineCustomizedTemplate.tenant_id == current_tenant_id, ) .limit(1) ) @@ -169,7 +181,7 @@ class RagPipelineService: select(PipelineCustomizedTemplate) .where( PipelineCustomizedTemplate.name == template_name, - PipelineCustomizedTemplate.tenant_id == current_user.current_tenant_id, + PipelineCustomizedTemplate.tenant_id == current_tenant_id, PipelineCustomizedTemplate.id != template_id, ) .limit(1) @@ -184,15 +196,16 @@ class RagPipelineService: return customized_template @classmethod - def delete_customized_pipeline_template(cls, template_id: str): + def delete_customized_pipeline_template(cls, template_id: str, current_tenant_id: str | None = None): """ Delete customized pipeline template. """ + current_tenant_id = resolve_tenant_id_fallback(current_tenant_id) customized_template: PipelineCustomizedTemplate | None = db.session.scalar( select(PipelineCustomizedTemplate) .where( PipelineCustomizedTemplate.id == template_id, - PipelineCustomizedTemplate.tenant_id == current_user.current_tenant_id, + PipelineCustomizedTemplate.tenant_id == current_tenant_id, ) .limit(1) ) @@ -1174,10 +1187,17 @@ class RagPipelineService: return list(node_executions) @classmethod - def publish_customized_pipeline_template(cls, pipeline_id: str, args: dict[str, Any]): + def publish_customized_pipeline_template( + cls, + pipeline_id: str, + args: dict[str, Any], + current_user: Account | None = None, + current_tenant_id: str | None = None, + ): """ Publish customized pipeline template """ + current_user, _ = resolve_account_fallback(current_user, current_tenant_id) pipeline = db.session.get(Pipeline, pipeline_id) if not pipeline: raise ValueError("Pipeline not found") @@ -1357,7 +1377,7 @@ class RagPipelineService: return [] return marketplace.batch_fetch_plugin_by_ids(plugin_ids) - def get_recommended_plugins(self, type: str) -> dict[str, Any]: + def get_recommended_plugins(self, type: str, current_user: Account, current_tenant_id: str) -> dict[str, Any]: # Query active recommended plugins stmt = select(PipelineRecommendedPlugin).where(PipelineRecommendedPlugin.active == True) if type and type != "all": @@ -1375,7 +1395,7 @@ class RagPipelineService: plugin_ids = [plugin.plugin_id for plugin in pipeline_recommended_plugins] providers = BuiltinToolManageService.list_builtin_tools( user_id=current_user.id, - tenant_id=current_user.current_tenant_id, + tenant_id=current_tenant_id, ) providers_map = {provider.plugin_id: provider.to_dict() for provider in providers} diff --git a/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py b/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py index 027356b6278..75b0d3c5002 100644 --- a/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py +++ b/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable +from inspect import unwrap from typing import cast from unittest.mock import MagicMock, patch from uuid import uuid4 @@ -20,8 +21,8 @@ from controllers.console.datasets.rag_pipeline.rag_pipeline import ( PipelineTemplateListApi, PublishCustomizedPipelineTemplateApi, ) +from models.account import Account from models.dataset import PipelineCustomizedTemplate -from tests.test_containers_integration_tests.controllers.console.helpers import unwrap class TestPipelineTemplateListApi: @@ -53,7 +54,7 @@ class TestPipelineTemplateListApi: return_value=templates, ), ): - response, status = method(api) + response, status = method(api, str(uuid4())) assert status == 200 assert response == { @@ -147,6 +148,9 @@ class TestCustomizedPipelineTemplateApi: def test_patch_success(self, app: Flask) -> None: api = CustomizedPipelineTemplateApi() method = unwrap(api.patch) + account = Account(name="Test User", email="test@example.com") + account.id = str(uuid4()) + tenant_id = str(uuid4()) payload = { "name": "Template", @@ -161,15 +165,18 @@ class TestCustomizedPipelineTemplateApi: "controllers.console.datasets.rag_pipeline.rag_pipeline.RagPipelineService.update_customized_pipeline_template" ) as update_mock, ): - response, status = method(api, "tpl-1") + response, status = method(api, tenant_id, account, "tpl-1") update_mock.assert_called_once() + assert update_mock.call_args.args[2] is account + assert update_mock.call_args.args[3] == tenant_id assert status == 204 assert response == "" def test_delete_success(self, app: Flask) -> None: api = CustomizedPipelineTemplateApi() method = unwrap(api.delete) + tenant_id = str(uuid4()) with ( app.test_request_context("/"), @@ -177,9 +184,9 @@ class TestCustomizedPipelineTemplateApi: "controllers.console.datasets.rag_pipeline.rag_pipeline.RagPipelineService.delete_customized_pipeline_template" ) as delete_mock, ): - response, status = method(api, "tpl-1") + response, status = method(api, tenant_id, "tpl-1") - delete_mock.assert_called_once_with("tpl-1") + delete_mock.assert_called_once_with("tpl-1", tenant_id) assert status == 204 assert response == "" @@ -227,6 +234,9 @@ class TestPublishCustomizedPipelineTemplateApi: def test_post_success(self, app: Flask) -> None: api = PublishCustomizedPipelineTemplateApi() method = unwrap(api.post) + account = Account(name="Test User", email="test@example.com") + account.id = str(uuid4()) + tenant_id = str(uuid4()) payload = { "name": "Template", @@ -244,8 +254,10 @@ class TestPublishCustomizedPipelineTemplateApi: return_value=service, ), ): - response, status = method(api, "pipeline-1") + response, status = method(api, tenant_id, account, "pipeline-1") service.publish_customized_pipeline_template.assert_called_once() + assert service.publish_customized_pipeline_template.call_args.args[2] is account + assert service.publish_customized_pipeline_template.call_args.args[3] == tenant_id assert status == 204 assert response == "" diff --git a/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py b/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py index d1d8e6fd757..bdec903ef33 100644 --- a/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py +++ b/api/tests/test_containers_integration_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_workflow.py @@ -5,7 +5,6 @@ from __future__ import annotations import json from datetime import datetime from inspect import unwrap -from types import SimpleNamespace from typing import TypedDict, Unpack from unittest.mock import MagicMock, patch from uuid import uuid4 @@ -35,12 +34,15 @@ from controllers.console.datasets.rag_pipeline.rag_pipeline_workflow import ( RagPipelineTaskStopApi, RagPipelineTransformApi, RagPipelineWorkflowLastRunApi, + RagPipelineWorkflowRunNodeExecutionListApi, ) from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError +from graphon.enums import WorkflowNodeExecutionStatus from libs.datetime_utils import naive_utc_now from models.account import Account, TenantAccountRole from models.dataset import Pipeline -from models.workflow import Workflow +from models.enums import CreatorUserRole +from models.workflow import Workflow, WorkflowNodeExecutionModel, WorkflowNodeExecutionTriggeredFrom from services.errors.app import IsDraftWorkflowError, WorkflowHashNotEqualError, WorkflowNotFoundError from services.errors.llm import InvokeRateLimitError @@ -71,7 +73,7 @@ class WorkflowFactoryPayload(TypedDict): created_by: str created_at: datetime updated_by: str | None - updated_at: datetime + updated_at: datetime | None environment_variables: list[WorkflowVariablePayload] conversation_variables: list[WorkflowVariablePayload] rag_pipeline_variables: list[WorkflowVariablePayload] @@ -90,39 +92,69 @@ class WorkflowFactoryOverrides(TypedDict, total=False): created_by: str created_at: datetime updated_by: str | None - updated_at: datetime + updated_at: datetime | None environment_variables: list[WorkflowVariablePayload] conversation_variables: list[WorkflowVariablePayload] rag_pipeline_variables: list[WorkflowVariablePayload] -def make_node_execution(**overrides: object) -> SimpleNamespace: - payload: dict[str, object] = { +class NodeExecutionOverrides(TypedDict, total=False): + id: str + tenant_id: str + app_id: str + workflow_id: str + workflow_run_id: str | None + index: int + predecessor_node_id: str | None + node_execution_id: str | None + node_id: str + node_type: str + title: str + inputs: str | None + process_data: str | None + outputs: str | None + status: WorkflowNodeExecutionStatus + error: str | None + elapsed_time: float + execution_metadata: str | None + created_at: datetime + created_by_role: CreatorUserRole + created_by: str + finished_at: datetime | None + + +def make_node_execution(**overrides: Unpack[NodeExecutionOverrides]) -> WorkflowNodeExecutionModel: + payload: NodeExecutionOverrides = { "id": "node-exec-1", + "tenant_id": DEFAULT_WORKFLOW_TENANT_ID, + "app_id": DEFAULT_WORKFLOW_APP_ID, + "workflow_id": "workflow-1", + "workflow_run_id": None, "index": 1, "predecessor_node_id": None, + "node_execution_id": None, "node_id": "node1", "node_type": "start", "title": "Start", - "inputs_dict": {"query": "hello"}, - "process_data_dict": {}, - "outputs_dict": {"answer": "world"}, - "status": "succeeded", + "inputs": json.dumps({"query": "hello"}), + "process_data": json.dumps({}), + "outputs": json.dumps({"answer": "world"}), + "status": WorkflowNodeExecutionStatus.SUCCEEDED, "error": None, "elapsed_time": 1.0, - "execution_metadata_dict": {}, - "extras": {}, + "execution_metadata": json.dumps({}), "created_at": datetime(2026, 1, 1, 0, 0, 0), - "created_by_role": "account", - "created_by_account": None, - "created_by_end_user": None, + "created_by_role": CreatorUserRole.ACCOUNT, + "created_by": DEFAULT_WORKFLOW_CREATED_BY, "finished_at": datetime(2026, 1, 1, 0, 0, 1), - "inputs_truncated": False, - "outputs_truncated": False, - "process_data_truncated": False, } payload.update(overrides) - return SimpleNamespace(**payload) + execution = WorkflowNodeExecutionModel( + triggered_from=WorkflowNodeExecutionTriggeredFrom.RAG_PIPELINE_RUN, + **payload, + ) + execution.offload_data = [] + return execution def default_workflow_payload() -> WorkflowFactoryPayload: @@ -274,7 +306,10 @@ class TestDraftWorkflowApi: pipeline = make_pipeline() user = make_account(id="account-1") - workflow = MagicMock(unique_hash="restored-hash", updated_at=None, created_at=datetime(2024, 1, 1)) + workflow = make_workflow( + graph=json.dumps({"nodes": [{"id": "restored"}], "edges": []}), + created_at=datetime(2024, 1, 1), + ) service = MagicMock() service.restore_published_workflow_to_draft.return_value = workflow @@ -289,7 +324,7 @@ class TestDraftWorkflowApi: result = method(api, user, pipeline, "published-workflow") assert result["result"] == "success" - assert result["hash"] == "restored-hash" + assert result["hash"] == workflow.unique_hash def test_restore_published_workflow_to_draft_not_found(self, app: Flask) -> None: api = RagPipelineDraftWorkflowRestoreApi() @@ -515,10 +550,7 @@ class TestPublishedPipelineApis: user = make_account(id="u1") - workflow = MagicMock( - id=str(uuid4()), - created_at=naive_utc_now(), - ) + workflow = make_workflow(id=str(uuid4()), created_at=naive_utc_now()) service = MagicMock() service.publish_workflow.return_value = workflow @@ -576,6 +608,8 @@ class TestMiscApis: service = MagicMock() service.get_recommended_plugins.return_value = [{"id": "p1"}] + user = make_account() + tenant_id = "tenant-1" with ( app.test_request_context("/?type=all"), @@ -584,8 +618,9 @@ class TestMiscApis: return_value=service, ), ): - result = method(api) + result = method(api, tenant_id, user) assert result == [{"id": "p1"}] + service.get_recommended_plugins.assert_called_once_with("all", user, tenant_id) class TestPublishedRagPipelineRunApi: @@ -814,7 +849,7 @@ class TestRagPipelineWorkflowLastRunApi: method = unwrap(api.get) pipeline = make_pipeline() - workflow = MagicMock() + workflow = make_workflow() node_exec = make_node_execution() service = MagicMock() @@ -853,6 +888,42 @@ class TestRagPipelineWorkflowLastRunApi: method(api, pipeline, "node1") +class TestRagPipelineWorkflowRunNodeExecutionListApi: + @pytest.fixture + def app(self, flask_app_with_containers: Flask) -> Flask: + return flask_app_with_containers + + def test_get_node_executions_passes_current_user(self, app: Flask) -> None: + api = RagPipelineWorkflowRunNodeExecutionListApi() + method = unwrap(api.get) + + user = make_account() + pipeline = make_pipeline() + run_id = uuid4() + node_exec = make_node_execution(workflow_run_id=str(run_id)) + + service = MagicMock() + service.get_rag_pipeline_workflow_run_node_executions.return_value = [node_exec] + + with ( + app.test_request_context("/"), + patch( + "controllers.console.datasets.rag_pipeline.rag_pipeline_workflow.RagPipelineService", + return_value=service, + ), + ): + result = method(api, user, pipeline, run_id) + + service.get_rag_pipeline_workflow_run_node_executions.assert_called_once_with( + pipeline=pipeline, + run_id=str(run_id), + user=user, + ) + assert result["data"][0]["id"] == "node-exec-1" + assert result["data"][0]["inputs"] == {"query": "hello"} + assert result["data"][0]["outputs"] == {"answer": "world"} + + class TestRagPipelineDatasourceVariableApi: @pytest.fixture def app(self, flask_app_with_containers: Flask) -> Flask: diff --git a/api/tests/test_containers_integration_tests/controllers/console/explore/test_conversation.py b/api/tests/test_containers_integration_tests/controllers/console/explore/test_conversation.py index b5f5917ee99..3d5fce4b6ca 100644 --- a/api/tests/test_containers_integration_tests/controllers/console/explore/test_conversation.py +++ b/api/tests/test_containers_integration_tests/controllers/console/explore/test_conversation.py @@ -2,7 +2,10 @@ from __future__ import annotations -from unittest.mock import MagicMock, patch +from dataclasses import dataclass +from inspect import unwrap +from typing import cast +from unittest.mock import patch import pytest from flask import Flask @@ -10,85 +13,103 @@ from werkzeug.exceptions import NotFound import controllers.console.explore.conversation as conversation_module from controllers.console.explore.error import NotChatAppError +from libs.infinite_scroll_pagination import InfiniteScrollPagination from models import Account -from models.model import AppMode +from models.enums import ConversationFromSource, ConversationStatus +from models.model import App, AppMode, Conversation, InstalledApp from services.errors.conversation import ( ConversationNotExistsError, LastConversationNotExistsError, ) -def unwrap(func): - while hasattr(func, "__wrapped__"): - func = func.__wrapped__ - return func - - -class FakeConversation: - def __init__(self, cid): - self.id = cid - self.name = "test" - self.inputs = {} - self.status = "normal" - self.introduction = "" +@dataclass +class InstalledAppCarrier: + app: App | None @pytest.fixture -def chat_app(): - app_model = MagicMock(mode=AppMode.CHAT, id="app-id") - return MagicMock(app=app_model) +def chat_app() -> InstalledApp: + app_model = App( + tenant_id="tenant-1", + name="Chat App", + mode=AppMode.CHAT, + enable_site=True, + enable_api=False, + ) + app_model.id = "app-id" + return cast(InstalledApp, InstalledAppCarrier(app=app_model)) @pytest.fixture -def non_chat_app(): - app_model = MagicMock(mode=AppMode.COMPLETION) - return MagicMock(app=app_model) +def non_chat_app() -> InstalledApp: + app_model = App( + tenant_id="tenant-1", + name="Completion App", + mode=AppMode.COMPLETION, + enable_site=True, + enable_api=False, + ) + app_model.id = "app-id" + return cast(InstalledApp, InstalledAppCarrier(app=app_model)) + + +def make_conversation(*, id: str) -> Conversation: + conversation = Conversation( + app_id="app-id", + mode=AppMode.CHAT, + name="test", + from_source=ConversationFromSource.API, + ) + conversation.id = id + conversation.inputs = {} + conversation.status = ConversationStatus.NORMAL + conversation.introduction = "" + return conversation @pytest.fixture -def user(): - user = MagicMock(spec=Account) +def user() -> Account: + user = Account(name="User", email="user.com") user.id = "uid" return user class TestConversationListApi: @pytest.fixture - def app(self, flask_app_with_containers: Flask): + def app(self, flask_app_with_containers: Flask) -> Flask: return flask_app_with_containers - def test_get_success(self, app: Flask, chat_app, user): + def test_get_success(self, app: Flask, chat_app: InstalledApp, user: Account) -> None: api = conversation_module.ConversationListApi() method = unwrap(api.get) - pagination = MagicMock( + pagination = InfiniteScrollPagination( + data=[make_conversation(id="c1"), make_conversation(id="c2")], limit=20, has_more=False, - data=[FakeConversation("c1"), FakeConversation("c2")], ) with ( app.test_request_context("/?limit=20"), - patch.object(conversation_module, "current_user", user), patch.object( conversation_module.WebConversationService, "pagination_by_last_id", return_value=pagination, ), ): - result = method(chat_app) + result = method(api, user, chat_app) assert result["limit"] == 20 assert result["has_more"] is False assert len(result["data"]) == 2 - def test_last_conversation_not_exists(self, app: Flask, chat_app, user): + def test_last_conversation_not_exists(self, app: Flask, chat_app: InstalledApp, user: Account) -> None: api = conversation_module.ConversationListApi() method = unwrap(api.get) with ( app.test_request_context("/"), - patch.object(conversation_module, "current_user", user), patch.object( conversation_module.WebConversationService, "pagination_by_last_id", @@ -96,47 +117,45 @@ class TestConversationListApi: ), ): with pytest.raises(NotFound): - method(chat_app) + method(api, user, chat_app) - def test_wrong_app_mode(self, app: Flask, non_chat_app): + def test_wrong_app_mode(self, app: Flask, non_chat_app: InstalledApp, user: Account) -> None: api = conversation_module.ConversationListApi() method = unwrap(api.get) with app.test_request_context("/"): with pytest.raises(NotChatAppError): - method(non_chat_app) + method(api, user, non_chat_app) class TestConversationApi: @pytest.fixture - def app(self, flask_app_with_containers: Flask): + def app(self, flask_app_with_containers: Flask) -> Flask: return flask_app_with_containers - def test_delete_success(self, app: Flask, chat_app, user): + def test_delete_success(self, app: Flask, chat_app: InstalledApp, user: Account) -> None: api = conversation_module.ConversationApi() method = unwrap(api.delete) with ( app.test_request_context("/"), - patch.object(conversation_module, "current_user", user), patch.object( conversation_module.ConversationService, "delete", ), ): - result = method(chat_app, "cid") + result = method(api, user, chat_app, "cid") body, status = result assert status == 204 assert body == "" - def test_delete_not_found(self, app: Flask, chat_app, user): + def test_delete_not_found(self, app: Flask, chat_app: InstalledApp, user: Account) -> None: api = conversation_module.ConversationApi() method = unwrap(api.delete) with ( app.test_request_context("/"), - patch.object(conversation_module, "current_user", user), patch.object( conversation_module.ConversationService, "delete", @@ -144,48 +163,46 @@ class TestConversationApi: ), ): with pytest.raises(NotFound): - method(chat_app, "cid") + method(api, user, chat_app, "cid") - def test_delete_wrong_app_mode(self, app: Flask, non_chat_app): + def test_delete_wrong_app_mode(self, app: Flask, non_chat_app: InstalledApp, user: Account) -> None: api = conversation_module.ConversationApi() method = unwrap(api.delete) with app.test_request_context("/"): with pytest.raises(NotChatAppError): - method(non_chat_app, "cid") + method(api, user, non_chat_app, "cid") class TestConversationRenameApi: @pytest.fixture - def app(self, flask_app_with_containers: Flask): + def app(self, flask_app_with_containers: Flask) -> Flask: return flask_app_with_containers - def test_rename_success(self, app: Flask, chat_app, user): + def test_rename_success(self, app: Flask, chat_app: InstalledApp, user: Account) -> None: api = conversation_module.ConversationRenameApi() method = unwrap(api.post) - conversation = FakeConversation("cid") + conversation = make_conversation(id="cid") with ( app.test_request_context("/", json={"name": "new"}), - patch.object(conversation_module, "current_user", user), patch.object( conversation_module.ConversationService, "rename", return_value=conversation, ), ): - result = method(chat_app, "cid") + result = method(api, user, chat_app, "cid") assert result["id"] == "cid" - def test_rename_not_found(self, app: Flask, chat_app, user): + def test_rename_not_found(self, app: Flask, chat_app: InstalledApp, user: Account) -> None: api = conversation_module.ConversationRenameApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"name": "new"}), - patch.object(conversation_module, "current_user", user), patch.object( conversation_module.ConversationService, "rename", @@ -193,48 +210,46 @@ class TestConversationRenameApi: ), ): with pytest.raises(NotFound): - method(chat_app, "cid") + method(api, user, chat_app, "cid") class TestConversationPinApi: @pytest.fixture - def app(self, flask_app_with_containers: Flask): + def app(self, flask_app_with_containers: Flask) -> Flask: return flask_app_with_containers - def test_pin_success(self, app: Flask, chat_app, user): + def test_pin_success(self, app: Flask, chat_app: InstalledApp, user: Account) -> None: api = conversation_module.ConversationPinApi() method = unwrap(api.patch) with ( app.test_request_context("/"), - patch.object(conversation_module, "current_user", user), patch.object( conversation_module.WebConversationService, "pin", ), ): - result = method(chat_app, "cid") + result = method(api, user, chat_app, "cid") assert result == {"result": "success"} class TestConversationUnPinApi: @pytest.fixture - def app(self, flask_app_with_containers: Flask): + def app(self, flask_app_with_containers: Flask) -> Flask: return flask_app_with_containers - def test_unpin_success(self, app: Flask, chat_app, user): + def test_unpin_success(self, app: Flask, chat_app: InstalledApp, user: Account) -> None: api = conversation_module.ConversationUnPinApi() method = unwrap(api.patch) with ( app.test_request_context("/"), - patch.object(conversation_module, "current_user", user), patch.object( conversation_module.WebConversationService, "unpin", ), ): - result = method(chat_app, "cid") + result = method(api, user, chat_app, "cid") assert result == {"result": "success"} diff --git a/api/tests/test_containers_integration_tests/controllers/console/workspace/test_trigger_providers.py b/api/tests/test_containers_integration_tests/controllers/console/workspace/test_trigger_providers.py index 6c74b3193b9..6684381880c 100644 --- a/api/tests/test_containers_integration_tests/controllers/console/workspace/test_trigger_providers.py +++ b/api/tests/test_containers_integration_tests/controllers/console/workspace/test_trigger_providers.py @@ -2,6 +2,7 @@ from __future__ import annotations +from inspect import unwrap from unittest.mock import MagicMock, patch import pytest @@ -31,123 +32,110 @@ from core.plugin.entities.plugin_daemon import CredentialType from models.account import Account -def unwrap(func): - while hasattr(func, "__wrapped__"): - func = func.__wrapped__ - return func - - -def mock_user(): - user = MagicMock(spec=Account) +def mock_user() -> Account: + user = Account(name="User", email="user.com") user.id = "u1" - user.current_tenant_id = "t1" return user class TestTriggerProviderApis: @pytest.fixture - def app(self, flask_app_with_containers: Flask): + def app(self, flask_app_with_containers: Flask) -> Flask: return flask_app_with_containers - def test_icon_success(self, app: Flask): + def test_icon_success(self, app: Flask) -> None: api = TriggerProviderIconApi() method = unwrap(api.get) with ( app.test_request_context("/"), - patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch( "controllers.console.workspace.trigger_providers.TriggerManager.get_trigger_plugin_icon", return_value="icon", ), ): - assert method(api, "github") == "icon" + assert method(api, "t1", "github") == "icon" - def test_list_providers(self, app: Flask): + def test_list_providers(self, app: Flask) -> None: api = TriggerProviderListApi() method = unwrap(api.get) with ( app.test_request_context("/"), - patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch( "controllers.console.workspace.trigger_providers.TriggerProviderService.list_trigger_providers", return_value=[], ), ): - assert method(api) == [] + assert method(api, "t1") == [] - def test_provider_info(self, app: Flask): + def test_provider_info(self, app: Flask) -> None: api = TriggerProviderInfoApi() method = unwrap(api.get) with ( app.test_request_context("/"), - patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch( "controllers.console.workspace.trigger_providers.TriggerProviderService.get_trigger_provider", return_value={"id": "p1"}, ), ): - assert method(api, "github") == {"id": "p1"} + assert method(api, "t1", "github") == {"id": "p1"} class TestTriggerSubscriptionListApi: @pytest.fixture - def app(self, flask_app_with_containers: Flask): + def app(self, flask_app_with_containers: Flask) -> Flask: return flask_app_with_containers - def test_list_success(self, app: Flask): + def test_list_success(self, app: Flask) -> None: api = TriggerSubscriptionListApi() method = unwrap(api.get) with ( app.test_request_context("/"), - patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch( "controllers.console.workspace.trigger_providers.TriggerProviderService.list_trigger_provider_subscriptions", return_value=[], ), ): - assert method(api, "github") == [] + assert method(api, "t1", mock_user(), "github") == [] - def test_list_invalid_provider(self, app: Flask): + def test_list_invalid_provider(self, app: Flask) -> None: api = TriggerSubscriptionListApi() method = unwrap(api.get) with ( app.test_request_context("/"), - patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch( "controllers.console.workspace.trigger_providers.TriggerProviderService.list_trigger_provider_subscriptions", side_effect=ValueError("bad"), ), ): - result, status = method(api, "bad") + result, status = method(api, "t1", mock_user(), "bad") assert status == 404 class TestTriggerSubscriptionBuilderApis: @pytest.fixture - def app(self, flask_app_with_containers: Flask): + def app(self, flask_app_with_containers: Flask) -> Flask: return flask_app_with_containers - def test_create_builder(self, app: Flask): + def test_create_builder(self, app: Flask) -> None: api = TriggerSubscriptionBuilderCreateApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"credential_type": "UNAUTHORIZED"}), - patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch( "controllers.console.workspace.trigger_providers.TriggerSubscriptionBuilderService.create_trigger_subscription_builder", return_value={"id": "b1"}, ), ): - result = method(api, "github") + result = method(api, "t1", mock_user(), "github") assert "subscription_builder" in result - def test_get_builder(self, app: Flask): + def test_get_builder(self, app: Flask) -> None: api = TriggerSubscriptionBuilderGetApi() method = unwrap(api.get) @@ -160,50 +148,47 @@ class TestTriggerSubscriptionBuilderApis: ): assert method(api, "github", "b1") == {"id": "b1"} - def test_verify_builder(self, app: Flask): + def test_verify_builder(self, app: Flask) -> None: api = TriggerSubscriptionBuilderVerifyApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"credentials": {"a": 1}}), - patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch( "controllers.console.workspace.trigger_providers.TriggerSubscriptionBuilderService.update_and_verify_builder", return_value={"ok": True}, ), ): - assert method(api, "github", "b1") == {"ok": True} + assert method(api, "t1", mock_user(), "github", "b1") == {"ok": True} - def test_verify_builder_error(self, app: Flask): + def test_verify_builder_error(self, app: Flask) -> None: api = TriggerSubscriptionBuilderVerifyApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"credentials": {}}), - patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch( "controllers.console.workspace.trigger_providers.TriggerSubscriptionBuilderService.update_and_verify_builder", side_effect=Exception("err"), ), ): with pytest.raises(ValueError): - method(api, "github", "b1") + method(api, "t1", mock_user(), "github", "b1") - def test_update_builder(self, app: Flask): + def test_update_builder(self, app: Flask) -> None: api = TriggerSubscriptionBuilderUpdateApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"name": "n"}), - patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch( "controllers.console.workspace.trigger_providers.TriggerSubscriptionBuilderService.update_trigger_subscription_builder", return_value={"id": "b1"}, ), ): - assert method(api, "github", "b1") == {"id": "b1"} + assert method(api, "t1", "github", "b1") == {"id": "b1"} - def test_logs(self, app: Flask): + def test_logs(self, app: Flask) -> None: api = TriggerSubscriptionBuilderLogsApi() method = unwrap(api.get) @@ -212,7 +197,6 @@ class TestTriggerSubscriptionBuilderApis: with ( app.test_request_context("/"), - patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch( "controllers.console.workspace.trigger_providers.TriggerSubscriptionBuilderService.list_logs", return_value=[log], @@ -220,27 +204,26 @@ class TestTriggerSubscriptionBuilderApis: ): assert "logs" in method(api, "github", "b1") - def test_build(self, app: Flask): + def test_build(self, app: Flask) -> None: api = TriggerSubscriptionBuilderBuildApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"name": "x"}), - patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch( "controllers.console.workspace.trigger_providers.TriggerSubscriptionBuilderService.update_and_build_builder", return_value=None, ), ): - assert method(api, "github", "b1") == 200 + assert method(api, "t1", mock_user(), "github", "b1") == 200 class TestTriggerSubscriptionCrud: @pytest.fixture - def app(self, flask_app_with_containers: Flask): + def app(self, flask_app_with_containers: Flask) -> Flask: return flask_app_with_containers - def test_update_rename_only(self, app: Flask): + def test_update_rename_only(self, app: Flask) -> None: api = TriggerSubscriptionUpdateApi() method = unwrap(api.post) @@ -250,43 +233,40 @@ class TestTriggerSubscriptionCrud: with ( app.test_request_context("/", json={"name": "x"}), - patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch( "controllers.console.workspace.trigger_providers.TriggerProviderService.get_subscription_by_id", return_value=sub, ), patch("controllers.console.workspace.trigger_providers.TriggerProviderService.update_trigger_subscription"), ): - assert method(api, "s1") == 200 + assert method(api, "t1", "s1") == 200 - def test_update_not_found(self, app: Flask): + def test_update_not_found(self, app: Flask) -> None: api = TriggerSubscriptionUpdateApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"name": "x"}), - patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch( "controllers.console.workspace.trigger_providers.TriggerProviderService.get_subscription_by_id", return_value=None, ), ): with pytest.raises(NotFoundError): - method(api, "x") + method(api, "t1", "x") - def test_update_rebuild(self, app: Flask): + def test_update_rebuild(self, app: Flask) -> None: api = TriggerSubscriptionUpdateApi() method = unwrap(api.post) sub = MagicMock() sub.provider_id = "github" sub.credential_type = CredentialType.OAUTH2 - sub.credentials = {} - sub.parameters = {} + sub.credentials = {"token": "old"} + sub.parameters = {"repo": "demo"} with ( app.test_request_context("/", json={"credentials": {}}), - patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch( "controllers.console.workspace.trigger_providers.TriggerProviderService.get_subscription_by_id", return_value=sub, @@ -295,9 +275,9 @@ class TestTriggerSubscriptionCrud: "controllers.console.workspace.trigger_providers.TriggerProviderService.rebuild_trigger_subscription" ), ): - assert method(api, "s1") == 200 + assert method(api, "t1", "s1") == 200 - def test_delete_subscription(self, app: Flask): + def test_delete_subscription(self, app: Flask) -> None: api = TriggerSubscriptionDeleteApi() method = unwrap(api.post) @@ -305,7 +285,6 @@ class TestTriggerSubscriptionCrud: with ( app.test_request_context("/"), - patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch("controllers.console.workspace.trigger_providers.db") as mock_db, patch("controllers.console.workspace.trigger_providers.sessionmaker") as mock_session_cls, patch("controllers.console.workspace.trigger_providers.TriggerProviderService.delete_trigger_provider"), @@ -316,17 +295,16 @@ class TestTriggerSubscriptionCrud: mock_db.engine = MagicMock() mock_session_cls.return_value.begin.return_value.__enter__.return_value = mock_session - result = method(api, "sub1") + result = method(api, "t1", "sub1") assert result["result"] == "success" - def test_delete_subscription_value_error(self, app: Flask): + def test_delete_subscription_value_error(self, app: Flask) -> None: api = TriggerSubscriptionDeleteApi() method = unwrap(api.post) with ( app.test_request_context("/"), - patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch("controllers.console.workspace.trigger_providers.db") as mock_db, patch("controllers.console.workspace.trigger_providers.sessionmaker") as session_cls, patch( @@ -338,21 +316,20 @@ class TestTriggerSubscriptionCrud: session_cls.return_value.begin.return_value.__enter__.return_value = MagicMock() with pytest.raises(BadRequest): - method(api, "sub1") + method(api, "t1", "sub1") class TestTriggerOAuthApis: @pytest.fixture - def app(self, flask_app_with_containers: Flask): + def app(self, flask_app_with_containers: Flask) -> Flask: return flask_app_with_containers - def test_oauth_authorize_success(self, app: Flask): + def test_oauth_authorize_success(self, app: Flask) -> None: api = TriggerOAuthAuthorizeApi() method = unwrap(api.get) with ( app.test_request_context("/"), - patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch( "controllers.console.workspace.trigger_providers.TriggerProviderService.get_oauth_client", return_value={"a": 1}, @@ -370,25 +347,24 @@ class TestTriggerOAuthApis: return_value=MagicMock(authorization_url="url"), ), ): - resp = method(api, "github") + resp = method(api, "t1", mock_user(), "github") assert resp.status_code == 200 - def test_oauth_authorize_no_client(self, app: Flask): + def test_oauth_authorize_no_client(self, app: Flask) -> None: api = TriggerOAuthAuthorizeApi() method = unwrap(api.get) with ( app.test_request_context("/"), - patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch( "controllers.console.workspace.trigger_providers.TriggerProviderService.get_oauth_client", return_value=None, ), ): with pytest.raises(NotFoundError): - method(api, "github") + method(api, "t1", mock_user(), "github") - def test_oauth_callback_forbidden(self, app: Flask): + def test_oauth_callback_forbidden(self, app: Flask) -> None: api = TriggerOAuthCallbackApi() method = unwrap(api.get) @@ -396,7 +372,7 @@ class TestTriggerOAuthApis: with pytest.raises(Forbidden): method(api, "github") - def test_oauth_callback_success(self, app: Flask): + def test_oauth_callback_success(self, app: Flask) -> None: api = TriggerOAuthCallbackApi() method = unwrap(api.get) @@ -426,7 +402,7 @@ class TestTriggerOAuthApis: resp = method(api, "github") assert resp.status_code == 302 - def test_oauth_callback_no_oauth_client(self, app: Flask): + def test_oauth_callback_no_oauth_client(self, app: Flask) -> None: api = TriggerOAuthCallbackApi() method = unwrap(api.get) @@ -450,7 +426,7 @@ class TestTriggerOAuthApis: with pytest.raises(Forbidden): method(api, "github") - def test_oauth_callback_empty_credentials(self, app: Flask): + def test_oauth_callback_empty_credentials(self, app: Flask) -> None: api = TriggerOAuthCallbackApi() method = unwrap(api.get) @@ -481,16 +457,15 @@ class TestTriggerOAuthApis: class TestTriggerOAuthClientManageApi: @pytest.fixture - def app(self, flask_app_with_containers: Flask): + def app(self, flask_app_with_containers: Flask) -> Flask: return flask_app_with_containers - def test_get_client(self, app: Flask): + def test_get_client(self, app: Flask) -> None: api = TriggerOAuthClientManageApi() method = unwrap(api.get) with ( app.test_request_context("/"), - patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch( "controllers.console.workspace.trigger_providers.TriggerProviderService.get_custom_oauth_client_params", return_value={}, @@ -508,84 +483,79 @@ class TestTriggerOAuthClientManageApi: return_value=MagicMock(get_oauth_client_schema=lambda: {}), ), ): - result = method(api, "github") + result = method(api, "t1", "github") assert "configured" in result - def test_post_client(self, app: Flask): + def test_post_client(self, app: Flask) -> None: api = TriggerOAuthClientManageApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"enabled": True}), - patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch( "controllers.console.workspace.trigger_providers.TriggerProviderService.save_custom_oauth_client_params", return_value={"ok": True}, ), ): - assert method(api, "github") == {"ok": True} + assert method(api, "t1", "github") == {"ok": True} - def test_delete_client(self, app: Flask): + def test_delete_client(self, app: Flask) -> None: api = TriggerOAuthClientManageApi() method = unwrap(api.delete) with ( app.test_request_context("/"), - patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch( "controllers.console.workspace.trigger_providers.TriggerProviderService.delete_custom_oauth_client_params", return_value={"ok": True}, ), ): - assert method(api, "github") == {"ok": True} + assert method(api, "t1", "github") == {"ok": True} - def test_oauth_client_post_value_error(self, app: Flask): + def test_oauth_client_post_value_error(self, app: Flask) -> None: api = TriggerOAuthClientManageApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"enabled": True}), - patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch( "controllers.console.workspace.trigger_providers.TriggerProviderService.save_custom_oauth_client_params", side_effect=ValueError("bad"), ), ): with pytest.raises(BadRequest): - method(api, "github") + method(api, "t1", "github") class TestTriggerSubscriptionVerifyApi: @pytest.fixture - def app(self, flask_app_with_containers: Flask): + def app(self, flask_app_with_containers: Flask) -> Flask: return flask_app_with_containers - def test_verify_success(self, app: Flask): + def test_verify_success(self, app: Flask) -> None: api = TriggerSubscriptionVerifyApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"credentials": {}}), - patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch( "controllers.console.workspace.trigger_providers.TriggerProviderService.verify_subscription_credentials", return_value={"ok": True}, ), ): - assert method(api, "github", "s1") == {"ok": True} + assert method(api, "t1", mock_user(), "github", "s1") == {"ok": True} @pytest.mark.parametrize("raised_exception", [ValueError("bad"), Exception("boom")]) - def test_verify_errors(self, app: Flask, raised_exception): + def test_verify_errors(self, app: Flask, raised_exception: Exception) -> None: api = TriggerSubscriptionVerifyApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"credentials": {}}), - patch("controllers.console.workspace.trigger_providers.current_user", mock_user()), patch( "controllers.console.workspace.trigger_providers.TriggerProviderService.verify_subscription_credentials", side_effect=raised_exception, ), ): with pytest.raises(BadRequest): - method(api, "github", "s1") + method(api, "t1", mock_user(), "github", "s1") diff --git a/api/tests/test_containers_integration_tests/services/rag_pipeline/test_rag_pipeline_service_db.py b/api/tests/test_containers_integration_tests/services/rag_pipeline/test_rag_pipeline_service_db.py index 8fc1809a467..8f126e1cff0 100644 --- a/api/tests/test_containers_integration_tests/services/rag_pipeline/test_rag_pipeline_service_db.py +++ b/api/tests/test_containers_integration_tests/services/rag_pipeline/test_rag_pipeline_service_db.py @@ -11,19 +11,29 @@ Covers: """ from collections.abc import Generator -from types import SimpleNamespace from unittest.mock import patch from uuid import uuid4 import pytest +from flask import Flask from sqlalchemy.orm import Session, sessionmaker +from models import Account, Tenant from models.dataset import Dataset, Pipeline, PipelineCustomizedTemplate from models.enums import DataSourceType from services.entities.knowledge_entities.rag_pipeline_entities import IconInfo, PipelineTemplateInfoEntity from services.rag_pipeline.rag_pipeline import RagPipelineService +def _make_account(account_id: str, tenant_id: str) -> Account: + account = Account(name="Test User", email=f"{account_id}@example.com") + account.id = account_id + tenant = Tenant(name="Test Tenant") + tenant.id = tenant_id + account._current_tenant = tenant + return account + + class TestRagPipelineServiceGetPipeline: """Integration tests for RagPipelineService.get_pipeline.""" @@ -32,7 +42,7 @@ class TestRagPipelineServiceGetPipeline: yield db_session_with_containers.rollback() - def _make_service(self, flask_app_with_containers) -> RagPipelineService: + def _make_service(self, flask_app_with_containers: Flask) -> RagPipelineService: with ( patch( "services.rag_pipeline.rag_pipeline.DifyAPIRepositoryFactory.create_api_workflow_node_execution_repository", @@ -72,7 +82,7 @@ class TestRagPipelineServiceGetPipeline: return dataset def test_get_pipeline_raises_when_dataset_not_found( - self, db_session_with_containers: Session, flask_app_with_containers + self, db_session_with_containers: Session, flask_app_with_containers: Flask ) -> None: """get_pipeline raises ValueError when dataset does not exist.""" service = self._make_service(flask_app_with_containers) @@ -81,7 +91,7 @@ class TestRagPipelineServiceGetPipeline: service.get_pipeline(tenant_id=str(uuid4()), dataset_id=str(uuid4())) def test_get_pipeline_raises_when_pipeline_not_found( - self, db_session_with_containers: Session, flask_app_with_containers + self, db_session_with_containers: Session, flask_app_with_containers: Flask ) -> None: """get_pipeline raises ValueError when dataset exists but has no linked pipeline.""" tenant_id = str(uuid4()) @@ -95,7 +105,7 @@ class TestRagPipelineServiceGetPipeline: service.get_pipeline(tenant_id=tenant_id, dataset_id=dataset.id) def test_get_pipeline_returns_pipeline_when_found( - self, db_session_with_containers: Session, flask_app_with_containers + self, db_session_with_containers: Session, flask_app_with_containers: Flask ) -> None: """get_pipeline returns the Pipeline when both Dataset and Pipeline exist.""" tenant_id = str(uuid4()) @@ -139,43 +149,44 @@ class TestUpdateCustomizedPipelineTemplate: db_session.flush() return template - def test_update_template_succeeds(self, db_session_with_containers: Session, flask_app_with_containers) -> None: + def test_update_template_succeeds( + self, db_session_with_containers: Session, flask_app_with_containers: Flask + ) -> None: """update_customized_pipeline_template updates name and description.""" tenant_id = str(uuid4()) created_by = str(uuid4()) template = self._create_template(db_session_with_containers, tenant_id, created_by) db_session_with_containers.flush() - fake_user = SimpleNamespace(id=created_by, current_tenant_id=tenant_id) + account = _make_account(created_by, tenant_id) - with patch("services.rag_pipeline.rag_pipeline.current_user", fake_user): - info = PipelineTemplateInfoEntity( - name="Updated Name", - description="Updated description", - icon_info=IconInfo(icon="🔥"), - ) - result = RagPipelineService.update_customized_pipeline_template(template.id, info) + info = PipelineTemplateInfoEntity( + name="Updated Name", + description="Updated description", + icon_info=IconInfo(icon="🔥"), + ) + result = RagPipelineService.update_customized_pipeline_template(template.id, info, account, tenant_id) assert result.name == "Updated Name" assert result.description == "Updated description" def test_update_template_raises_when_not_found( - self, db_session_with_containers: Session, flask_app_with_containers + self, db_session_with_containers: Session, flask_app_with_containers: Flask ) -> None: """update_customized_pipeline_template raises ValueError when template doesn't exist.""" - fake_user = SimpleNamespace(id=str(uuid4()), current_tenant_id=str(uuid4())) + tenant_id = str(uuid4()) + account = _make_account(str(uuid4()), tenant_id) - with patch("services.rag_pipeline.rag_pipeline.current_user", fake_user): - info = PipelineTemplateInfoEntity( - name="New Name", - description="desc", - icon_info=IconInfo(icon="📄"), - ) - with pytest.raises(ValueError, match="Customized pipeline template not found"): - RagPipelineService.update_customized_pipeline_template(str(uuid4()), info) + info = PipelineTemplateInfoEntity( + name="New Name", + description="desc", + icon_info=IconInfo(icon="📄"), + ) + with pytest.raises(ValueError, match="Customized pipeline template not found"): + RagPipelineService.update_customized_pipeline_template(str(uuid4()), info, account, tenant_id) def test_update_template_raises_on_duplicate_name( - self, db_session_with_containers: Session, flask_app_with_containers + self, db_session_with_containers: Session, flask_app_with_containers: Flask ) -> None: """update_customized_pipeline_template raises ValueError when new name already exists.""" tenant_id = str(uuid4()) @@ -184,16 +195,15 @@ class TestUpdateCustomizedPipelineTemplate: self._create_template(db_session_with_containers, tenant_id, created_by, name="Duplicate") db_session_with_containers.flush() - fake_user = SimpleNamespace(id=created_by, current_tenant_id=tenant_id) + account = _make_account(created_by, tenant_id) - with patch("services.rag_pipeline.rag_pipeline.current_user", fake_user): - info = PipelineTemplateInfoEntity( - name="Duplicate", - description="desc", - icon_info=IconInfo(icon="📄"), - ) - with pytest.raises(ValueError, match="Template name is already exists"): - RagPipelineService.update_customized_pipeline_template(template1.id, info) + info = PipelineTemplateInfoEntity( + name="Duplicate", + description="desc", + icon_info=IconInfo(icon="📄"), + ) + with pytest.raises(ValueError, match="Template name is already exists"): + RagPipelineService.update_customized_pipeline_template(template1.id, info, account, tenant_id) class TestDeleteCustomizedPipelineTemplate: @@ -221,7 +231,9 @@ class TestDeleteCustomizedPipelineTemplate: db_session.flush() return template - def test_delete_template_succeeds(self, db_session_with_containers: Session, flask_app_with_containers) -> None: + def test_delete_template_succeeds( + self, db_session_with_containers: Session, flask_app_with_containers: Flask + ) -> None: """delete_customized_pipeline_template removes the template from the DB.""" tenant_id = str(uuid4()) created_by = str(uuid4()) @@ -229,27 +241,23 @@ class TestDeleteCustomizedPipelineTemplate: template_id = template.id db_session_with_containers.flush() - fake_user = SimpleNamespace(id=created_by, current_tenant_id=tenant_id) + RagPipelineService.delete_customized_pipeline_template(template_id, tenant_id) - with patch("services.rag_pipeline.rag_pipeline.current_user", fake_user): - RagPipelineService.delete_customized_pipeline_template(template_id) + # Verify the record is deleted within the same context + from sqlalchemy import select - # Verify the record is deleted within the same context - from sqlalchemy import select + from extensions.ext_database import db as ext_db - from extensions.ext_database import db as ext_db - - remaining = ext_db.session.scalar( - select(PipelineCustomizedTemplate).where(PipelineCustomizedTemplate.id == template_id) - ) - assert remaining is None + remaining = ext_db.session.scalar( + select(PipelineCustomizedTemplate).where(PipelineCustomizedTemplate.id == template_id) + ) + assert remaining is None def test_delete_template_raises_when_not_found( - self, db_session_with_containers: Session, flask_app_with_containers + self, db_session_with_containers: Session, flask_app_with_containers: Flask ) -> None: """delete_customized_pipeline_template raises ValueError when template doesn't exist.""" - fake_user = SimpleNamespace(id=str(uuid4()), current_tenant_id=str(uuid4())) + tenant_id = str(uuid4()) - with patch("services.rag_pipeline.rag_pipeline.current_user", fake_user): - with pytest.raises(ValueError, match="Customized pipeline template not found"): - RagPipelineService.delete_customized_pipeline_template(str(uuid4())) + with pytest.raises(ValueError, match="Customized pipeline template not found"): + RagPipelineService.delete_customized_pipeline_template(str(uuid4()), tenant_id) diff --git a/api/tests/test_containers_integration_tests/services/test_metadata_partial_update.py b/api/tests/test_containers_integration_tests/services/test_metadata_partial_update.py index f3ab9eb3da8..5844441e6a5 100644 --- a/api/tests/test_containers_integration_tests/services/test_metadata_partial_update.py +++ b/api/tests/test_containers_integration_tests/services/test_metadata_partial_update.py @@ -1,6 +1,6 @@ from __future__ import annotations -from unittest.mock import Mock, patch +from unittest.mock import patch from uuid import uuid4 import pytest @@ -8,6 +8,7 @@ from flask import Flask from sqlalchemy import select from sqlalchemy.orm import Session +from models import Account, Tenant from models.dataset import Dataset, DatasetMetadataBinding, Document from models.enums import DataSourceType, DocumentCreatedFrom from services.entities.knowledge_entities.knowledge_entities import ( @@ -33,7 +34,7 @@ def _create_dataset(db_session: Session, *, tenant_id: str, built_in_field_enabl def _create_document( - db_session: Session, *, dataset_id: str, tenant_id: str, doc_metadata: dict | None = None + db_session: Session, *, dataset_id: str, tenant_id: str, doc_metadata: dict[str, str] | None = None ) -> Document: document = Document( tenant_id=tenant_id, @@ -63,18 +64,21 @@ class TestMetadataPartialUpdate: return str(uuid4()) @pytest.fixture - def mock_current_account(self, user_id, tenant_id): - account = Mock(id=user_id, current_tenant_id=tenant_id) - with patch("services.metadata_service.current_account_with_tenant", return_value=(account, tenant_id)): - yield account + def current_account(self, user_id: str, tenant_id: str) -> Account: + account = Account(name="Test User", email=f"{user_id}@example.com") + account.id = user_id + tenant = Tenant(name="Test Tenant") + tenant.id = tenant_id + account._current_tenant = tenant + return account def test_partial_update_merges_metadata( self, flask_app_with_containers: Flask, db_session_with_containers: Session, tenant_id: str, - mock_current_account, - ): + current_account: Account, + ) -> None: dataset = _create_dataset(db_session_with_containers, tenant_id=tenant_id) document = _create_document( db_session_with_containers, @@ -91,7 +95,7 @@ class TestMetadataPartialUpdate: ) metadata_args = MetadataOperationData(operation_data=[operation]) - MetadataService.update_documents_metadata(dataset, metadata_args) + MetadataService.update_documents_metadata(dataset, metadata_args, current_account) db_session_with_containers.expire_all() updated_doc = db_session_with_containers.get(Document, document.id) @@ -104,8 +108,8 @@ class TestMetadataPartialUpdate: flask_app_with_containers: Flask, db_session_with_containers: Session, tenant_id: str, - mock_current_account, - ): + current_account: Account, + ) -> None: dataset = _create_dataset(db_session_with_containers, tenant_id=tenant_id) document = _create_document( db_session_with_containers, @@ -122,7 +126,7 @@ class TestMetadataPartialUpdate: ) metadata_args = MetadataOperationData(operation_data=[operation]) - MetadataService.update_documents_metadata(dataset, metadata_args) + MetadataService.update_documents_metadata(dataset, metadata_args, current_account) db_session_with_containers.expire_all() updated_doc = db_session_with_containers.get(Document, document.id) @@ -134,10 +138,10 @@ class TestMetadataPartialUpdate: self, flask_app_with_containers: Flask, db_session_with_containers: Session, - tenant_id, - user_id, - mock_current_account, - ): + tenant_id: str, + user_id: str, + current_account: Account, + ) -> None: dataset = _create_dataset(db_session_with_containers, tenant_id=tenant_id) document = _create_document( db_session_with_containers, @@ -164,7 +168,7 @@ class TestMetadataPartialUpdate: ) metadata_args = MetadataOperationData(operation_data=[operation]) - MetadataService.update_documents_metadata(dataset, metadata_args) + MetadataService.update_documents_metadata(dataset, metadata_args, current_account) db_session_with_containers.expire_all() bindings = db_session_with_containers.scalars( @@ -180,8 +184,8 @@ class TestMetadataPartialUpdate: flask_app_with_containers: Flask, db_session_with_containers: Session, tenant_id: str, - mock_current_account, - ): + current_account: Account, + ) -> None: dataset = _create_dataset(db_session_with_containers, tenant_id=tenant_id) document = _create_document( db_session_with_containers, @@ -200,4 +204,4 @@ class TestMetadataPartialUpdate: with patch("services.metadata_service.db.session.commit", side_effect=RuntimeError("database connection lost")): with pytest.raises(RuntimeError, match="database connection lost"): - MetadataService.update_documents_metadata(dataset, metadata_args) + MetadataService.update_documents_metadata(dataset, metadata_args, current_account) diff --git a/api/tests/test_containers_integration_tests/services/test_metadata_service.py b/api/tests/test_containers_integration_tests/services/test_metadata_service.py index 8b1349be9a8..0c9e3830430 100644 --- a/api/tests/test_containers_integration_tests/services/test_metadata_service.py +++ b/api/tests/test_containers_integration_tests/services/test_metadata_service.py @@ -1,4 +1,6 @@ -from unittest.mock import create_autospec, patch +from collections.abc import Generator +from typing import TypedDict +from unittest.mock import Mock, patch import pytest from faker import Faker @@ -6,21 +8,25 @@ from sqlalchemy.orm import Session from core.rag.index_processor.constant.built_in_field import BuiltInField from core.rag.index_processor.constant.index_type import IndexStructureType -from models import Account, Tenant, TenantAccountJoin, TenantAccountRole +from models import Account, AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole, TenantStatus from models.dataset import Dataset, DatasetMetadata, DatasetMetadataBinding, Document -from models.enums import DatasetMetadataType, DataSourceType, DocumentCreatedFrom +from models.enums import DataSourceType, DocumentCreatedFrom from services.entities.knowledge_entities.knowledge_entities import MetadataArgs from services.metadata_service import MetadataService +class MetadataServiceDeps(TypedDict): + redis_client: Mock + document_service: Mock + + class TestMetadataService: """Integration tests for MetadataService using testcontainers.""" @pytest.fixture - def mock_external_service_dependencies(self): + def mock_external_service_dependencies(self) -> Generator[MetadataServiceDeps, None, None]: """Mock setup for external service dependencies.""" with ( - patch("libs.login.current_user", create_autospec(Account, instance=True)) as mock_current_user, patch("services.metadata_service.redis_client") as mock_redis_client, patch("services.dataset_service.DocumentService") as mock_document_service, ): @@ -30,12 +36,15 @@ class TestMetadataService: mock_redis_client.delete.return_value = 1 yield { - "current_user": mock_current_user, "redis_client": mock_redis_client, "document_service": mock_document_service, } - def _create_test_account_and_tenant(self, db_session_with_containers: Session, mock_external_service_dependencies): + def _create_test_account_and_tenant( + self, + db_session_with_containers: Session, + mock_external_service_dependencies: MetadataServiceDeps, + ) -> tuple[Account, Tenant]: """ Helper method to create a test account and tenant for testing. @@ -53,7 +62,7 @@ class TestMetadataService: email=fake.email(), name=fake.name(), interface_language="en-US", - status="active", + status=AccountStatus.ACTIVE, ) db_session_with_containers.add(account) @@ -62,7 +71,7 @@ class TestMetadataService: # Create tenant for the account tenant = Tenant( name=fake.company(), - status="normal", + status=TenantStatus.NORMAL, ) db_session_with_containers.add(tenant) db_session_with_containers.commit() @@ -83,8 +92,12 @@ class TestMetadataService: return account, tenant def _create_test_dataset( - self, db_session_with_containers: Session, mock_external_service_dependencies, account, tenant - ): + self, + db_session_with_containers: Session, + mock_external_service_dependencies: MetadataServiceDeps, + account: Account, + tenant: Tenant, + ) -> Dataset: """ Helper method to create a test dataset for testing. @@ -114,8 +127,12 @@ class TestMetadataService: return dataset def _create_test_document( - self, db_session_with_containers: Session, mock_external_service_dependencies, dataset, account - ): + self, + db_session_with_containers: Session, + mock_external_service_dependencies: MetadataServiceDeps, + dataset: Dataset, + account: Account, + ) -> Document: """ Helper method to create a test document for testing. @@ -149,7 +166,9 @@ class TestMetadataService: return document - def test_create_metadata_success(self, db_session_with_containers: Session, mock_external_service_dependencies): + def test_create_metadata_success( + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test successful metadata creation with valid parameters. """ @@ -161,14 +180,10 @@ class TestMetadataService: db_session_with_containers, mock_external_service_dependencies, account, tenant ) - # Setup mocks - mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id - mock_external_service_dependencies["current_user"].id = account.id - - metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="test_metadata") + metadata_args = MetadataArgs(type="string", name="test_metadata") # Act: Execute the method under test - result = MetadataService.create_metadata(dataset.id, metadata_args) + result = MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id) # Assert: Verify the expected outcomes assert result is not None @@ -185,8 +200,8 @@ class TestMetadataService: assert result.created_at is not None def test_create_metadata_name_too_long( - self, db_session_with_containers: Session, mock_external_service_dependencies - ): + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test metadata creation fails when name exceeds 255 characters. """ @@ -198,20 +213,16 @@ class TestMetadataService: db_session_with_containers, mock_external_service_dependencies, account, tenant ) - # Setup mocks - mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id - mock_external_service_dependencies["current_user"].id = account.id - long_name = "a" * 256 # 256 characters, exceeding 255 limit - metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name=long_name) + metadata_args = MetadataArgs(type="string", name=long_name) # Act & Assert: Verify proper error handling with pytest.raises(ValueError, match="Metadata name cannot exceed 255 characters."): - MetadataService.create_metadata(dataset.id, metadata_args) + MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id) def test_create_metadata_name_already_exists( - self, db_session_with_containers: Session, mock_external_service_dependencies - ): + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test metadata creation fails when name already exists in the same dataset. """ @@ -223,24 +234,20 @@ class TestMetadataService: db_session_with_containers, mock_external_service_dependencies, account, tenant ) - # Setup mocks - mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id - mock_external_service_dependencies["current_user"].id = account.id - # Create first metadata - first_metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="duplicate_name") - MetadataService.create_metadata(dataset.id, first_metadata_args) + first_metadata_args = MetadataArgs(type="string", name="duplicate_name") + MetadataService.create_metadata(dataset.id, first_metadata_args, account, tenant.id) # Try to create second metadata with same name - second_metadata_args = MetadataArgs(type=DatasetMetadataType.NUMBER, name="duplicate_name") + second_metadata_args = MetadataArgs(type="number", name="duplicate_name") # Act & Assert: Verify proper error handling with pytest.raises(ValueError, match="Metadata name already exists."): - MetadataService.create_metadata(dataset.id, second_metadata_args) + MetadataService.create_metadata(dataset.id, second_metadata_args, account, tenant.id) def test_create_metadata_name_conflicts_with_built_in_field( - self, db_session_with_containers: Session, mock_external_service_dependencies - ): + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test metadata creation fails when name conflicts with built-in field names. """ @@ -252,21 +259,17 @@ class TestMetadataService: db_session_with_containers, mock_external_service_dependencies, account, tenant ) - # Setup mocks - mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id - mock_external_service_dependencies["current_user"].id = account.id - # Try to create metadata with built-in field name built_in_field_name = BuiltInField.document_name - metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name=built_in_field_name) + metadata_args = MetadataArgs(type="string", name=built_in_field_name) # Act & Assert: Verify proper error handling with pytest.raises(ValueError, match="Metadata name already exists in Built-in fields."): - MetadataService.create_metadata(dataset.id, metadata_args) + MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id) def test_update_metadata_name_success( - self, db_session_with_containers: Session, mock_external_service_dependencies - ): + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test successful metadata name update with valid parameters. """ @@ -278,17 +281,13 @@ class TestMetadataService: db_session_with_containers, mock_external_service_dependencies, account, tenant ) - # Setup mocks - mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id - mock_external_service_dependencies["current_user"].id = account.id - # Create metadata first - metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="old_name") - metadata = MetadataService.create_metadata(dataset.id, metadata_args) + metadata_args = MetadataArgs(type="string", name="old_name") + metadata = MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id) # Act: Execute the method under test new_name = "new_name" - result = MetadataService.update_metadata_name(dataset.id, metadata.id, new_name) + result = MetadataService.update_metadata_name(dataset.id, metadata.id, new_name, account, tenant.id) # Assert: Verify the expected outcomes assert result is not None @@ -302,8 +301,8 @@ class TestMetadataService: assert result.name == new_name def test_update_metadata_name_too_long( - self, db_session_with_containers: Session, mock_external_service_dependencies - ): + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test metadata name update fails when new name exceeds 255 characters. """ @@ -315,24 +314,20 @@ class TestMetadataService: db_session_with_containers, mock_external_service_dependencies, account, tenant ) - # Setup mocks - mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id - mock_external_service_dependencies["current_user"].id = account.id - # Create metadata first - metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="old_name") - metadata = MetadataService.create_metadata(dataset.id, metadata_args) + metadata_args = MetadataArgs(type="string", name="old_name") + metadata = MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id) # Try to update with too long name long_name = "a" * 256 # 256 characters, exceeding 255 limit # Act & Assert: Verify proper error handling with pytest.raises(ValueError, match="Metadata name cannot exceed 255 characters."): - MetadataService.update_metadata_name(dataset.id, metadata.id, long_name) + MetadataService.update_metadata_name(dataset.id, metadata.id, long_name, account, tenant.id) def test_update_metadata_name_already_exists( - self, db_session_with_containers: Session, mock_external_service_dependencies - ): + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test metadata name update fails when new name already exists in the same dataset. """ @@ -344,24 +339,20 @@ class TestMetadataService: db_session_with_containers, mock_external_service_dependencies, account, tenant ) - # Setup mocks - mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id - mock_external_service_dependencies["current_user"].id = account.id - # Create two metadata entries - first_metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="first_metadata") - first_metadata = MetadataService.create_metadata(dataset.id, first_metadata_args) + first_metadata_args = MetadataArgs(type="string", name="first_metadata") + first_metadata = MetadataService.create_metadata(dataset.id, first_metadata_args, account, tenant.id) - second_metadata_args = MetadataArgs(type=DatasetMetadataType.NUMBER, name="second_metadata") - second_metadata = MetadataService.create_metadata(dataset.id, second_metadata_args) + second_metadata_args = MetadataArgs(type="number", name="second_metadata") + second_metadata = MetadataService.create_metadata(dataset.id, second_metadata_args, account, tenant.id) # Try to update first metadata with second metadata's name with pytest.raises(ValueError, match="Metadata name already exists."): - MetadataService.update_metadata_name(dataset.id, first_metadata.id, "second_metadata") + MetadataService.update_metadata_name(dataset.id, first_metadata.id, "second_metadata", account, tenant.id) def test_update_metadata_name_conflicts_with_built_in_field( - self, db_session_with_containers: Session, mock_external_service_dependencies - ): + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test metadata name update fails when new name conflicts with built-in field names. """ @@ -373,23 +364,19 @@ class TestMetadataService: db_session_with_containers, mock_external_service_dependencies, account, tenant ) - # Setup mocks - mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id - mock_external_service_dependencies["current_user"].id = account.id - # Create metadata first - metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="old_name") - metadata = MetadataService.create_metadata(dataset.id, metadata_args) + metadata_args = MetadataArgs(type="string", name="old_name") + metadata = MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id) # Try to update with built-in field name built_in_field_name = BuiltInField.document_name with pytest.raises(ValueError, match="Metadata name already exists in Built-in fields."): - MetadataService.update_metadata_name(dataset.id, metadata.id, built_in_field_name) + MetadataService.update_metadata_name(dataset.id, metadata.id, built_in_field_name, account, tenant.id) def test_update_metadata_name_not_found( - self, db_session_with_containers: Session, mock_external_service_dependencies - ): + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test metadata name update fails when metadata ID does not exist. """ @@ -401,10 +388,6 @@ class TestMetadataService: db_session_with_containers, mock_external_service_dependencies, account, tenant ) - # Setup mocks - mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id - mock_external_service_dependencies["current_user"].id = account.id - # Try to update non-existent metadata import uuid @@ -412,12 +395,14 @@ class TestMetadataService: new_name = "new_name" # Act: Execute the method under test - result = MetadataService.update_metadata_name(dataset.id, fake_metadata_id, new_name) + result = MetadataService.update_metadata_name(dataset.id, fake_metadata_id, new_name, account, tenant.id) # Assert: Verify the method returns None when metadata is not found assert result is None - def test_delete_metadata_success(self, db_session_with_containers: Session, mock_external_service_dependencies): + def test_delete_metadata_success( + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test successful metadata deletion with valid parameters. """ @@ -429,13 +414,9 @@ class TestMetadataService: db_session_with_containers, mock_external_service_dependencies, account, tenant ) - # Setup mocks - mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id - mock_external_service_dependencies["current_user"].id = account.id - # Create metadata first - metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="to_be_deleted") - metadata = MetadataService.create_metadata(dataset.id, metadata_args) + metadata_args = MetadataArgs(type="string", name="to_be_deleted") + metadata = MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id) # Act: Execute the method under test result = MetadataService.delete_metadata(dataset.id, metadata.id) @@ -449,7 +430,9 @@ class TestMetadataService: deleted_metadata = db_session_with_containers.query(DatasetMetadata).filter_by(id=metadata.id).first() assert deleted_metadata is None - def test_delete_metadata_not_found(self, db_session_with_containers: Session, mock_external_service_dependencies): + def test_delete_metadata_not_found( + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test metadata deletion fails when metadata ID does not exist. """ @@ -461,10 +444,6 @@ class TestMetadataService: db_session_with_containers, mock_external_service_dependencies, account, tenant ) - # Setup mocks - mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id - mock_external_service_dependencies["current_user"].id = account.id - # Try to delete non-existent metadata import uuid @@ -477,8 +456,8 @@ class TestMetadataService: assert result is None def test_delete_metadata_with_document_bindings( - self, db_session_with_containers: Session, mock_external_service_dependencies - ): + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test metadata deletion successfully removes document metadata bindings. """ @@ -493,13 +472,9 @@ class TestMetadataService: db_session_with_containers, mock_external_service_dependencies, dataset, account ) - # Setup mocks - mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id - mock_external_service_dependencies["current_user"].id = account.id - # Create metadata - metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="test_metadata") - metadata = MetadataService.create_metadata(dataset.id, metadata_args) + metadata_args = MetadataArgs(type="string", name="test_metadata") + metadata = MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id) # Create metadata binding binding = DatasetMetadataBinding( @@ -531,7 +506,9 @@ class TestMetadataService: # Note: The service attempts to update document metadata but may not succeed # due to mock configuration. The main functionality (metadata deletion) is verified. - def test_get_built_in_fields_success(self, db_session_with_containers: Session, mock_external_service_dependencies): + def test_get_built_in_fields_success( + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test successful retrieval of built-in metadata fields. """ @@ -557,8 +534,8 @@ class TestMetadataService: assert "time" in field_types def test_enable_built_in_field_success( - self, db_session_with_containers: Session, mock_external_service_dependencies - ): + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test successful enabling of built-in fields for a dataset. """ @@ -573,10 +550,6 @@ class TestMetadataService: db_session_with_containers, mock_external_service_dependencies, dataset, account ) - # Setup mocks - mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id - mock_external_service_dependencies["current_user"].id = account.id - # Mock DocumentService.get_working_documents_by_dataset_id mock_external_service_dependencies["document_service"].get_working_documents_by_dataset_id.return_value = [ document @@ -591,14 +564,14 @@ class TestMetadataService: # Assert: Verify the expected outcomes db_session_with_containers.refresh(dataset) - assert dataset.built_in_field_enabled is True + assert dataset.built_in_field_enabled # Note: Document metadata update depends on DocumentService mock working correctly # The main functionality (enabling built-in fields) is verified def test_enable_built_in_field_already_enabled( - self, db_session_with_containers: Session, mock_external_service_dependencies - ): + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test enabling built-in fields when they are already enabled. """ @@ -610,10 +583,6 @@ class TestMetadataService: db_session_with_containers, mock_external_service_dependencies, account, tenant ) - # Setup mocks - mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id - mock_external_service_dependencies["current_user"].id = account.id - # Enable built-in fields first dataset.built_in_field_enabled = True @@ -621,7 +590,9 @@ class TestMetadataService: db_session_with_containers.commit() # Mock DocumentService.get_working_documents_by_dataset_id - mock_external_service_dependencies["document_service"].get_working_documents_by_dataset_id.return_value = [] + mock_external_service_dependencies["document_service"].get_working_documents_by_dataset_id.return_value = list[ + Document + ]() # Act: Execute the method under test MetadataService.enable_built_in_field(dataset) @@ -631,8 +602,8 @@ class TestMetadataService: assert dataset.built_in_field_enabled is True def test_enable_built_in_field_with_no_documents( - self, db_session_with_containers: Session, mock_external_service_dependencies - ): + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test enabling built-in fields for a dataset with no documents. """ @@ -644,12 +615,10 @@ class TestMetadataService: db_session_with_containers, mock_external_service_dependencies, account, tenant ) - # Setup mocks - mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id - mock_external_service_dependencies["current_user"].id = account.id - # Mock DocumentService.get_working_documents_by_dataset_id to return empty list - mock_external_service_dependencies["document_service"].get_working_documents_by_dataset_id.return_value = [] + mock_external_service_dependencies["document_service"].get_working_documents_by_dataset_id.return_value = list[ + Document + ]() # Act: Execute the method under test MetadataService.enable_built_in_field(dataset) @@ -657,11 +626,11 @@ class TestMetadataService: # Assert: Verify the expected outcomes db_session_with_containers.refresh(dataset) - assert dataset.built_in_field_enabled is True + assert dataset.built_in_field_enabled def test_disable_built_in_field_success( - self, db_session_with_containers: Session, mock_external_service_dependencies - ): + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test successful disabling of built-in fields for a dataset. """ @@ -676,10 +645,6 @@ class TestMetadataService: db_session_with_containers, mock_external_service_dependencies, dataset, account ) - # Setup mocks - mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id - mock_external_service_dependencies["current_user"].id = account.id - # Enable built-in fields first dataset.built_in_field_enabled = True @@ -713,8 +678,8 @@ class TestMetadataService: # The main functionality (disabling built-in fields) is verified def test_disable_built_in_field_already_disabled( - self, db_session_with_containers: Session, mock_external_service_dependencies - ): + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test disabling built-in fields when they are already disabled. """ @@ -726,15 +691,13 @@ class TestMetadataService: db_session_with_containers, mock_external_service_dependencies, account, tenant ) - # Setup mocks - mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id - mock_external_service_dependencies["current_user"].id = account.id - # Verify dataset starts with built-in fields disabled assert dataset.built_in_field_enabled is False # Mock DocumentService.get_working_documents_by_dataset_id - mock_external_service_dependencies["document_service"].get_working_documents_by_dataset_id.return_value = [] + mock_external_service_dependencies["document_service"].get_working_documents_by_dataset_id.return_value = list[ + Document + ]() # Act: Execute the method under test MetadataService.disable_built_in_field(dataset) @@ -742,11 +705,11 @@ class TestMetadataService: # Assert: Verify the method returns early without changes db_session_with_containers.refresh(dataset) - assert dataset.built_in_field_enabled is False + assert not dataset.built_in_field_enabled def test_disable_built_in_field_with_no_documents( - self, db_session_with_containers: Session, mock_external_service_dependencies - ): + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test disabling built-in fields for a dataset with no documents. """ @@ -758,10 +721,6 @@ class TestMetadataService: db_session_with_containers, mock_external_service_dependencies, account, tenant ) - # Setup mocks - mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id - mock_external_service_dependencies["current_user"].id = account.id - # Enable built-in fields first dataset.built_in_field_enabled = True @@ -769,7 +728,9 @@ class TestMetadataService: db_session_with_containers.commit() # Mock DocumentService.get_working_documents_by_dataset_id to return empty list - mock_external_service_dependencies["document_service"].get_working_documents_by_dataset_id.return_value = [] + mock_external_service_dependencies["document_service"].get_working_documents_by_dataset_id.return_value = list[ + Document + ]() # Act: Execute the method under test MetadataService.disable_built_in_field(dataset) @@ -779,8 +740,8 @@ class TestMetadataService: assert dataset.built_in_field_enabled is False def test_update_documents_metadata_success( - self, db_session_with_containers: Session, mock_external_service_dependencies - ): + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test successful update of documents metadata. """ @@ -795,13 +756,9 @@ class TestMetadataService: db_session_with_containers, mock_external_service_dependencies, dataset, account ) - # Setup mocks - mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id - mock_external_service_dependencies["current_user"].id = account.id - # Create metadata - metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="test_metadata") - metadata = MetadataService.create_metadata(dataset.id, metadata_args) + metadata_args = MetadataArgs(type="string", name="test_metadata") + metadata = MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id) # Mock DocumentService.get_document mock_external_service_dependencies["document_service"].get_document.return_value = document @@ -820,7 +777,7 @@ class TestMetadataService: operation_data = MetadataOperationData(operation_data=[operation]) # Act: Execute the method under test - MetadataService.update_documents_metadata(dataset, operation_data) + MetadataService.update_documents_metadata(dataset, operation_data, account) # Assert: Verify the expected outcomes @@ -841,8 +798,8 @@ class TestMetadataService: assert binding.dataset_id == dataset.id def test_update_documents_metadata_with_built_in_fields_enabled( - self, db_session_with_containers: Session, mock_external_service_dependencies - ): + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test update of documents metadata when built-in fields are enabled. """ @@ -863,13 +820,9 @@ class TestMetadataService: db_session_with_containers.add(dataset) db_session_with_containers.commit() - # Setup mocks - mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id - mock_external_service_dependencies["current_user"].id = account.id - # Create metadata - metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="test_metadata") - metadata = MetadataService.create_metadata(dataset.id, metadata_args) + metadata_args = MetadataArgs(type="string", name="test_metadata") + metadata = MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id) # Mock DocumentService.get_document mock_external_service_dependencies["document_service"].get_document.return_value = document @@ -888,7 +841,7 @@ class TestMetadataService: operation_data = MetadataOperationData(operation_data=[operation]) # Act: Execute the method under test - MetadataService.update_documents_metadata(dataset, operation_data) + MetadataService.update_documents_metadata(dataset, operation_data, account) # Assert: Verify the expected outcomes # Verify document metadata was updated with both custom and built-in fields @@ -901,8 +854,8 @@ class TestMetadataService: # The main functionality (custom metadata update) is verified def test_update_documents_metadata_document_not_found( - self, db_session_with_containers: Session, mock_external_service_dependencies - ): + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test update of documents metadata when document is not found. """ @@ -914,13 +867,9 @@ class TestMetadataService: db_session_with_containers, mock_external_service_dependencies, account, tenant ) - # Setup mocks - mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id - mock_external_service_dependencies["current_user"].id = account.id - # Create metadata - metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="test_metadata") - metadata = MetadataService.create_metadata(dataset.id, metadata_args) + metadata_args = MetadataArgs(type="string", name="test_metadata") + metadata = MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id) # Create metadata operation data from services.entities.knowledge_entities.knowledge_entities import ( @@ -941,11 +890,11 @@ class TestMetadataService: # Act & Assert: The method should raise ValueError("Document not found.") # because the exception is now re-raised after rollback with pytest.raises(ValueError, match="Document not found"): - MetadataService.update_documents_metadata(dataset, operation_data) + MetadataService.update_documents_metadata(dataset, operation_data, account) def test_knowledge_base_metadata_lock_check_dataset_id( - self, db_session_with_containers: Session, mock_external_service_dependencies - ): + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test metadata lock check for dataset operations. """ @@ -967,8 +916,8 @@ class TestMetadataService: assert call_args[0][0] == f"dataset_metadata_lock_{dataset_id}" def test_knowledge_base_metadata_lock_check_document_id( - self, db_session_with_containers: Session, mock_external_service_dependencies - ): + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test metadata lock check for document operations. """ @@ -990,8 +939,8 @@ class TestMetadataService: assert call_args[0][0] == f"document_metadata_lock_{document_id}" def test_knowledge_base_metadata_lock_check_lock_exists( - self, db_session_with_containers: Session, mock_external_service_dependencies - ): + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test metadata lock check when lock already exists. """ @@ -1007,8 +956,8 @@ class TestMetadataService: MetadataService.knowledge_base_metadata_lock_check(dataset_id, None) def test_knowledge_base_metadata_lock_check_document_lock_exists( - self, db_session_with_containers: Session, mock_external_service_dependencies - ): + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test metadata lock check when document lock already exists. """ @@ -1022,8 +971,8 @@ class TestMetadataService: MetadataService.knowledge_base_metadata_lock_check(None, document_id) def test_get_dataset_metadatas_success( - self, db_session_with_containers: Session, mock_external_service_dependencies - ): + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test successful retrieval of dataset metadata information. """ @@ -1035,13 +984,9 @@ class TestMetadataService: db_session_with_containers, mock_external_service_dependencies, account, tenant ) - # Setup mocks - mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id - mock_external_service_dependencies["current_user"].id = account.id - # Create metadata - metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="test_metadata") - metadata = MetadataService.create_metadata(dataset.id, metadata_args) + metadata_args = MetadataArgs(type="string", name="test_metadata") + metadata = MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id) # Create document and metadata binding document = self._create_test_document( @@ -1079,8 +1024,8 @@ class TestMetadataService: assert result["built_in_field_enabled"] is False def test_get_dataset_metadatas_with_built_in_fields_enabled( - self, db_session_with_containers: Session, mock_external_service_dependencies - ): + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test retrieval of dataset metadata when built-in fields are enabled. """ @@ -1098,13 +1043,9 @@ class TestMetadataService: db_session_with_containers.add(dataset) db_session_with_containers.commit() - # Setup mocks - mock_external_service_dependencies["current_user"].current_tenant_id = tenant.id - mock_external_service_dependencies["current_user"].id = account.id - # Create metadata - metadata_args = MetadataArgs(type=DatasetMetadataType.STRING, name="test_metadata") - metadata = MetadataService.create_metadata(dataset.id, metadata_args) + metadata_args = MetadataArgs(type="string", name="test_metadata") + metadata = MetadataService.create_metadata(dataset.id, metadata_args, account, tenant.id) # Act: Execute the method under test result = MetadataService.get_dataset_metadatas(dataset) @@ -1122,8 +1063,8 @@ class TestMetadataService: assert result["built_in_field_enabled"] is True def test_get_dataset_metadatas_no_metadata( - self, db_session_with_containers: Session, mock_external_service_dependencies - ): + self, db_session_with_containers: Session, mock_external_service_dependencies: MetadataServiceDeps + ) -> None: """ Test retrieval of dataset metadata when no metadata exists. """ diff --git a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py index 5a66bc4e92f..bca2d73ad9f 100644 --- a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py +++ b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline.py @@ -15,6 +15,7 @@ from controllers.console.datasets.rag_pipeline.rag_pipeline import ( PipelineTemplateListApi, PublishCustomizedPipelineTemplateApi, ) +from models.account import Account from models.dataset import PipelineCustomizedTemplate from services.entities.knowledge_entities.rag_pipeline_entities import PipelineTemplateInfoEntity @@ -50,24 +51,31 @@ def _payload() -> dict[str, object]: } +def _account() -> Account: + account = Account(name="Test User", email="test@example.com") + account.id = "account-1" + return account + + class TestPipelineTemplateListApi: def test_get_uses_query_defaults_and_serializes_nullable_fields(self, app: Flask) -> None: api = PipelineTemplateListApi() method = unwrap(api.get) - service_calls: list[tuple[str, str]] = [] + tenant_id = "tenant-1" + service_calls: list[tuple[str, str, str]] = [] - def get_pipeline_templates(template_type: str, language: str) -> dict[str, object]: - service_calls.append((template_type, language)) + def get_pipeline_templates(template_type: str, language: str, current_tenant_id: str) -> dict[str, object]: + service_calls.append((template_type, language, current_tenant_id)) return {"pipeline_templates": [_template_item()]} with ( app.test_request_context("/rag/pipeline/templates"), patch.object(module.RagPipelineService, "get_pipeline_templates", side_effect=get_pipeline_templates), ): - response, status = method(api) + response, status = method(api, tenant_id) assert status == 200 - assert service_calls == [("built-in", "en-US")] + assert service_calls == [("built-in", "en-US", tenant_id)] assert response == { "pipeline_templates": [ { @@ -81,21 +89,22 @@ class TestPipelineTemplateListApi: def test_get_passes_explicit_query_to_service(self, app: Flask) -> None: api = PipelineTemplateListApi() method = unwrap(api.get) - service_calls: list[tuple[str, str]] = [] + tenant_id = "tenant-1" + service_calls: list[tuple[str, str, str]] = [] - def get_pipeline_templates(template_type: str, language: str) -> dict[str, object]: - service_calls.append((template_type, language)) + def get_pipeline_templates(template_type: str, language: str, current_tenant_id: str) -> dict[str, object]: + service_calls.append((template_type, language, current_tenant_id)) return {"pipeline_templates": []} with ( app.test_request_context("/rag/pipeline/templates?type=customized&language=ja-JP"), patch.object(module.RagPipelineService, "get_pipeline_templates", side_effect=get_pipeline_templates), ): - response, status = method(api) + response, status = method(api, tenant_id) assert status == 200 assert response == {"pipeline_templates": []} - assert service_calls == [("customized", "ja-JP")] + assert service_calls == [("customized", "ja-JP", tenant_id)] class TestPipelineTemplateDetailApi: @@ -140,22 +149,28 @@ class TestCustomizedPipelineTemplateApi: api = CustomizedPipelineTemplateApi() method = unwrap(api.patch) payload = _payload() - service_calls: list[tuple[str, PipelineTemplateInfoEntity]] = [] + account = _account() + tenant_id = "tenant-1" + service_calls: list[tuple[str, PipelineTemplateInfoEntity, Account, str]] = [] - def update_template(template_id: str, template_info: PipelineTemplateInfoEntity) -> None: - service_calls.append((template_id, template_info)) + def update_template( + template_id: str, template_info: PipelineTemplateInfoEntity, current_user: Account, current_tenant_id: str + ) -> None: + service_calls.append((template_id, template_info, current_user, current_tenant_id)) with ( app.test_request_context("/rag/pipeline/customized/templates/template-1", method="PATCH", json=payload), patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=payload), patch.object(module.RagPipelineService, "update_customized_pipeline_template", side_effect=update_template), ): - response, status = method(api, "template-1") + response, status = method(api, tenant_id, account, "template-1") assert (response, status) == ("", 204) assert len(service_calls) == 1 - template_id, template_info = service_calls[0] + template_id, template_info, current_user, current_tenant_id = service_calls[0] assert template_id == "template-1" + assert current_user is account + assert current_tenant_id == tenant_id assert template_info.name == "Updated template" assert template_info.description == "Updated description" assert template_info.icon_info.model_dump() == { @@ -172,22 +187,28 @@ class TestCustomizedPipelineTemplateApi: "name": "Updated template", "description": "Updated description", } - service_calls: list[tuple[str, PipelineTemplateInfoEntity]] = [] + account = _account() + tenant_id = "tenant-1" + service_calls: list[tuple[str, PipelineTemplateInfoEntity, Account, str]] = [] - def update_template(template_id: str, template_info: PipelineTemplateInfoEntity) -> None: - service_calls.append((template_id, template_info)) + def update_template( + template_id: str, template_info: PipelineTemplateInfoEntity, current_user: Account, current_tenant_id: str + ) -> None: + service_calls.append((template_id, template_info, current_user, current_tenant_id)) with ( app.test_request_context("/rag/pipeline/customized/templates/template-1", method="PATCH", json=payload), patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=payload), patch.object(module.RagPipelineService, "update_customized_pipeline_template", side_effect=update_template), ): - response, status = method(api, "template-1") + response, status = method(api, tenant_id, account, "template-1") assert (response, status) == ("", 204) assert len(service_calls) == 1 - template_id, template_info = service_calls[0] + template_id, template_info, current_user, current_tenant_id = service_calls[0] assert template_id == "template-1" + assert current_user is account + assert current_tenant_id == tenant_id assert template_info.icon_info.model_dump() == { "icon": "", "icon_background": None, @@ -198,19 +219,20 @@ class TestCustomizedPipelineTemplateApi: def test_delete_returns_empty_204(self, app: Flask) -> None: api = CustomizedPipelineTemplateApi() method = unwrap(api.delete) - deleted_template_ids: list[str] = [] + tenant_id = "tenant-1" + deleted_templates: list[tuple[str, str]] = [] - def delete_template(template_id: str) -> None: - deleted_template_ids.append(template_id) + def delete_template(template_id: str, current_tenant_id: str) -> None: + deleted_templates.append((template_id, current_tenant_id)) with ( app.test_request_context("/rag/pipeline/customized/templates/template-1", method="DELETE"), patch.object(module.RagPipelineService, "delete_customized_pipeline_template", side_effect=delete_template), ): - response, status = method(api, "template-1") + response, status = method(api, tenant_id, "template-1") assert (response, status) == ("", 204) - assert deleted_template_ids == ["template-1"] + assert deleted_templates == [("template-1", tenant_id)] def test_post_exports_yaml_from_orm_template(self, app: Flask) -> None: api = CustomizedPipelineTemplateApi() @@ -292,21 +314,25 @@ class TestPublishCustomizedPipelineTemplateApi: api = PublishCustomizedPipelineTemplateApi() method = unwrap(api.post) payload = _payload() - service_calls: list[tuple[str, dict[str, object]]] = [] + account = _account() + tenant_id = "tenant-1" + service_calls: list[tuple[str, dict[str, object], Account, str]] = [] class Service: - def publish_customized_pipeline_template(self, pipeline_id: str, data: dict[str, object]) -> None: - service_calls.append((pipeline_id, data)) + def publish_customized_pipeline_template( + self, pipeline_id: str, data: dict[str, object], current_user: Account, current_tenant_id: str + ) -> None: + service_calls.append((pipeline_id, data, current_user, current_tenant_id)) with ( app.test_request_context("/rag/pipelines/pipeline-1/customized/publish", method="POST", json=payload), patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=payload), patch.object(module, "RagPipelineService", Service), ): - response, status = method(api, "pipeline-1") + response, status = method(api, tenant_id, account, "pipeline-1") assert (response, status) == ("", 204) - assert service_calls == [("pipeline-1", payload)] + assert service_calls == [("pipeline-1", payload, account, tenant_id)] def test_post_allows_missing_icon_info_for_publish_service_fallback(self, app: Flask) -> None: api = PublishCustomizedPipelineTemplateApi() @@ -315,18 +341,22 @@ class TestPublishCustomizedPipelineTemplateApi: "name": "Published template", "description": "Description", } - service_calls: list[tuple[str, dict[str, object]]] = [] + account = _account() + tenant_id = "tenant-1" + service_calls: list[tuple[str, dict[str, object], Account, str]] = [] class Service: - def publish_customized_pipeline_template(self, pipeline_id: str, data: dict[str, object]) -> None: - service_calls.append((pipeline_id, data)) + def publish_customized_pipeline_template( + self, pipeline_id: str, data: dict[str, object], current_user: Account, current_tenant_id: str + ) -> None: + service_calls.append((pipeline_id, data, current_user, current_tenant_id)) with ( app.test_request_context("/rag/pipelines/pipeline-1/customized/publish", method="POST", json=payload), patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=payload), patch.object(module, "RagPipelineService", Service), ): - response, status = method(api, "pipeline-1") + response, status = method(api, tenant_id, account, "pipeline-1") assert (response, status) == ("", 204) assert service_calls == [ @@ -341,5 +371,7 @@ class TestPublishCustomizedPipelineTemplateApi: "icon_url": None, }, }, + account, + tenant_id, ) ] diff --git a/api/tests/unit_tests/controllers/console/datasets/test_hit_testing.py b/api/tests/unit_tests/controllers/console/datasets/test_hit_testing.py index faedd4d7e1d..3de780f3bbb 100644 --- a/api/tests/unit_tests/controllers/console/datasets/test_hit_testing.py +++ b/api/tests/unit_tests/controllers/console/datasets/test_hit_testing.py @@ -1,4 +1,5 @@ import uuid +from inspect import unwrap from unittest.mock import PropertyMock, patch import pytest @@ -8,16 +9,10 @@ from werkzeug.exceptions import NotFound from controllers.console import console_ns from controllers.console.datasets.hit_testing import HitTestingApi +from models.account import Account, Tenant, TenantAccountRole from models.dataset import Dataset -def unwrap(func): - """Recursively unwrap decorated functions.""" - while hasattr(func, "__wrapped__"): - func = func.__wrapped__ - return func - - @pytest.fixture def app(): app = Flask("test_hit_testing") @@ -35,6 +30,17 @@ def dataset(): return Dataset(id="dataset-1", tenant_id="tenant-1", name="Dataset", created_by="account-1") +@pytest.fixture +def account() -> Account: + account = Account(name="User", email="user@example.com") + account.id = "account-1" + tenant = Tenant(name="Tenant") + tenant.id = "tenant-1" + account._current_tenant = tenant + account.role = TenantAccountRole.OWNER + return account + + def hit_testing_record() -> dict[str, object]: return { "segment": { @@ -98,7 +104,7 @@ def bypass_decorators(mocker: MockerFixture): class TestHitTestingApi: - def test_hit_testing_success(self, app: Flask, dataset, dataset_id): + def test_hit_testing_success(self, app: Flask, dataset, dataset_id, account: Account): api = HitTestingApi() method = unwrap(api.post) @@ -129,13 +135,13 @@ class TestHitTestingApi: return_value={"query": {"content": "what is vector search"}, "records": []}, ), ): - result = method(api, dataset_id) + result = method(api, account, "tenant-1", dataset_id) assert "query" in result assert "records" in result assert result["records"] == [] - def test_hit_testing_success_with_optional_record_fields(self, app: Flask, dataset, dataset_id): + def test_hit_testing_success_with_optional_record_fields(self, app: Flask, dataset, dataset_id, account: Account): api = HitTestingApi() method = unwrap(api.post) @@ -167,7 +173,7 @@ class TestHitTestingApi: return_value={"query": {"content": payload["query"]}, "records": records}, ), ): - result = method(api, dataset_id) + result = method(api, account, "tenant-1", dataset_id) assert result["query"] == {"content": payload["query"]} assert result["records"][0]["segment"]["keywords"] == [] @@ -175,7 +181,7 @@ class TestHitTestingApi: assert result["records"][0]["files"] == [] assert result["records"][0]["score"] is None - def test_hit_testing_dataset_not_found(self, app: Flask, dataset_id): + def test_hit_testing_dataset_not_found(self, app: Flask, dataset_id, account: Account): api = HitTestingApi() method = unwrap(api.post) @@ -198,9 +204,9 @@ class TestHitTestingApi: ), ): with pytest.raises(NotFound, match="Dataset not found"): - method(api, dataset_id) + method(api, account, "tenant-1", dataset_id) - def test_hit_testing_invalid_args(self, app: Flask, dataset, dataset_id): + def test_hit_testing_invalid_args(self, app: Flask, dataset, dataset_id, account: Account): api = HitTestingApi() method = unwrap(api.post) @@ -228,4 +234,4 @@ class TestHitTestingApi: ), ): with pytest.raises(ValueError, match="Invalid parameters"): - method(api, dataset_id) + method(api, account, "tenant-1", dataset_id) diff --git a/api/tests/unit_tests/controllers/console/datasets/test_hit_testing_base.py b/api/tests/unit_tests/controllers/console/datasets/test_hit_testing_base.py index 072aa559dff..0fcf0df5262 100644 --- a/api/tests/unit_tests/controllers/console/datasets/test_hit_testing_base.py +++ b/api/tests/unit_tests/controllers/console/datasets/test_hit_testing_base.py @@ -1,4 +1,4 @@ -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest from werkzeug.exceptions import Forbidden, InternalServerError, NotFound @@ -21,7 +21,7 @@ from core.errors.error import ( QuotaExceededError, ) from graphon.model_runtime.errors.invoke import InvokeError -from models.account import Account +from models.account import Account, Tenant, TenantAccountRole from models.dataset import Dataset from services.dataset_service import DatasetService from services.hit_testing_service import HitTestingService @@ -29,19 +29,15 @@ from services.hit_testing_service import HitTestingService @pytest.fixture def account(): - acc = MagicMock(spec=Account) + acc = Account(name="User", email="user@example.com") + acc.id = "account-1" + tenant = Tenant(name="Tenant") + tenant.id = "tenant-1" + acc._current_tenant = tenant + acc.role = TenantAccountRole.OWNER return acc -@pytest.fixture(autouse=True) -def patch_current_user(mocker, account): - """Patch current_user to a valid Account.""" - mocker.patch( - "controllers.console.datasets.hit_testing_base.current_user", - account, - ) - - @pytest.fixture def dataset(): return Dataset(id="dataset-1", tenant_id="tenant-1", name="Dataset", created_by="account-1") @@ -86,7 +82,7 @@ def hit_testing_record() -> dict[str, object]: class TestGetAndValidateDataset: - def test_success(self, dataset): + def test_success(self, dataset, account): with ( patch.object( DatasetService, @@ -98,20 +94,20 @@ class TestGetAndValidateDataset: "check_dataset_permission", ), ): - result = DatasetsHitTestingBase.get_and_validate_dataset("dataset-1") + result = DatasetsHitTestingBase.get_and_validate_dataset("dataset-1", account, "tenant-1") assert result == dataset - def test_dataset_not_found(self): + def test_dataset_not_found(self, account): with patch.object( DatasetService, "get_dataset", return_value=None, ): with pytest.raises(NotFound, match="Dataset not found"): - DatasetsHitTestingBase.get_and_validate_dataset("dataset-1") + DatasetsHitTestingBase.get_and_validate_dataset("dataset-1", account, "tenant-1") - def test_permission_denied(self, dataset): + def test_permission_denied(self, dataset, account): with ( patch.object( DatasetService, @@ -125,7 +121,7 @@ class TestGetAndValidateDataset: ), ): with pytest.raises(Forbidden, match="no access"): - DatasetsHitTestingBase.get_and_validate_dataset("dataset-1") + DatasetsHitTestingBase.get_and_validate_dataset("dataset-1", account, "tenant-1") class TestHitTestingArgsCheck: @@ -164,7 +160,7 @@ class TestParseArgs: class TestPerformHitTesting: - def test_success(self, dataset): + def test_success(self, dataset, account): response = { "query": {"content": "hello"}, "records": [], @@ -175,12 +171,12 @@ class TestPerformHitTesting: "retrieve", return_value=response, ): - result = DatasetsHitTestingBase.perform_hit_testing(dataset, {"query": "hello"}) + result = DatasetsHitTestingBase.perform_hit_testing(dataset, {"query": "hello"}, account, "tenant-1") assert result["query"] == {"content": "hello"} assert result["records"] == [] - def test_success_prepares_nullable_list_fields(self, dataset): + def test_success_prepares_nullable_list_fields(self, dataset, account): response = { "query": {"content": "hello"}, "records": [hit_testing_record()], @@ -191,7 +187,7 @@ class TestPerformHitTesting: "retrieve", return_value=response, ): - result = DatasetsHitTestingBase.perform_hit_testing(dataset, {"query": "hello"}) + result = DatasetsHitTestingBase.perform_hit_testing(dataset, {"query": "hello"}, account, "tenant-1") assert result["query"] == {"content": "hello"} record = result["records"][0] @@ -203,7 +199,7 @@ class TestPerformHitTesting: assert record["tsne_position"] is None assert record["summary"] is None - def test_invalid_query_response_raises_value_error(self, dataset): + def test_invalid_query_response_raises_value_error(self, dataset, account): with ( patch.object( HitTestingService, @@ -212,7 +208,7 @@ class TestPerformHitTesting: ), pytest.raises(ValueError, match="Invalid hit testing query response"), ): - DatasetsHitTestingBase.perform_hit_testing(dataset, {"query": "hello"}) + DatasetsHitTestingBase.perform_hit_testing(dataset, {"query": "hello"}, account, "tenant-1") def test_invalid_records_response_raises_value_error(self): with pytest.raises(ValueError, match="Invalid hit testing records response"): @@ -222,74 +218,74 @@ class TestPerformHitTesting: with pytest.raises(ValueError, match="Invalid hit testing record response"): DatasetsHitTestingBase._prepare_hit_testing_records(["record"]) - def test_index_not_initialized(self, dataset): + def test_index_not_initialized(self, dataset, account): with patch.object( HitTestingService, "retrieve", side_effect=services.errors.index.IndexNotInitializedError(), ): with pytest.raises(DatasetNotInitializedError): - DatasetsHitTestingBase.perform_hit_testing(dataset, {"query": "hello"}) + DatasetsHitTestingBase.perform_hit_testing(dataset, {"query": "hello"}, account, "tenant-1") - def test_provider_token_not_init(self, dataset): + def test_provider_token_not_init(self, dataset, account): with patch.object( HitTestingService, "retrieve", side_effect=ProviderTokenNotInitError("token missing"), ): with pytest.raises(ProviderNotInitializeError): - DatasetsHitTestingBase.perform_hit_testing(dataset, {"query": "hello"}) + DatasetsHitTestingBase.perform_hit_testing(dataset, {"query": "hello"}, account, "tenant-1") - def test_quota_exceeded(self, dataset): + def test_quota_exceeded(self, dataset, account): with patch.object( HitTestingService, "retrieve", side_effect=QuotaExceededError(), ): with pytest.raises(ProviderQuotaExceededError): - DatasetsHitTestingBase.perform_hit_testing(dataset, {"query": "hello"}) + DatasetsHitTestingBase.perform_hit_testing(dataset, {"query": "hello"}, account, "tenant-1") - def test_model_not_supported(self, dataset): + def test_model_not_supported(self, dataset, account): with patch.object( HitTestingService, "retrieve", side_effect=ModelCurrentlyNotSupportError(), ): with pytest.raises(ProviderModelCurrentlyNotSupportError): - DatasetsHitTestingBase.perform_hit_testing(dataset, {"query": "hello"}) + DatasetsHitTestingBase.perform_hit_testing(dataset, {"query": "hello"}, account, "tenant-1") - def test_llm_bad_request(self, dataset): + def test_llm_bad_request(self, dataset, account): with patch.object( HitTestingService, "retrieve", side_effect=LLMBadRequestError("bad request"), ): with pytest.raises(ProviderNotInitializeError): - DatasetsHitTestingBase.perform_hit_testing(dataset, {"query": "hello"}) + DatasetsHitTestingBase.perform_hit_testing(dataset, {"query": "hello"}, account, "tenant-1") - def test_invoke_error(self, dataset): + def test_invoke_error(self, dataset, account): with patch.object( HitTestingService, "retrieve", side_effect=InvokeError("invoke failed"), ): with pytest.raises(CompletionRequestError): - DatasetsHitTestingBase.perform_hit_testing(dataset, {"query": "hello"}) + DatasetsHitTestingBase.perform_hit_testing(dataset, {"query": "hello"}, account, "tenant-1") - def test_value_error(self, dataset): + def test_value_error(self, dataset, account): with patch.object( HitTestingService, "retrieve", side_effect=ValueError("bad args"), ): with pytest.raises(ValueError, match="bad args"): - DatasetsHitTestingBase.perform_hit_testing(dataset, {"query": "hello"}) + DatasetsHitTestingBase.perform_hit_testing(dataset, {"query": "hello"}, account, "tenant-1") - def test_unexpected_error(self, dataset): + def test_unexpected_error(self, dataset, account): with patch.object( HitTestingService, "retrieve", side_effect=Exception("boom"), ): with pytest.raises(InternalServerError, match="boom"): - DatasetsHitTestingBase.perform_hit_testing(dataset, {"query": "hello"}) + DatasetsHitTestingBase.perform_hit_testing(dataset, {"query": "hello"}, account, "tenant-1") diff --git a/api/tests/unit_tests/controllers/console/datasets/test_metadata.py b/api/tests/unit_tests/controllers/console/datasets/test_metadata.py index 3015ed6604b..785c0ac09f2 100644 --- a/api/tests/unit_tests/controllers/console/datasets/test_metadata.py +++ b/api/tests/unit_tests/controllers/console/datasets/test_metadata.py @@ -1,4 +1,5 @@ import uuid +from inspect import unwrap from unittest.mock import MagicMock, PropertyMock, patch import pytest @@ -14,6 +15,7 @@ from controllers.console.datasets.metadata import ( DatasetMetadataCreateApi, DocumentMetadataEditApi, ) +from models.account import Account from services.dataset_service import DatasetService from services.entities.knowledge_entities.knowledge_entities import ( MetadataArgs, @@ -22,13 +24,6 @@ from services.entities.knowledge_entities.knowledge_entities import ( from services.metadata_service import MetadataService -def unwrap(func): - """Recursively unwrap decorated functions.""" - while hasattr(func, "__wrapped__"): - func = func.__wrapped__ - return func - - @pytest.fixture def app(): app = Flask("test_dataset_metadata") @@ -37,8 +32,8 @@ def app(): @pytest.fixture -def current_user(): - user = MagicMock() +def current_user() -> Account: + user = Account(name="Test User", email="test@example.com") user.id = "user-1" return user @@ -116,7 +111,7 @@ class TestDatasetMetadataCreateApi: return_value={"id": "m1", "type": "string", "name": "author"}, ), ): - result, status = method(api, current_user, dataset_id) + result, status = method(api, "tenant-1", current_user, dataset_id) assert status == 201 assert result["type"] == "string" @@ -151,7 +146,7 @@ class TestDatasetMetadataCreateApi: ), ): with pytest.raises(NotFound, match="Dataset not found"): - method(api, current_user, dataset_id) + method(api, "tenant-1", current_user, dataset_id) class TestDatasetMetadataGetApi: @@ -227,7 +222,7 @@ class TestDatasetMetadataApi: return_value={"id": "m1", "type": "string", "name": "updated-name"}, ), ): - result, status = method(api, current_user, dataset_id, metadata_id) + result, status = method(api, "tenant-1", current_user, dataset_id, metadata_id) assert status == 200 assert result["type"] == "string" diff --git a/api/tests/unit_tests/controllers/console/explore/test_completion.py b/api/tests/unit_tests/controllers/console/explore/test_completion.py index 420392f1dfa..8b9121c4d7f 100644 --- a/api/tests/unit_tests/controllers/console/explore/test_completion.py +++ b/api/tests/unit_tests/controllers/console/explore/test_completion.py @@ -1,3 +1,4 @@ +from inspect import unwrap from unittest.mock import MagicMock, PropertyMock, patch import pytest @@ -15,15 +16,11 @@ from models.model import AppMode from services.errors.llm import InvokeRateLimitError -def unwrap(func): - while hasattr(func, "__wrapped__"): - func = func.__wrapped__ - return func - - @pytest.fixture def user(): - return MagicMock(spec=Account) + account = Account(name="User", email="user.com") + account.id = "uid" + return account @pytest.fixture @@ -59,7 +56,6 @@ class TestCompletionApi: with ( app.test_request_context("/", json={}), payload_patch, - patch.object(completion_module, "current_user", user), patch.object( completion_module.AppGenerateService, "generate", @@ -71,18 +67,18 @@ class TestCompletionApi: return_value=("ok", 200), ), ): - result = method(completion_app) + result = method(api, user, completion_app) assert result == ("ok", 200) - def test_post_wrong_app_mode(self): + def test_post_wrong_app_mode(self, user): api = completion_module.CompletionApi() method = unwrap(api.post) installed_app = MagicMock(app=MagicMock(mode=AppMode.CHAT)) with pytest.raises(NotCompletionAppError): - method(installed_app) + method(api, user, installed_app) def test_conversation_completed(self, app: Flask, completion_app, user, payload_patch): api = completion_module.CompletionApi() @@ -91,7 +87,6 @@ class TestCompletionApi: with ( app.test_request_context("/", json={}), payload_patch, - patch.object(completion_module, "current_user", user), patch.object( completion_module.AppGenerateService, "generate", @@ -99,7 +94,7 @@ class TestCompletionApi: ), ): with pytest.raises(ConversationCompletedError): - method(completion_app) + method(api, user, completion_app) def test_internal_error(self, app: Flask, completion_app, user, payload_patch): api = completion_module.CompletionApi() @@ -108,7 +103,6 @@ class TestCompletionApi: with ( app.test_request_context("/", json={}), payload_patch, - patch.object(completion_module, "current_user", user), patch.object( completion_module.AppGenerateService, "generate", @@ -116,7 +110,7 @@ class TestCompletionApi: ), ): with pytest.raises(InternalServerError): - method(completion_app) + method(api, user, completion_app) def test_conversation_not_exists(self, app: Flask, completion_app, user, payload_patch): api = completion_module.CompletionApi() @@ -125,7 +119,6 @@ class TestCompletionApi: with ( app.test_request_context("/", json={}), payload_patch, - patch.object(completion_module, "current_user", user), patch.object( completion_module.AppGenerateService, "generate", @@ -133,7 +126,7 @@ class TestCompletionApi: ), ): with pytest.raises(completion_module.NotFound): - method(completion_app) + method(api, user, completion_app) def test_app_unavailable(self, app: Flask, completion_app, user, payload_patch): api = completion_module.CompletionApi() @@ -142,7 +135,6 @@ class TestCompletionApi: with ( app.test_request_context("/", json={}), payload_patch, - patch.object(completion_module, "current_user", user), patch.object( completion_module.AppGenerateService, "generate", @@ -150,7 +142,7 @@ class TestCompletionApi: ), ): with pytest.raises(completion_module.AppUnavailableError): - method(completion_app) + method(api, user, completion_app) def test_provider_not_initialized(self, app: Flask, completion_app, user, payload_patch): api = completion_module.CompletionApi() @@ -159,7 +151,6 @@ class TestCompletionApi: with ( app.test_request_context("/", json={}), payload_patch, - patch.object(completion_module, "current_user", user), patch.object( completion_module.AppGenerateService, "generate", @@ -167,7 +158,7 @@ class TestCompletionApi: ), ): with pytest.raises(completion_module.ProviderNotInitializeError): - method(completion_app) + method(api, user, completion_app) def test_quota_exceeded(self, app: Flask, completion_app, user, payload_patch): api = completion_module.CompletionApi() @@ -176,7 +167,6 @@ class TestCompletionApi: with ( app.test_request_context("/", json={}), payload_patch, - patch.object(completion_module, "current_user", user), patch.object( completion_module.AppGenerateService, "generate", @@ -184,7 +174,7 @@ class TestCompletionApi: ), ): with pytest.raises(completion_module.ProviderQuotaExceededError): - method(completion_app) + method(api, user, completion_app) def test_model_not_supported(self, app: Flask, completion_app, user, payload_patch): api = completion_module.CompletionApi() @@ -193,7 +183,6 @@ class TestCompletionApi: with ( app.test_request_context("/", json={}), payload_patch, - patch.object(completion_module, "current_user", user), patch.object( completion_module.AppGenerateService, "generate", @@ -201,7 +190,7 @@ class TestCompletionApi: ), ): with pytest.raises(completion_module.ProviderModelCurrentlyNotSupportError): - method(completion_app) + method(api, user, completion_app) def test_invoke_error(self, app: Flask, completion_app, user, payload_patch): api = completion_module.CompletionApi() @@ -210,7 +199,6 @@ class TestCompletionApi: with ( app.test_request_context("/", json={}), payload_patch, - patch.object(completion_module, "current_user", user), patch.object( completion_module.AppGenerateService, "generate", @@ -218,7 +206,7 @@ class TestCompletionApi: ), ): with pytest.raises(completion_module.CompletionRequestError): - method(completion_app) + method(api, user, completion_app) class TestCompletionStopApi: @@ -250,7 +238,6 @@ class TestChatApi: with ( app.test_request_context("/", json={}), payload_patch, - patch.object(completion_module, "current_user", user), patch.object( completion_module.AppGenerateService, "generate", @@ -262,18 +249,18 @@ class TestChatApi: return_value=("ok", 200), ), ): - result = method(chat_app) + result = method(api, user, chat_app) assert result == ("ok", 200) - def test_post_not_chat_app(self): + def test_post_not_chat_app(self, user): api = completion_module.ChatApi() method = unwrap(api.post) installed_app = MagicMock(app=MagicMock(mode=AppMode.COMPLETION)) with pytest.raises(NotChatAppError): - method(installed_app) + method(api, user, installed_app) def test_rate_limit_error(self, app: Flask, chat_app, user, payload_patch): api = completion_module.ChatApi() @@ -282,7 +269,6 @@ class TestChatApi: with ( app.test_request_context("/", json={}), payload_patch, - patch.object(completion_module, "current_user", user), patch.object( completion_module.AppGenerateService, "generate", @@ -290,7 +276,7 @@ class TestChatApi: ), ): with pytest.raises(InvokeRateLimitHttpError): - method(chat_app) + method(api, user, chat_app) def test_conversation_completed_chat(self, app: Flask, chat_app, user, payload_patch): api = completion_module.ChatApi() @@ -299,7 +285,6 @@ class TestChatApi: with ( app.test_request_context("/", json={}), payload_patch, - patch.object(completion_module, "current_user", user), patch.object( completion_module.AppGenerateService, "generate", @@ -307,7 +292,7 @@ class TestChatApi: ), ): with pytest.raises(ConversationCompletedError): - method(chat_app) + method(api, user, chat_app) def test_conversation_not_exists_chat(self, app: Flask, chat_app, user, payload_patch): api = completion_module.ChatApi() @@ -316,7 +301,6 @@ class TestChatApi: with ( app.test_request_context("/", json={}), payload_patch, - patch.object(completion_module, "current_user", user), patch.object( completion_module.AppGenerateService, "generate", @@ -324,7 +308,7 @@ class TestChatApi: ), ): with pytest.raises(completion_module.NotFound): - method(chat_app) + method(api, user, chat_app) def test_app_unavailable_chat(self, app: Flask, chat_app, user, payload_patch): api = completion_module.ChatApi() @@ -333,7 +317,6 @@ class TestChatApi: with ( app.test_request_context("/", json={}), payload_patch, - patch.object(completion_module, "current_user", user), patch.object( completion_module.AppGenerateService, "generate", @@ -341,7 +324,7 @@ class TestChatApi: ), ): with pytest.raises(completion_module.AppUnavailableError): - method(chat_app) + method(api, user, chat_app) def test_provider_not_initialized_chat(self, app: Flask, chat_app, user, payload_patch): api = completion_module.ChatApi() @@ -350,7 +333,6 @@ class TestChatApi: with ( app.test_request_context("/", json={}), payload_patch, - patch.object(completion_module, "current_user", user), patch.object( completion_module.AppGenerateService, "generate", @@ -358,7 +340,7 @@ class TestChatApi: ), ): with pytest.raises(completion_module.ProviderNotInitializeError): - method(chat_app) + method(api, user, chat_app) def test_quota_exceeded_chat(self, app: Flask, chat_app, user, payload_patch): api = completion_module.ChatApi() @@ -367,7 +349,6 @@ class TestChatApi: with ( app.test_request_context("/", json={}), payload_patch, - patch.object(completion_module, "current_user", user), patch.object( completion_module.AppGenerateService, "generate", @@ -375,7 +356,7 @@ class TestChatApi: ), ): with pytest.raises(completion_module.ProviderQuotaExceededError): - method(chat_app) + method(api, user, chat_app) def test_model_not_supported_chat(self, app: Flask, chat_app, user, payload_patch): api = completion_module.ChatApi() @@ -384,7 +365,6 @@ class TestChatApi: with ( app.test_request_context("/", json={}), payload_patch, - patch.object(completion_module, "current_user", user), patch.object( completion_module.AppGenerateService, "generate", @@ -392,7 +372,7 @@ class TestChatApi: ), ): with pytest.raises(completion_module.ProviderModelCurrentlyNotSupportError): - method(chat_app) + method(api, user, chat_app) def test_invoke_error_chat(self, app: Flask, chat_app, user, payload_patch): api = completion_module.ChatApi() @@ -401,7 +381,6 @@ class TestChatApi: with ( app.test_request_context("/", json={}), payload_patch, - patch.object(completion_module, "current_user", user), patch.object( completion_module.AppGenerateService, "generate", @@ -409,7 +388,7 @@ class TestChatApi: ), ): with pytest.raises(completion_module.CompletionRequestError): - method(chat_app) + method(api, user, chat_app) def test_internal_error_chat(self, app: Flask, chat_app, user, payload_patch): api = completion_module.ChatApi() @@ -418,7 +397,6 @@ class TestChatApi: with ( app.test_request_context("/", json={}), payload_patch, - patch.object(completion_module, "current_user", user), patch.object( completion_module.AppGenerateService, "generate", @@ -426,7 +404,7 @@ class TestChatApi: ), ): with pytest.raises(InternalServerError): - method(chat_app) + method(api, user, chat_app) class TestChatStopApi: diff --git a/api/tests/unit_tests/controllers/console/explore/test_trial.py b/api/tests/unit_tests/controllers/console/explore/test_trial.py index 641209d1deb..be68a3beed6 100644 --- a/api/tests/unit_tests/controllers/console/explore/test_trial.py +++ b/api/tests/unit_tests/controllers/console/explore/test_trial.py @@ -1,4 +1,6 @@ +from inspect import unwrap as inspect_unwrap from io import BytesIO +from typing import Any from unittest.mock import MagicMock, patch from uuid import uuid4 @@ -33,22 +35,24 @@ from models.model import AppMode from services.errors.conversation import ConversationNotExistsError from services.errors.llm import InvokeRateLimitError - -def unwrap(func): - while hasattr(func, "__wrapped__"): - func = func.__wrapped__ - return func +unwrap: Any = inspect_unwrap @pytest.fixture -def account(): - acc = MagicMock(spec=Account) +def account() -> Account: + acc = Account(name="User", email="user@example.com") acc.id = "u1" return acc +def _file_data() -> Any: + file_data: Any = BytesIO(b"fake audio data") + file_data.filename = "test.wav" + return file_data + + @pytest.fixture -def trial_app_chat(): +def trial_app_chat() -> MagicMock: app = MagicMock() app.id = "a-chat" app.mode = AppMode.CHAT @@ -56,7 +60,7 @@ def trial_app_chat(): @pytest.fixture -def trial_app_completion(): +def trial_app_completion() -> MagicMock: app = MagicMock() app.id = "a-comp" app.mode = AppMode.COMPLETION @@ -64,7 +68,7 @@ def trial_app_completion(): @pytest.fixture -def trial_app_workflow(): +def trial_app_workflow() -> MagicMock: app = MagicMock() app.id = "a-workflow" app.mode = AppMode.WORKFLOW @@ -72,7 +76,7 @@ def trial_app_workflow(): @pytest.fixture -def valid_parameters(): +def valid_parameters() -> dict[str, object]: return { "user_input_form": [], "system_parameters": {}, @@ -88,41 +92,39 @@ def valid_parameters(): } -def test_trial_workflow_uses_trial_scoped_simple_account_model(): +def test_trial_workflow_uses_trial_scoped_simple_account_model() -> None: assert module.simple_account_model.name == "TrialSimpleAccount" assert hasattr(module.simple_account_model, "items") class TestTrialAppWorkflowRunApi: - def test_not_workflow_app(self, app: Flask): + def test_not_workflow_app(self, app: Flask, account: Account) -> None: api = module.TrialAppWorkflowRunApi() method = unwrap(api.post) with app.test_request_context("/"): with pytest.raises(NotWorkflowAppError): - method(api, MagicMock(mode=AppMode.CHAT)) + method(api, account, MagicMock(mode=AppMode.CHAT)) - def test_success(self, app: Flask, trial_app_workflow, account): + def test_success(self, app: Flask, trial_app_workflow: MagicMock, account: Account) -> None: api = module.TrialAppWorkflowRunApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}}), - patch.object(module, "current_user", account), patch.object(module.AppGenerateService, "generate", return_value=MagicMock()), patch.object(module.RecommendedAppService, "add_trial_app_record"), ): - result = method(api, trial_app_workflow) + result = method(api, account, trial_app_workflow) assert result is not None - def test_workflow_provider_not_init(self, app: Flask, trial_app_workflow, account): + def test_workflow_provider_not_init(self, app: Flask, trial_app_workflow: MagicMock, account: Account) -> None: api = module.TrialAppWorkflowRunApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}}), - patch.object(module, "current_user", account), patch.object( module.AppGenerateService, "generate", @@ -130,15 +132,14 @@ class TestTrialAppWorkflowRunApi: ), ): with pytest.raises(ProviderNotInitializeError): - method(api, trial_app_workflow) + method(api, account, trial_app_workflow) - def test_workflow_quota_exceeded(self, app: Flask, trial_app_workflow, account): + def test_workflow_quota_exceeded(self, app: Flask, trial_app_workflow: MagicMock, account: Account) -> None: api = module.TrialAppWorkflowRunApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}}), - patch.object(module, "current_user", account), patch.object( module.AppGenerateService, "generate", @@ -146,15 +147,14 @@ class TestTrialAppWorkflowRunApi: ), ): with pytest.raises(ProviderQuotaExceededError): - method(api, trial_app_workflow) + method(api, account, trial_app_workflow) - def test_workflow_model_not_support(self, app: Flask, trial_app_workflow, account): + def test_workflow_model_not_support(self, app: Flask, trial_app_workflow: MagicMock, account: Account) -> None: api = module.TrialAppWorkflowRunApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}}), - patch.object(module, "current_user", account), patch.object( module.AppGenerateService, "generate", @@ -162,15 +162,14 @@ class TestTrialAppWorkflowRunApi: ), ): with pytest.raises(ProviderModelCurrentlyNotSupportError): - method(api, trial_app_workflow) + method(api, account, trial_app_workflow) - def test_workflow_invoke_error(self, app: Flask, trial_app_workflow, account): + def test_workflow_invoke_error(self, app: Flask, trial_app_workflow: MagicMock, account: Account) -> None: api = module.TrialAppWorkflowRunApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}}), - patch.object(module, "current_user", account), patch.object( module.AppGenerateService, "generate", @@ -178,15 +177,14 @@ class TestTrialAppWorkflowRunApi: ), ): with pytest.raises(CompletionRequestError): - method(api, trial_app_workflow) + method(api, account, trial_app_workflow) - def test_workflow_rate_limit_error(self, app: Flask, trial_app_workflow, account): + def test_workflow_rate_limit_error(self, app: Flask, trial_app_workflow: MagicMock, account: Account) -> None: api = module.TrialAppWorkflowRunApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}}), - patch.object(module, "current_user", account), patch.object( module.AppGenerateService, "generate", @@ -194,15 +192,14 @@ class TestTrialAppWorkflowRunApi: ), ): with pytest.raises(InvokeRateLimitHttpError): - method(api, trial_app_workflow) + method(api, account, trial_app_workflow) - def test_workflow_value_error(self, app: Flask, trial_app_workflow, account): + def test_workflow_value_error(self, app: Flask, trial_app_workflow: MagicMock, account: Account) -> None: api = module.TrialAppWorkflowRunApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}, "files": []}), - patch.object(module, "current_user", account), patch.object( module.AppGenerateService, "generate", @@ -210,15 +207,14 @@ class TestTrialAppWorkflowRunApi: ), ): with pytest.raises(ValueError): - method(api, trial_app_workflow) + method(api, account, trial_app_workflow) - def test_workflow_generic_exception(self, app: Flask, trial_app_workflow, account): + def test_workflow_generic_exception(self, app: Flask, trial_app_workflow: MagicMock, account: Account) -> None: api = module.TrialAppWorkflowRunApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}, "files": []}), - patch.object(module, "current_user", account), patch.object( module.AppGenerateService, "generate", @@ -226,39 +222,37 @@ class TestTrialAppWorkflowRunApi: ), ): with pytest.raises(InternalServerError): - method(api, trial_app_workflow) + method(api, account, trial_app_workflow) class TestTrialChatApi: - def test_not_chat_app(self, app: Flask): + def test_not_chat_app(self, app: Flask, account: Account) -> None: api = module.TrialChatApi() method = unwrap(api.post) with app.test_request_context("/", json={"inputs": {}, "query": "hi"}): with pytest.raises(NotChatAppError): - method(api, MagicMock(mode="completion")) + method(api, account, MagicMock(mode="completion")) - def test_success(self, app: Flask, trial_app_chat, account): + def test_success(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}, "query": "hi"}), - patch.object(module, "current_user", account), patch.object(module.AppGenerateService, "generate", return_value=MagicMock()), patch.object(module.RecommendedAppService, "add_trial_app_record"), ): - result = method(api, trial_app_chat) + result = method(api, account, trial_app_chat) assert result is not None - def test_chat_conversation_not_exists(self, app: Flask, trial_app_chat, account): + def test_chat_conversation_not_exists(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}, "query": "hi"}), - patch.object(module, "current_user", account), patch.object( module.AppGenerateService, "generate", @@ -266,15 +260,14 @@ class TestTrialChatApi: ), ): with pytest.raises(NotFound): - method(api, trial_app_chat) + method(api, account, trial_app_chat) - def test_chat_conversation_completed(self, app: Flask, trial_app_chat, account): + def test_chat_conversation_completed(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}, "query": "hi"}), - patch.object(module, "current_user", account), patch.object( module.AppGenerateService, "generate", @@ -282,15 +275,14 @@ class TestTrialChatApi: ), ): with pytest.raises(ConversationCompletedError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) - def test_chat_app_config_broken(self, app: Flask, trial_app_chat, account): + def test_chat_app_config_broken(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}, "query": "hi"}), - patch.object(module, "current_user", account), patch.object( module.AppGenerateService, "generate", @@ -298,15 +290,14 @@ class TestTrialChatApi: ), ): with pytest.raises(AppUnavailableError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) - def test_chat_provider_not_init(self, app: Flask, trial_app_chat, account): + def test_chat_provider_not_init(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}, "query": "hi"}), - patch.object(module, "current_user", account), patch.object( module.AppGenerateService, "generate", @@ -314,15 +305,14 @@ class TestTrialChatApi: ), ): with pytest.raises(ProviderNotInitializeError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) - def test_chat_quota_exceeded(self, app: Flask, trial_app_chat, account): + def test_chat_quota_exceeded(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}, "query": "hi"}), - patch.object(module, "current_user", account), patch.object( module.AppGenerateService, "generate", @@ -330,15 +320,14 @@ class TestTrialChatApi: ), ): with pytest.raises(ProviderQuotaExceededError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) - def test_chat_model_not_support(self, app: Flask, trial_app_chat, account): + def test_chat_model_not_support(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}, "query": "hi"}), - patch.object(module, "current_user", account), patch.object( module.AppGenerateService, "generate", @@ -346,15 +335,14 @@ class TestTrialChatApi: ), ): with pytest.raises(ProviderModelCurrentlyNotSupportError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) - def test_chat_invoke_error(self, app: Flask, trial_app_chat, account): + def test_chat_invoke_error(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}, "query": "hi"}), - patch.object(module, "current_user", account), patch.object( module.AppGenerateService, "generate", @@ -362,15 +350,14 @@ class TestTrialChatApi: ), ): with pytest.raises(CompletionRequestError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) - def test_chat_rate_limit_error(self, app: Flask, trial_app_chat, account): + def test_chat_rate_limit_error(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}, "query": "hi"}), - patch.object(module, "current_user", account), patch.object( module.AppGenerateService, "generate", @@ -378,15 +365,14 @@ class TestTrialChatApi: ), ): with pytest.raises(InvokeRateLimitHttpError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) - def test_chat_value_error(self, app: Flask, trial_app_chat, account): + def test_chat_value_error(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}, "query": "hi"}), - patch.object(module, "current_user", account), patch.object( module.AppGenerateService, "generate", @@ -394,15 +380,14 @@ class TestTrialChatApi: ), ): with pytest.raises(ValueError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) - def test_chat_generic_exception(self, app: Flask, trial_app_chat, account): + def test_chat_generic_exception(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}, "query": "hi"}), - patch.object(module, "current_user", account), patch.object( module.AppGenerateService, "generate", @@ -410,39 +395,37 @@ class TestTrialChatApi: ), ): with pytest.raises(InternalServerError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) class TestTrialCompletionApi: - def test_not_completion_app(self, app: Flask): + def test_not_completion_app(self, app: Flask, account: Account) -> None: api = module.TrialCompletionApi() method = unwrap(api.post) with app.test_request_context("/", json={"inputs": {}, "query": ""}): with pytest.raises(NotCompletionAppError): - method(api, MagicMock(mode=AppMode.CHAT)) + method(api, account, MagicMock(mode=AppMode.CHAT)) - def test_success(self, app: Flask, trial_app_completion, account): + def test_success(self, app: Flask, trial_app_completion: MagicMock, account: Account) -> None: api = module.TrialCompletionApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}, "query": ""}), - patch.object(module, "current_user", account), patch.object(module.AppGenerateService, "generate", return_value=MagicMock()), patch.object(module.RecommendedAppService, "add_trial_app_record"), ): - result = method(api, trial_app_completion) + result = method(api, account, trial_app_completion) assert result is not None - def test_completion_app_config_broken(self, app: Flask, trial_app_completion, account): + def test_completion_app_config_broken(self, app: Flask, trial_app_completion: MagicMock, account: Account) -> None: api = module.TrialCompletionApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}, "query": ""}), - patch.object(module, "current_user", account), patch.object( module.AppGenerateService, "generate", @@ -450,15 +433,14 @@ class TestTrialCompletionApi: ), ): with pytest.raises(AppUnavailableError): - method(api, trial_app_completion) + method(api, account, trial_app_completion) - def test_completion_provider_not_init(self, app: Flask, trial_app_completion, account): + def test_completion_provider_not_init(self, app: Flask, trial_app_completion: MagicMock, account: Account) -> None: api = module.TrialCompletionApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}, "query": ""}), - patch.object(module, "current_user", account), patch.object( module.AppGenerateService, "generate", @@ -466,15 +448,14 @@ class TestTrialCompletionApi: ), ): with pytest.raises(ProviderNotInitializeError): - method(api, trial_app_completion) + method(api, account, trial_app_completion) - def test_completion_quota_exceeded(self, app: Flask, trial_app_completion, account): + def test_completion_quota_exceeded(self, app: Flask, trial_app_completion: MagicMock, account: Account) -> None: api = module.TrialCompletionApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}, "query": ""}), - patch.object(module, "current_user", account), patch.object( module.AppGenerateService, "generate", @@ -482,15 +463,14 @@ class TestTrialCompletionApi: ), ): with pytest.raises(ProviderQuotaExceededError): - method(api, trial_app_completion) + method(api, account, trial_app_completion) - def test_completion_model_not_support(self, app: Flask, trial_app_completion, account): + def test_completion_model_not_support(self, app: Flask, trial_app_completion: MagicMock, account: Account) -> None: api = module.TrialCompletionApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}, "query": ""}), - patch.object(module, "current_user", account), patch.object( module.AppGenerateService, "generate", @@ -498,15 +478,14 @@ class TestTrialCompletionApi: ), ): with pytest.raises(ProviderModelCurrentlyNotSupportError): - method(api, trial_app_completion) + method(api, account, trial_app_completion) - def test_completion_invoke_error(self, app: Flask, trial_app_completion, account): + def test_completion_invoke_error(self, app: Flask, trial_app_completion: MagicMock, account: Account) -> None: api = module.TrialCompletionApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}, "query": ""}), - patch.object(module, "current_user", account), patch.object( module.AppGenerateService, "generate", @@ -514,15 +493,14 @@ class TestTrialCompletionApi: ), ): with pytest.raises(CompletionRequestError): - method(api, trial_app_completion) + method(api, account, trial_app_completion) - def test_completion_rate_limit_error(self, app: Flask, trial_app_completion, account): + def test_completion_rate_limit_error(self, app: Flask, trial_app_completion: MagicMock, account: Account) -> None: api = module.TrialCompletionApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}, "query": ""}), - patch.object(module, "current_user", account), patch.object( module.AppGenerateService, "generate", @@ -530,15 +508,14 @@ class TestTrialCompletionApi: ), ): with pytest.raises(InternalServerError): - method(api, trial_app_completion) + method(api, account, trial_app_completion) - def test_completion_value_error(self, app: Flask, trial_app_completion, account): + def test_completion_value_error(self, app: Flask, trial_app_completion: MagicMock, account: Account) -> None: api = module.TrialCompletionApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}, "query": ""}), - patch.object(module, "current_user", account), patch.object( module.AppGenerateService, "generate", @@ -546,15 +523,14 @@ class TestTrialCompletionApi: ), ): with pytest.raises(ValueError): - method(api, trial_app_completion) + method(api, account, trial_app_completion) - def test_completion_generic_exception(self, app: Flask, trial_app_completion, account): + def test_completion_generic_exception(self, app: Flask, trial_app_completion: MagicMock, account: Account) -> None: api = module.TrialCompletionApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"inputs": {}, "query": ""}), - patch.object(module, "current_user", account), patch.object( module.AppGenerateService, "generate", @@ -562,42 +538,40 @@ class TestTrialCompletionApi: ), ): with pytest.raises(InternalServerError): - method(api, trial_app_completion) + method(api, account, trial_app_completion) class TestTrialMessageSuggestedQuestionApi: - def test_not_chat_app(self, app: Flask): + def test_not_chat_app(self, app: Flask, account: Account) -> None: api = module.TrialMessageSuggestedQuestionApi() method = unwrap(api.get) with app.test_request_context("/"): with pytest.raises(NotChatAppError): - method(MagicMock(mode="completion"), str(uuid4())) + method(api, account, MagicMock(mode="completion"), str(uuid4())) - def test_success(self, app: Flask, trial_app_chat, account): + def test_success(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialMessageSuggestedQuestionApi() method = unwrap(api.get) with ( app.test_request_context("/"), - patch.object(module, "current_user", account), patch.object( module.MessageService, "get_suggested_questions_after_answer", return_value=["q1", "q2"], ), ): - result = method(trial_app_chat, str(uuid4())) + result = method(api, account, trial_app_chat, str(uuid4())) assert result == {"data": ["q1", "q2"]} - def test_conversation_not_exists(self, app: Flask, trial_app_chat, account): + def test_conversation_not_exists(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialMessageSuggestedQuestionApi() method = unwrap(api.get) with ( app.test_request_context("/"), - patch.object(module, "current_user", account), patch.object( module.MessageService, "get_suggested_questions_after_answer", @@ -605,18 +579,18 @@ class TestTrialMessageSuggestedQuestionApi: ), ): with pytest.raises(NotFound): - method(trial_app_chat, str(uuid4())) + method(api, account, trial_app_chat, str(uuid4())) class TestTrialAppParameterApi: - def test_app_unavailable(self): + def test_app_unavailable(self) -> None: api = module.TrialAppParameterApi() method = unwrap(api.get) with pytest.raises(AppUnavailableError): method(api, None) - def test_success_non_workflow(self, valid_parameters): + def test_success_non_workflow(self, valid_parameters: dict[str, object]) -> None: api = module.TrialAppParameterApi() method = unwrap(api.get) @@ -643,37 +617,33 @@ class TestTrialAppParameterApi: class TestTrialChatAudioApi: - def test_success(self, app: Flask, trial_app_chat, account): + def test_success(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatAudioApi() method = unwrap(api.post) - file_data = BytesIO(b"fake audio data") - file_data.filename = "test.wav" + file_data = _file_data() with ( app.test_request_context( "/", method="POST", data={"file": (file_data, "test.wav")}, content_type="multipart/form-data" ), - patch.object(module, "current_user", account), patch.object(module.AudioService, "transcript_asr", return_value={"text": "hello"}), patch.object(module.RecommendedAppService, "add_trial_app_record"), ): - result = method(api, trial_app_chat) + result = method(api, account, trial_app_chat) assert result == {"text": "hello"} - def test_app_config_broken(self, app: Flask, trial_app_chat, account): + def test_app_config_broken(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatAudioApi() method = unwrap(api.post) - file_data = BytesIO(b"fake audio data") - file_data.filename = "test.wav" + file_data = _file_data() with ( app.test_request_context( "/", method="POST", data={"file": (file_data, "test.wav")}, content_type="multipart/form-data" ), - patch.object(module, "current_user", account), patch.object( module.AudioService, "transcript_asr", @@ -681,20 +651,18 @@ class TestTrialChatAudioApi: ), ): with pytest.raises(module.AppUnavailableError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) - def test_no_audio_uploaded(self, app: Flask, trial_app_chat, account): + def test_no_audio_uploaded(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatAudioApi() method = unwrap(api.post) - file_data = BytesIO(b"fake audio data") - file_data.filename = "test.wav" + file_data = _file_data() with ( app.test_request_context( "/", method="POST", data={"file": (file_data, "test.wav")}, content_type="multipart/form-data" ), - patch.object(module, "current_user", account), patch.object( module.AudioService, "transcript_asr", @@ -702,20 +670,18 @@ class TestTrialChatAudioApi: ), ): with pytest.raises(module.NoAudioUploadedError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) - def test_audio_too_large(self, app: Flask, trial_app_chat, account): + def test_audio_too_large(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatAudioApi() method = unwrap(api.post) - file_data = BytesIO(b"fake audio data") - file_data.filename = "test.wav" + file_data = _file_data() with ( app.test_request_context( "/", method="POST", data={"file": (file_data, "test.wav")}, content_type="multipart/form-data" ), - patch.object(module, "current_user", account), patch.object( module.AudioService, "transcript_asr", @@ -723,20 +689,18 @@ class TestTrialChatAudioApi: ), ): with pytest.raises(module.AudioTooLargeError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) - def test_unsupported_audio_type(self, app: Flask, trial_app_chat, account): + def test_unsupported_audio_type(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatAudioApi() method = unwrap(api.post) - file_data = BytesIO(b"fake audio data") - file_data.filename = "test.wav" + file_data = _file_data() with ( app.test_request_context( "/", method="POST", data={"file": (file_data, "test.wav")}, content_type="multipart/form-data" ), - patch.object(module, "current_user", account), patch.object( module.AudioService, "transcript_asr", @@ -744,20 +708,18 @@ class TestTrialChatAudioApi: ), ): with pytest.raises(module.UnsupportedAudioTypeError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) - def test_provider_not_support_tts(self, app: Flask, trial_app_chat, account): + def test_provider_not_support_tts(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatAudioApi() method = unwrap(api.post) - file_data = BytesIO(b"fake audio data") - file_data.filename = "test.wav" + file_data = _file_data() with ( app.test_request_context( "/", method="POST", data={"file": (file_data, "test.wav")}, content_type="multipart/form-data" ), - patch.object(module, "current_user", account), patch.object( module.AudioService, "transcript_asr", @@ -765,65 +727,59 @@ class TestTrialChatAudioApi: ), ): with pytest.raises(module.ProviderNotSupportSpeechToTextError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) - def test_provider_not_init(self, app: Flask, trial_app_chat, account): + def test_provider_not_init(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatAudioApi() method = unwrap(api.post) - file_data = BytesIO(b"fake audio data") - file_data.filename = "test.wav" + file_data = _file_data() with ( app.test_request_context( "/", method="POST", data={"file": (file_data, "test.wav")}, content_type="multipart/form-data" ), - patch.object(module, "current_user", account), patch.object(module.AudioService, "transcript_asr", side_effect=ProviderTokenNotInitError("test")), ): with pytest.raises(ProviderNotInitializeError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) - def test_quota_exceeded(self, app: Flask, trial_app_chat, account): + def test_quota_exceeded(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatAudioApi() method = unwrap(api.post) - file_data = BytesIO(b"fake audio data") - file_data.filename = "test.wav" + file_data = _file_data() with ( app.test_request_context( "/", method="POST", data={"file": (file_data, "test.wav")}, content_type="multipart/form-data" ), - patch.object(module, "current_user", account), patch.object(module.AudioService, "transcript_asr", side_effect=QuotaExceededError()), ): with pytest.raises(ProviderQuotaExceededError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) class TestTrialChatTextApi: - def test_success(self, app: Flask, trial_app_chat, account): + def test_success(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatTextApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"text": "hello", "voice": "en-US"}), - patch.object(module, "current_user", account), patch.object(module.AudioService, "transcript_tts", return_value={"audio": "base64_data"}), patch.object(module.RecommendedAppService, "add_trial_app_record"), ): - result = method(api, trial_app_chat) + result = method(api, account, trial_app_chat) assert result == {"audio": "base64_data"} - def test_app_config_broken(self, app: Flask, trial_app_chat, account): + def test_app_config_broken(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatTextApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"text": "hello", "voice": "en-US"}), - patch.object(module, "current_user", account), patch.object( module.AudioService, "transcript_tts", @@ -831,15 +787,14 @@ class TestTrialChatTextApi: ), ): with pytest.raises(module.AppUnavailableError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) - def test_provider_not_support(self, app: Flask, trial_app_chat, account): + def test_provider_not_support(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatTextApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"text": "hello", "voice": "en-US"}), - patch.object(module, "current_user", account), patch.object( module.AudioService, "transcript_tts", @@ -847,15 +802,14 @@ class TestTrialChatTextApi: ), ): with pytest.raises(module.ProviderNotSupportSpeechToTextError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) - def test_audio_too_large(self, app: Flask, trial_app_chat, account): + def test_audio_too_large(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatTextApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"text": "hello", "voice": "en-US"}), - patch.object(module, "current_user", account), patch.object( module.AudioService, "transcript_tts", @@ -863,15 +817,14 @@ class TestTrialChatTextApi: ), ): with pytest.raises(module.AudioTooLargeError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) - def test_no_audio_uploaded(self, app: Flask, trial_app_chat, account): + def test_no_audio_uploaded(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatTextApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"text": "hello", "voice": "en-US"}), - patch.object(module, "current_user", account), patch.object( module.AudioService, "transcript_tts", @@ -879,59 +832,55 @@ class TestTrialChatTextApi: ), ): with pytest.raises(module.NoAudioUploadedError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) - def test_provider_not_init(self, app: Flask, trial_app_chat, account): + def test_provider_not_init(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatTextApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"text": "hello", "voice": "en-US"}), - patch.object(module, "current_user", account), patch.object(module.AudioService, "transcript_tts", side_effect=ProviderTokenNotInitError("test")), ): with pytest.raises(ProviderNotInitializeError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) - def test_quota_exceeded(self, app: Flask, trial_app_chat, account): + def test_quota_exceeded(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatTextApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"text": "hello", "voice": "en-US"}), - patch.object(module, "current_user", account), patch.object(module.AudioService, "transcript_tts", side_effect=QuotaExceededError()), ): with pytest.raises(ProviderQuotaExceededError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) - def test_model_not_support(self, app: Flask, trial_app_chat, account): + def test_model_not_support(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatTextApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"text": "hello", "voice": "en-US"}), - patch.object(module, "current_user", account), patch.object(module.AudioService, "transcript_tts", side_effect=ModelCurrentlyNotSupportError()), ): with pytest.raises(ProviderModelCurrentlyNotSupportError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) - def test_invoke_error(self, app: Flask, trial_app_chat, account): + def test_invoke_error(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatTextApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"text": "hello", "voice": "en-US"}), - patch.object(module, "current_user", account), patch.object(module.AudioService, "transcript_tts", side_effect=InvokeError("test error")), ): with pytest.raises(CompletionRequestError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) class TestTrialAppWorkflowTaskStopApi: - def test_not_workflow_app(self, app: Flask, trial_app_chat): + def test_not_workflow_app(self, app: Flask, trial_app_chat: MagicMock) -> None: api = module.TrialAppWorkflowTaskStopApi() method = unwrap(api.post) @@ -939,14 +888,13 @@ class TestTrialAppWorkflowTaskStopApi: with pytest.raises(NotWorkflowAppError): method(api, trial_app_chat, str(uuid4())) - def test_success(self, app: Flask, trial_app_workflow, account): + def test_success(self, app: Flask, trial_app_workflow: MagicMock) -> None: api = module.TrialAppWorkflowTaskStopApi() method = unwrap(api.post) task_id = str(uuid4()) with ( app.test_request_context("/"), - patch.object(module, "current_user", account), patch.object(module.AppQueueManager, "set_stop_flag_no_user_check") as mock_set_flag, patch.object(module.GraphEngineManager, "send_stop_command") as mock_send_cmd, ): @@ -958,7 +906,7 @@ class TestTrialAppWorkflowTaskStopApi: class TestTrialSitApi: - def test_no_site(self, app: Flask): + def test_no_site(self, app: Flask) -> None: api = module.TrialSitApi() method = unwrap(api.get) app_model = MagicMock() @@ -969,7 +917,7 @@ class TestTrialSitApi: with pytest.raises(Forbidden): method(api, app_model) - def test_archived_tenant(self, app: Flask): + def test_archived_tenant(self, app: Flask) -> None: api = module.TrialSitApi() method = unwrap(api.get) @@ -984,7 +932,7 @@ class TestTrialSitApi: with pytest.raises(Forbidden): method(api, app_model) - def test_success(self, app: Flask): + def test_success(self, app: Flask) -> None: api = module.TrialSitApi() method = unwrap(api.get) @@ -1009,18 +957,16 @@ class TestTrialSitApi: class TestTrialChatAudioApiExceptionHandlers: - def test_provider_not_init(self, app: Flask, trial_app_chat, account): + def test_provider_not_init(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatAudioApi() method = unwrap(api.post) - file_data = BytesIO(b"fake audio data") - file_data.filename = "test.wav" + file_data = _file_data() with ( app.test_request_context( "/", method="POST", data={"file": (file_data, "test.wav")}, content_type="multipart/form-data" ), - patch.object(module, "current_user", account), patch.object( module.AudioService, "transcript_asr", @@ -1028,20 +974,18 @@ class TestTrialChatAudioApiExceptionHandlers: ), ): with pytest.raises(ProviderNotInitializeError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) - def test_quota_exceeded(self, app: Flask, trial_app_chat, account): + def test_quota_exceeded(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatAudioApi() method = unwrap(api.post) - file_data = BytesIO(b"fake audio data") - file_data.filename = "test.wav" + file_data = _file_data() with ( app.test_request_context( "/", method="POST", data={"file": (file_data, "test.wav")}, content_type="multipart/form-data" ), - patch.object(module, "current_user", account), patch.object( module.AudioService, "transcript_asr", @@ -1049,20 +993,18 @@ class TestTrialChatAudioApiExceptionHandlers: ), ): with pytest.raises(ProviderQuotaExceededError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) - def test_invoke_error(self, app: Flask, trial_app_chat, account): + def test_invoke_error(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatAudioApi() method = unwrap(api.post) - file_data = BytesIO(b"fake audio data") - file_data.filename = "test.wav" + file_data = _file_data() with ( app.test_request_context( "/", method="POST", data={"file": (file_data, "test.wav")}, content_type="multipart/form-data" ), - patch.object(module, "current_user", account), patch.object( module.AudioService, "transcript_asr", @@ -1070,17 +1012,16 @@ class TestTrialChatAudioApiExceptionHandlers: ), ): with pytest.raises(CompletionRequestError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) class TestTrialChatTextApiExceptionHandlers: - def test_app_config_broken(self, app: Flask, trial_app_chat, account): + def test_app_config_broken(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatTextApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"text": "hello", "voice": "en-US"}), - patch.object(module, "current_user", account), patch.object( module.AudioService, "transcript_tts", @@ -1088,15 +1029,14 @@ class TestTrialChatTextApiExceptionHandlers: ), ): with pytest.raises(module.AppUnavailableError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) - def test_unsupported_audio_type(self, app: Flask, trial_app_chat, account): + def test_unsupported_audio_type(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None: api = module.TrialChatTextApi() method = unwrap(api.post) with ( app.test_request_context("/", json={"text": "hello", "voice": "en-US"}), - patch.object(module, "current_user", account), patch.object( module.AudioService, "transcript_tts", @@ -1104,4 +1044,4 @@ class TestTrialChatTextApiExceptionHandlers: ), ): with pytest.raises(module.UnsupportedAudioTypeError): - method(api, trial_app_chat) + method(api, account, trial_app_chat) diff --git a/api/tests/unit_tests/controllers/console/test_feature.py b/api/tests/unit_tests/controllers/console/test_feature.py index d92454d6d84..3e804583a6e 100644 --- a/api/tests/unit_tests/controllers/console/test_feature.py +++ b/api/tests/unit_tests/controllers/console/test_feature.py @@ -1,32 +1,36 @@ +from inspect import unwrap + from pytest_mock import MockerFixture -from werkzeug.exceptions import Unauthorized + +from models import Account +from services.feature_service import FeatureModel, LimitationModel, SystemFeatureModel -def unwrap(func): - """ - Recursively unwrap decorated functions. - """ - while hasattr(func, "__wrapped__"): - func = func.__wrapped__ - return func +def make_account() -> Account: + account = Account(name="Alice", email="alice@example.com") + account.id = "account-1" + return account class TestFeatureApi: def test_get_tenant_features_success(self, mocker: MockerFixture): from controllers.console.feature import FeatureApi + features = FeatureModel( + knowledge_rate_limit=42, + vector_space=LimitationModel(size=1, limit=2), + ) get_features = mocker.patch("controllers.console.feature.FeatureService.get_features") - get_features.return_value.model_dump.return_value = { - "features": {"feature_a": True}, - "vector_space": {"size": 1, "limit": 2}, - } + get_features.return_value = features api = FeatureApi() raw_get = unwrap(FeatureApi.get) result = raw_get(api, "tenant_123") - assert result == {"features": {"feature_a": True}} + expected = features.model_dump() + expected.pop("vector_space") + assert result == expected get_features.assert_called_once_with("tenant_123", exclude_vector_space=True) @@ -35,7 +39,7 @@ class TestFeatureVectorSpaceApi: from controllers.console.feature import FeatureVectorSpaceApi get_vector_space = mocker.patch("controllers.console.feature.FeatureService.get_vector_space") - get_vector_space.return_value.model_dump.return_value = {"size": 5120, "limit": 20480} + get_vector_space.return_value = LimitationModel(size=5120, limit=20480) api = FeatureVectorSpaceApi() @@ -85,22 +89,23 @@ class TestSystemFeatureApi: from controllers.console.feature import SystemFeatureApi - fake_user = mocker.Mock() - fake_user.is_authenticated = True - - mocker.patch( - "controllers.console.feature.current_user", - fake_user, + account = make_account() + current_account = mocker.patch( + "controllers.console.feature.current_account_with_tenant_optional", + return_value=(account, "tenant-123"), + ) + system_features = SystemFeatureModel(is_allow_register=True) + get_system_features = mocker.patch( + "controllers.console.feature.FeatureService.get_system_features", + return_value=system_features, ) - - mocker.patch( - "controllers.console.feature.FeatureService.get_system_features" - ).return_value.model_dump.return_value = {"features": {"sys_feature": True}} api = SystemFeatureApi() result = api.get() - assert result == {"features": {"sys_feature": True}} + assert result == system_features.model_dump() + current_account.assert_called_once_with() + get_system_features.assert_called_once_with(is_authenticated=True) def test_get_system_features_unauthenticated(self, mocker: MockerFixture): """ @@ -109,19 +114,19 @@ class TestSystemFeatureApi: from controllers.console.feature import SystemFeatureApi - fake_user = mocker.Mock() - type(fake_user).is_authenticated = mocker.PropertyMock(side_effect=Unauthorized()) - - mocker.patch( - "controllers.console.feature.current_user", - fake_user, + current_account = mocker.patch( + "controllers.console.feature.current_account_with_tenant_optional", + return_value=(None, None), + ) + system_features = SystemFeatureModel(is_allow_register=False) + get_system_features = mocker.patch( + "controllers.console.feature.FeatureService.get_system_features", + return_value=system_features, ) - - mocker.patch( - "controllers.console.feature.FeatureService.get_system_features" - ).return_value.model_dump.return_value = {"features": {"sys_feature": False}} api = SystemFeatureApi() result = api.get() - assert result == {"features": {"sys_feature": False}} + assert result == system_features.model_dump() + current_account.assert_called_once_with() + get_system_features.assert_called_once_with(is_authenticated=False) diff --git a/api/tests/unit_tests/controllers/console/test_wraps.py b/api/tests/unit_tests/controllers/console/test_wraps.py index fb2ef55fe80..937505dab28 100644 --- a/api/tests/unit_tests/controllers/console/test_wraps.py +++ b/api/tests/unit_tests/controllers/console/test_wraps.py @@ -1,3 +1,4 @@ +from typing import override from unittest.mock import MagicMock, patch import pytest @@ -36,6 +37,7 @@ class MockUser(UserMixin): self.id = user_id self.current_tenant_id = "tenant123" + @override def get_id(self) -> str: return self.id @@ -210,6 +212,7 @@ class TestModelValidationInjection: Handler().post() assert exc_info.value.code == 422 + assert exc_info.value.description is not None assert "count" in exc_info.value.description diff --git a/api/tests/unit_tests/controllers/service_api/dataset/test_hit_testing.py b/api/tests/unit_tests/controllers/service_api/dataset/test_hit_testing.py index 4809cc0e8a2..f47de0c8d50 100644 --- a/api/tests/unit_tests/controllers/service_api/dataset/test_hit_testing.py +++ b/api/tests/unit_tests/controllers/service_api/dataset/test_hit_testing.py @@ -9,21 +9,21 @@ Strategy: - ``HitTestingApi.post`` is decorated with ``@cloud_edition_billing_rate_limit_check`` which preserves ``__wrapped__``. We call ``post.__wrapped__(self, ...)`` to skip the billing decorator and test the business logic directly. -- Base-class methods (``get_and_validate_dataset``, ``perform_hit_testing``) read - ``current_user`` from ``controllers.console.datasets.hit_testing_base``, so we - patch it there. +- ``validate_dataset_token`` installs the tenant owner account into Flask-Login's + request context before calling the handler, so direct method-call tests install + the same concrete account on ``g._login_user``. """ import uuid -from unittest.mock import Mock, patch +from unittest.mock import patch import pytest -from flask import Flask +from flask import Flask, g from werkzeug.exceptions import Forbidden, NotFound import services from controllers.service_api.dataset.hit_testing import HitTestingApi, HitTestingPayload -from models.account import Account +from models.account import Account, Tenant, TenantAccountRole from models.dataset import Dataset from services.entities.knowledge_entities.knowledge_entities import RetrievalModel @@ -131,13 +131,21 @@ class TestHitTestingApiPost: def _dataset(dataset_id: str, tenant_id: str) -> Dataset: return Dataset(id=dataset_id, tenant_id=tenant_id, name="Dataset", created_by="account-1") + @staticmethod + def _account(tenant_id: str) -> Account: + account = Account(name="Service API", email="service-api@example.com") + account.id = "account-1" + tenant = Tenant(name="Tenant") + tenant.id = tenant_id + account._current_tenant = tenant + account.role = TenantAccountRole.OWNER + return account + @patch("controllers.service_api.dataset.hit_testing.service_api_ns") @patch("controllers.console.datasets.hit_testing_base.HitTestingService") @patch("controllers.console.datasets.hit_testing_base.DatasetService") - @patch("controllers.console.datasets.hit_testing_base.current_user", new_callable=lambda: Mock(spec=Account)) def test_post_success( self, - mock_current_user, mock_dataset_svc, mock_hit_svc, mock_ns, @@ -148,6 +156,7 @@ class TestHitTestingApiPost: tenant_id = str(uuid.uuid4()) mock_dataset = self._dataset(dataset_id, tenant_id) + account = self._account(tenant_id) mock_dataset_svc.get_dataset.return_value = mock_dataset mock_dataset_svc.check_dataset_permission.return_value = None @@ -158,6 +167,8 @@ class TestHitTestingApiPost: mock_ns.payload = {"query": "test query"} with app.test_request_context(): + # TODO: the service APIs are NOT migrated yet, so we have to do the very dirty hack + g._login_user = account api = HitTestingApi() # Skip billing decorator via __wrapped__ response = HitTestingApi.post.__wrapped__(api, tenant_id, dataset_id) @@ -168,10 +179,8 @@ class TestHitTestingApiPost: @patch("controllers.service_api.dataset.hit_testing.service_api_ns") @patch("controllers.console.datasets.hit_testing_base.HitTestingService") @patch("controllers.console.datasets.hit_testing_base.DatasetService") - @patch("controllers.console.datasets.hit_testing_base.current_user", new_callable=lambda: Mock(spec=Account)) def test_post_with_retrieval_model( self, - mock_current_user, mock_dataset_svc, mock_hit_svc, mock_ns, @@ -182,6 +191,7 @@ class TestHitTestingApiPost: tenant_id = str(uuid.uuid4()) mock_dataset = self._dataset(dataset_id, tenant_id) + account = self._account(tenant_id) mock_dataset_svc.get_dataset.return_value = mock_dataset mock_dataset_svc.check_dataset_permission.return_value = None @@ -204,6 +214,8 @@ class TestHitTestingApiPost: } with app.test_request_context(): + # TODO: the service APIs are NOT migrated yet, so we have to do the very dirty hack + g._login_user = account api = HitTestingApi() response = HitTestingApi.post.__wrapped__(api, tenant_id, dataset_id) @@ -218,10 +230,8 @@ class TestHitTestingApiPost: @patch("controllers.service_api.dataset.hit_testing.service_api_ns") @patch("controllers.console.datasets.hit_testing_base.HitTestingService") @patch("controllers.console.datasets.hit_testing_base.DatasetService") - @patch("controllers.console.datasets.hit_testing_base.current_user", new_callable=lambda: Mock(spec=Account)) def test_post_preserves_retrieval_model_metadata_filtering_conditions( self, - mock_current_user, mock_dataset_svc, mock_hit_svc, mock_ns, @@ -232,6 +242,7 @@ class TestHitTestingApiPost: tenant_id = str(uuid.uuid4()) mock_dataset = self._dataset(dataset_id, tenant_id) + account = self._account(tenant_id) mock_dataset_svc.get_dataset.return_value = mock_dataset mock_dataset_svc.check_dataset_permission.return_value = None @@ -260,6 +271,8 @@ class TestHitTestingApiPost: } with app.test_request_context(): + # TODO: the service APIs are NOT migrated yet, so we have to do the very dirty hack + g._login_user = account api = HitTestingApi() HitTestingApi.post.__wrapped__(api, tenant_id, dataset_id) @@ -270,10 +283,8 @@ class TestHitTestingApiPost: @patch("controllers.service_api.dataset.hit_testing.service_api_ns") @patch("controllers.console.datasets.hit_testing_base.HitTestingService") @patch("controllers.console.datasets.hit_testing_base.DatasetService") - @patch("controllers.console.datasets.hit_testing_base.current_user", new_callable=lambda: Mock(spec=Account)) def test_post_prepares_nullable_list_fields( self, - mock_current_user, mock_dataset_svc, mock_hit_svc, mock_ns, @@ -284,6 +295,7 @@ class TestHitTestingApiPost: tenant_id = str(uuid.uuid4()) mock_dataset = self._dataset(dataset_id, tenant_id) + account = self._account(tenant_id) mock_dataset_svc.get_dataset.return_value = mock_dataset mock_dataset_svc.check_dataset_permission.return_value = None @@ -297,6 +309,8 @@ class TestHitTestingApiPost: mock_ns.payload = {"query": "legacy query"} with app.test_request_context(): + # TODO: the service APIs are NOT migrated yet, so we have to do the very dirty hack + g._login_user = account api = HitTestingApi() response = HitTestingApi.post.__wrapped__(api, tenant_id, dataset_id) @@ -312,10 +326,8 @@ class TestHitTestingApiPost: @patch("controllers.service_api.dataset.hit_testing.service_api_ns") @patch("controllers.console.datasets.hit_testing_base.DatasetService") - @patch("controllers.console.datasets.hit_testing_base.current_user", new_callable=lambda: Mock(spec=Account)) def test_post_dataset_not_found( self, - mock_current_user, mock_dataset_svc, mock_ns, app: Flask, @@ -323,21 +335,22 @@ class TestHitTestingApiPost: """Test hit testing with non-existent dataset.""" dataset_id = str(uuid.uuid4()) tenant_id = str(uuid.uuid4()) + account = self._account(tenant_id) mock_dataset_svc.get_dataset.return_value = None mock_ns.payload = {"query": "test query"} with app.test_request_context(): + # TODO: the service APIs are NOT migrated yet, so we have to do the very dirty hack + g._login_user = account api = HitTestingApi() with pytest.raises(NotFound): HitTestingApi.post.__wrapped__(api, tenant_id, dataset_id) @patch("controllers.service_api.dataset.hit_testing.service_api_ns") @patch("controllers.console.datasets.hit_testing_base.DatasetService") - @patch("controllers.console.datasets.hit_testing_base.current_user", new_callable=lambda: Mock(spec=Account)) def test_post_no_dataset_permission( self, - mock_current_user, mock_dataset_svc, mock_ns, app: Flask, @@ -347,6 +360,7 @@ class TestHitTestingApiPost: tenant_id = str(uuid.uuid4()) mock_dataset = self._dataset(dataset_id, tenant_id) + account = self._account(tenant_id) mock_dataset_svc.get_dataset.return_value = mock_dataset mock_dataset_svc.check_dataset_permission.side_effect = services.errors.account.NoPermissionError( @@ -355,6 +369,8 @@ class TestHitTestingApiPost: mock_ns.payload = {"query": "test query"} with app.test_request_context(): + # TODO: the service APIs are NOT migrated yet, so we have to do the very dirty hack + g._login_user = account api = HitTestingApi() with pytest.raises(Forbidden): HitTestingApi.post.__wrapped__(api, tenant_id, dataset_id) diff --git a/api/tests/unit_tests/libs/test_login.py b/api/tests/unit_tests/libs/test_login.py index 2bf22128448..8b32e448d64 100644 --- a/api/tests/unit_tests/libs/test_login.py +++ b/api/tests/unit_tests/libs/test_login.py @@ -1,15 +1,15 @@ -from types import SimpleNamespace +from typing import cast from unittest.mock import MagicMock import pytest from flask import Flask, Response, g -from flask_login import UserMixin from pytest_mock import MockerFixture +from werkzeug.exceptions import Unauthorized import libs.login as login_module from extensions.ext_login import DifyLoginManager from libs.login import current_user -from models.account import Account +from models.account import Account, Tenant @pytest.fixture @@ -23,7 +23,7 @@ def protected_view(): return _protected_view -class MockUser(UserMixin): +class MockUser: """Mock user class for testing.""" def __init__(self, id: str, is_authenticated: bool = True): @@ -35,6 +35,22 @@ class MockUser(UserMixin): return self._is_authenticated +class LoginManagerStub: + def __init__(self, unauthorized_response: Response) -> None: + self._unauthorized_response = unauthorized_response + + def unauthorized(self) -> Response: + return self._unauthorized_response + + +def _login_manager(app: Flask) -> DifyLoginManager: + return cast(DifyLoginManager, app.__dict__["login_manager"]) + + +def _unauthorized_mock(app: Flask) -> MagicMock: + return cast(MagicMock, _login_manager(app).unauthorized) + + @pytest.fixture def login_app(mocker: MockerFixture) -> Flask: app = Flask(__name__) @@ -95,7 +111,7 @@ class TestLoginRequired: assert result == "Protected content" resolve_user.assert_called_once_with() - login_app.login_manager.unauthorized.assert_not_called() + _unauthorized_mock(login_app).assert_not_called() @pytest.mark.parametrize( ("resolved_user", "description"), @@ -120,11 +136,11 @@ class TestLoginRequired: with login_app.test_request_context(): result = protected_view() - assert result is login_app.login_manager.unauthorized.return_value, description + assert result is _unauthorized_mock(login_app).return_value, description assert isinstance(result, Response) assert result.status_code == 401 resolve_user.assert_called_once_with() - login_app.login_manager.unauthorized.assert_called_once_with() + _unauthorized_mock(login_app).assert_called_once_with() csrf_check.assert_not_called() def test_unauthorized_access_propagates_response_object( @@ -138,9 +154,7 @@ class TestLoginRequired: """Test that unauthorized responses are propagated as Flask Response objects.""" resolve_user = resolve_current_user(None) response = Response("Unauthorized", status=401, content_type="application/json") - mocker.patch.object( - login_module, "_get_login_manager", return_value=SimpleNamespace(unauthorized=lambda: response) - ) + mocker.patch.object(login_module, "_get_login_manager", return_value=LoginManagerStub(response)) with login_app.test_request_context(): result = protected_view() @@ -177,7 +191,7 @@ class TestLoginRequired: assert result == "Protected content" resolve_user.assert_not_called() csrf_check.assert_not_called() - login_app.login_manager.unauthorized.assert_not_called() + _unauthorized_mock(login_app).assert_not_called() class TestGetUser: @@ -191,6 +205,7 @@ class TestGetUser: g._login_user = mock_user user = login_module._get_user() assert user == mock_user + assert user is not None assert user.id == "test_user" def test_get_user_loads_user_if_not_in_g(self, login_app: Flask, mocker: MockerFixture): @@ -201,7 +216,7 @@ class TestGetUser: g._login_user = mock_user load_user = mocker.patch.object( - login_app.login_manager, + _login_manager(login_app), "load_user_from_request_context", side_effect=load_user_from_request_context, ) @@ -244,7 +259,9 @@ class TestCurrentAccountWithTenant: def test_returns_account_and_tenant_id(self, mocker: MockerFixture): account = Account(name="Test User", email="test@example.com") - account._current_tenant = SimpleNamespace(id="tenant-123") + tenant = Tenant(name="Test Tenant") + tenant.id = "tenant-123" + account._current_tenant = tenant current_user_proxy = mocker.Mock() current_user_proxy._get_current_object.return_value = account mocker.patch.object(login_module, "current_user", new=current_user_proxy) @@ -267,3 +284,58 @@ class TestCurrentAccountWithTenant: with pytest.raises(AssertionError, match="tenant information should be loaded"): login_module.current_account_with_tenant() + + +class TestCurrentAccountWithTenantOptional: + """Test cases for optional current account resolution.""" + + def test_returns_account_and_tenant_id_for_authenticated_account(self, mocker: MockerFixture) -> None: + account = Account(name="Test User", email="test@example.com") + tenant = Tenant(name="Test Tenant") + tenant.id = "tenant-123" + account._current_tenant = tenant + mocker.patch.object(login_module, "_resolve_current_user", return_value=account) + + user, tenant_id = login_module.current_account_with_tenant_optional() + + assert user is account + assert tenant_id == "tenant-123" + + def test_returns_none_pair_when_request_loader_raises_unauthorized(self, mocker: MockerFixture) -> None: + mocker.patch.object(login_module, "_resolve_current_user", side_effect=Unauthorized()) + + user, tenant_id = login_module.current_account_with_tenant_optional() + + assert user is None + assert tenant_id is None + + def test_returns_none_pair_when_resolved_user_is_not_account(self, mocker: MockerFixture) -> None: + mocker.patch.object(login_module, "_resolve_current_user", return_value=MockUser("end-user")) + + user, tenant_id = login_module.current_account_with_tenant_optional() + + assert user is None + assert tenant_id is None + + +class TestResolveTenantIdFallback: + """Test cases for tenant-only fallback helper.""" + + def test_returns_provided_tenant_id_without_current_user_lookup(self, mocker: MockerFixture) -> None: + current_account_with_tenant = mocker.patch.object(login_module, "current_account_with_tenant") + + tenant_id = login_module.resolve_tenant_id_fallback("tenant-123") + + assert tenant_id == "tenant-123" + current_account_with_tenant.assert_not_called() + + def test_falls_back_to_current_account_tenant(self, mocker: MockerFixture) -> None: + account = Account(name="Test User", email="test@example.com") + tenant = Tenant(name="Test Tenant") + tenant.id = "tenant-123" + account._current_tenant = tenant + mocker.patch.object(login_module, "current_account_with_tenant", return_value=(account, tenant.id)) + + tenant_id = login_module.resolve_tenant_id_fallback() + + assert tenant_id == "tenant-123" diff --git a/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_customized_retrieval.py b/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_customized_retrieval.py index 647a2f0bfc9..106b959a78b 100644 --- a/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_customized_retrieval.py +++ b/api/tests/unit_tests/services/rag_pipeline/pipeline_template/test_customized_retrieval.py @@ -5,10 +5,6 @@ from services.rag_pipeline.pipeline_template.pipeline_template_type import Pipel def test_get_pipeline_templates(mocker) -> None: - mocker.patch( - "services.rag_pipeline.pipeline_template.customized.customized_retrieval.current_account_with_tenant", - return_value=("account-id", "tenant-id"), - ) customized_template = SimpleNamespace( id="tpl-1", name="Custom Template", @@ -27,7 +23,7 @@ def test_get_pipeline_templates(mocker) -> None: ) retrieval = CustomizedPipelineTemplateRetrieval() - result = retrieval.get_pipeline_templates("en-US") + result = retrieval.get_pipeline_templates("en-US", "tenant-id") assert retrieval.get_type() == PipelineTemplateType.CUSTOMIZED assert result == { diff --git a/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_service.py b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_service.py index efb79aadde2..b255595047d 100644 --- a/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_service.py +++ b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_service.py @@ -1,9 +1,14 @@ +import json import time +from datetime import datetime from types import SimpleNamespace import pytest from sqlalchemy.orm import sessionmaker +from models import Account, Tenant +from models.dataset import Dataset, Pipeline, PipelineCustomizedTemplate, PipelineRecommendedPlugin +from models.workflow import Workflow from services.entities.knowledge_entities.rag_pipeline_entities import IconInfo, PipelineTemplateInfoEntity from services.rag_pipeline.rag_pipeline import RagPipelineService @@ -25,6 +30,89 @@ class MockRepo: pass +def _make_account(account_id: str = "u1", tenant_id: str = "t1") -> Account: + account = Account(name="Test User", email=f"{account_id}@example.com") + account.id = account_id + tenant = Tenant(name="Test Tenant") + tenant.id = tenant_id + account._current_tenant = tenant + return account + + +def _make_pipeline( + *, + pipeline_id: str = "p1", + tenant_id: str = "t1", + workflow_id: str | None = None, + is_published: bool = False, +) -> Pipeline: + pipeline = Pipeline(tenant_id=tenant_id, name="Test Pipeline", description="test") + pipeline.id = pipeline_id + pipeline.workflow_id = workflow_id + pipeline.is_published = is_published + return pipeline + + +def _make_workflow( + *, + workflow_id: str = "wf-1", + tenant_id: str = "t1", + app_id: str = "p1", + graph: dict[str, object] | None = None, + features: dict[str, object] | None = None, + created_by: str = "u1", +) -> Workflow: + workflow = Workflow( + id=workflow_id, + tenant_id=tenant_id, + app_id=app_id, + type="workflow", + version="draft", + marked_name="", + marked_comment="", + graph=json.dumps(graph or {"nodes": []}), + features=json.dumps(features or {}), + created_by=created_by, + created_at=datetime(2024, 1, 1), + updated_by=None, + updated_at=datetime(2024, 1, 1), + environment_variables=[], + conversation_variables=[], + rag_pipeline_variables=[], + ) + return workflow + + +def _make_dataset(*, dataset_id: str = "d1", pipeline_id: str = "p1", tenant_id: str = "t1") -> Dataset: + dataset = Dataset( + id=dataset_id, + tenant_id=tenant_id, + name="Test Dataset", + created_by="u1", + ) + dataset.pipeline_id = pipeline_id + return dataset + + +def _make_customized_template() -> PipelineCustomizedTemplate: + return PipelineCustomizedTemplate( + tenant_id="t1", + name="old", + description="old", + chunk_structure="paragraph", + icon={}, + position=1, + yaml_content="", + install_count=0, + language="en-US", + created_by="u1", + ) + + +def _make_recommended_plugin(plugin_id: str) -> PipelineRecommendedPlugin: + return PipelineRecommendedPlugin(plugin_id=plugin_id, provider_name=plugin_id, type="tool", position=0, active=True) + + def test_get_pipeline_templates_fallbacks_to_builtin_for_non_english_empty_result(mocker) -> None: mocker.patch("services.rag_pipeline.rag_pipeline.dify_config.HOSTED_FETCH_PIPELINE_TEMPLATES_MODE", "remote") @@ -74,7 +162,7 @@ def test_get_pipeline_template_detail_uses_expected_mode(mocker, template_type: def test_get_published_workflow_returns_none_when_pipeline_has_no_workflow_id(rag_pipeline_service) -> None: - pipeline = SimpleNamespace(workflow_id=None) + pipeline = _make_pipeline(workflow_id=None) result = rag_pipeline_service.get_published_workflow(pipeline) @@ -82,7 +170,7 @@ def test_get_published_workflow_returns_none_when_pipeline_has_no_workflow_id(ra def test_get_all_published_workflow_returns_empty_for_unpublished_pipeline(rag_pipeline_service) -> None: - pipeline = SimpleNamespace(workflow_id=None) + pipeline = _make_pipeline(workflow_id=None) session = SimpleNamespace() workflows, has_more = rag_pipeline_service.get_all_published_workflow( @@ -101,7 +189,7 @@ def test_get_all_published_workflow_returns_empty_for_unpublished_pipeline(rag_p def test_get_all_published_workflow_applies_limit_and_has_more(rag_pipeline_service) -> None: scalars_result = SimpleNamespace(all=lambda: ["wf1", "wf2", "wf3"]) session = SimpleNamespace(scalars=lambda stmt: scalars_result) - pipeline = SimpleNamespace(id="pipeline-1", workflow_id="wf-live") + pipeline = _make_pipeline(pipeline_id="pipeline-1", workflow_id="wf-live") workflows, has_more = rag_pipeline_service.get_all_published_workflow( session=session, @@ -133,8 +221,8 @@ def test_sync_draft_workflow_creates_new_when_none_exists(mocker, rag_pipeline_s mocker.patch("services.rag_pipeline.rag_pipeline.db.session.flush") mocker.patch("services.rag_pipeline.rag_pipeline.db.session.commit") - pipeline = SimpleNamespace(tenant_id="t1", id="p1", workflow_id=None) - account = SimpleNamespace(id="u1") + pipeline = _make_pipeline(workflow_id=None) + account = _make_account() result = rag_pipeline_service.sync_draft_workflow( pipeline=pipeline, @@ -153,11 +241,11 @@ def test_sync_draft_workflow_creates_new_when_none_exists(mocker, rag_pipeline_s def test_sync_draft_workflow_raises_on_hash_mismatch(mocker, rag_pipeline_service) -> None: from services.errors.app import WorkflowHashNotEqualError - existing_wf = SimpleNamespace(unique_hash="hash-old") + existing_wf = _make_workflow(graph={"nodes": [{"id": "old"}]}) mocker.patch.object(rag_pipeline_service, "get_draft_workflow", return_value=existing_wf) - pipeline = SimpleNamespace(tenant_id="t1", id="p1") - account = SimpleNamespace(id="u1") + pipeline = _make_pipeline() + account = _make_account() with pytest.raises(WorkflowHashNotEqualError): rag_pipeline_service.sync_draft_workflow( @@ -184,8 +272,8 @@ def test_sync_draft_workflow_updates_existing(mocker, rag_pipeline_service) -> N mocker.patch.object(rag_pipeline_service, "get_draft_workflow", return_value=existing_wf) mocker.patch("services.rag_pipeline.rag_pipeline.db.session.commit") - pipeline = SimpleNamespace(tenant_id="t1", id="p1") - account = SimpleNamespace(id="u1") + pipeline = _make_pipeline() + account = _make_account() result = rag_pipeline_service.sync_draft_workflow( pipeline=pipeline, @@ -275,7 +363,7 @@ def test_get_rag_pipeline_paginate_workflow_runs_delegates(mocker, rag_pipeline_ repo_mock.get_paginated_workflow_runs.return_value = expected rag_pipeline_service._workflow_run_repo = repo_mock - pipeline = SimpleNamespace(tenant_id="t1", id="p1") + pipeline = _make_pipeline() result = rag_pipeline_service.get_rag_pipeline_paginate_workflow_runs(pipeline, {"limit": 10, "last_id": "abc"}) assert result is expected @@ -297,7 +385,7 @@ def test_get_rag_pipeline_workflow_run_delegates(mocker, rag_pipeline_service) - repo_mock.get_workflow_run_by_id.return_value = expected rag_pipeline_service._workflow_run_repo = repo_mock - pipeline = SimpleNamespace(tenant_id="t1", id="p1") + pipeline = _make_pipeline() result = rag_pipeline_service.get_rag_pipeline_workflow_run(pipeline, "run-1") assert result is expected @@ -310,14 +398,14 @@ def test_get_rag_pipeline_workflow_run_delegates(mocker, rag_pipeline_service) - def test_is_workflow_exist_returns_true_when_draft_exists(mocker, rag_pipeline_service) -> None: mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=1) - pipeline = SimpleNamespace(tenant_id="t1", id="p1") + pipeline = _make_pipeline() assert rag_pipeline_service.is_workflow_exist(pipeline) is True def test_is_workflow_exist_returns_false_when_no_draft(mocker, rag_pipeline_service) -> None: mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=0) - pipeline = SimpleNamespace(tenant_id="t1", id="p1") + pipeline = _make_pipeline() assert rag_pipeline_service.is_workflow_exist(pipeline) is False @@ -635,17 +723,10 @@ def test_get_second_step_parameters_success(mocker, rag_pipeline_service) -> Non def test_publish_customized_pipeline_template_success(mocker, rag_pipeline_service) -> None: - from models.dataset import Pipeline - # 1. Setup mocks - pipeline = mocker.Mock(spec=Pipeline) - pipeline.id = "p1" - pipeline.tenant_id = "t1" - pipeline.workflow_id = "wf-1" - pipeline.is_published = True + pipeline = _make_pipeline(workflow_id="wf-1", is_published=True) - workflow = mocker.Mock() - workflow.id = "wf-1" + workflow = _make_workflow(workflow_id="wf-1") # Mock db itself to avoid app context errors mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db") @@ -656,8 +737,8 @@ def test_publish_customized_pipeline_template_success(mocker, rag_pipeline_servi mock_db.session.scalar.side_effect = [None, 5] # Mock retrieve_dataset - dataset = mocker.Mock() - pipeline.retrieve_dataset.return_value = dataset + dataset = _make_dataset() + dataset.chunk_structure = "paragraph" # Mock RagPipelineDslService mock_dsl_service = mocker.Mock() @@ -665,16 +746,14 @@ def test_publish_customized_pipeline_template_success(mocker, rag_pipeline_servi mocker.patch("services.rag_pipeline.rag_pipeline_dsl_service.RagPipelineDslService", return_value=mock_dsl_service) # Mock Session and commit - mocker.patch("services.rag_pipeline.rag_pipeline.Session", return_value=mocker.MagicMock()) + session_factory = mocker.patch("services.rag_pipeline.rag_pipeline.sessionmaker") + session_factory.return_value.begin.return_value.__enter__.return_value.scalar.return_value = dataset - # Mock current_user - mock_user = mocker.Mock() - mock_user.id = "user-123" - mocker.patch("services.rag_pipeline.rag_pipeline.current_user", mock_user) + account = _make_account(account_id="user-123") # 2. Run test args = {"name": "New Template", "description": "Desc", "icon_info": {"icon": "star"}, "tags": ["tag1"]} - rag_pipeline_service.publish_customized_pipeline_template("p1", args) + rag_pipeline_service.publish_customized_pipeline_template("p1", args, account, "t1") # 3. Assertions # Verify a new template was added to session or similar? @@ -687,14 +766,10 @@ def test_publish_customized_pipeline_template_success(mocker, rag_pipeline_servi def test_get_datasource_plugins_success(mocker, rag_pipeline_service) -> None: - from models.dataset import Dataset, Pipeline - # 1. Setup mocks - dataset = mocker.Mock(spec=Dataset) - dataset.pipeline_id = "p1" + dataset = _make_dataset() - pipeline = mocker.Mock(spec=Pipeline) - pipeline.id = "p1" + pipeline = _make_pipeline() workflow = mocker.Mock() workflow.graph_dict = { @@ -835,15 +910,10 @@ def test_set_datasource_variables_success(mocker, rag_pipeline_service) -> None: def test_get_draft_workflow_success(mocker, rag_pipeline_service) -> None: - from models.dataset import Pipeline - from models.workflow import Workflow - # 1. Setup mocks - pipeline = mocker.Mock(spec=Pipeline) - pipeline.id = "p1" - pipeline.tenant_id = "t1" + pipeline = _make_pipeline() - workflow = mocker.Mock(spec=Workflow) + workflow = _make_workflow() mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db") mock_db.session.scalar.return_value = workflow @@ -856,16 +926,10 @@ def test_get_draft_workflow_success(mocker, rag_pipeline_service) -> None: def test_get_published_workflow_success(mocker, rag_pipeline_service) -> None: - from models.dataset import Pipeline - from models.workflow import Workflow - # 1. Setup mocks - pipeline = mocker.Mock(spec=Pipeline) - pipeline.id = "p1" - pipeline.tenant_id = "t1" - pipeline.workflow_id = "wf-pub" + pipeline = _make_pipeline(workflow_id="wf-pub") - workflow = mocker.Mock(spec=Workflow) + workflow = _make_workflow(workflow_id="wf-pub") mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db") mock_db.session.scalar.return_value = workflow @@ -896,8 +960,8 @@ def test_get_default_block_config_success(rag_pipeline_service) -> None: def test_publish_workflow_raises_when_draft_workflow_missing(mocker, rag_pipeline_service) -> None: session = mocker.Mock() session.scalar.return_value = None - pipeline = SimpleNamespace(id="p1", tenant_id="t1") - account = SimpleNamespace(id="u1") + pipeline = _make_pipeline() + account = _make_account() with pytest.raises(ValueError, match="No valid workflow found"): rag_pipeline_service.publish_workflow(session=session, pipeline=pipeline, account=account) @@ -929,8 +993,8 @@ def test_get_default_block_config_injects_http_request_filter(mocker, rag_pipeli def test_run_draft_workflow_node_raises_when_workflow_missing(mocker, rag_pipeline_service) -> None: - pipeline = SimpleNamespace(id="p1", tenant_id="t1") - account = SimpleNamespace(id="u1") + pipeline = _make_pipeline() + account = _make_account() mocker.patch.object(rag_pipeline_service, "get_draft_workflow", return_value=None) with pytest.raises(ValueError, match="Workflow not initialized"): @@ -939,8 +1003,8 @@ def test_run_draft_workflow_node_raises_when_workflow_missing(mocker, rag_pipeli def test_run_draft_workflow_node_saves_execution_and_variables(mocker, rag_pipeline_service) -> None: mocker.patch("services.rag_pipeline.rag_pipeline.db", mocker.Mock(engine=mocker.Mock())) - pipeline = SimpleNamespace(id="p1", tenant_id="t1") - account = SimpleNamespace(id="u1") + pipeline = _make_pipeline() + account = _make_account() draft_workflow = mocker.Mock(id="wf-1") draft_workflow.get_node_config_by_id.return_value = {"id": "node-1"} draft_workflow.get_enclosing_node_type_and_id.return_value = ("loop", "enclosing-node") @@ -1163,11 +1227,11 @@ def test_get_second_step_parameters_handles_string_and_list_variable_references( def test_get_rag_pipeline_workflow_run_node_executions_empty_when_run_missing(mocker, rag_pipeline_service) -> None: - pipeline = SimpleNamespace(id="p1", tenant_id="t1") + pipeline = _make_pipeline() mocker.patch.object(rag_pipeline_service, "get_rag_pipeline_workflow_run", return_value=None) result = rag_pipeline_service.get_rag_pipeline_workflow_run_node_executions( - pipeline=pipeline, run_id="run-1", user=SimpleNamespace(id="u1") + pipeline=pipeline, run_id="run-1", user=_make_account() ) assert result == [] @@ -1175,14 +1239,14 @@ def test_get_rag_pipeline_workflow_run_node_executions_empty_when_run_missing(mo def test_get_rag_pipeline_workflow_run_node_executions_returns_sorted_executions(mocker, rag_pipeline_service) -> None: mocker.patch("services.rag_pipeline.rag_pipeline.db", mocker.Mock(engine=mocker.Mock())) - pipeline = SimpleNamespace(id="p1", tenant_id="t1") + pipeline = _make_pipeline() mocker.patch.object(rag_pipeline_service, "get_rag_pipeline_workflow_run", return_value=SimpleNamespace(id="run-1")) repo = mocker.Mock() repo.get_db_models_by_workflow_run.return_value = ["n1", "n2"] mocker.patch("services.rag_pipeline.rag_pipeline.SQLAlchemyWorkflowNodeExecutionRepository", return_value=repo) result = rag_pipeline_service.get_rag_pipeline_workflow_run_node_executions( - pipeline=pipeline, run_id="run-1", user=SimpleNamespace(id="u1") + pipeline=pipeline, run_id="run-1", user=_make_account() ) assert result == ["n1", "n2"] @@ -1192,7 +1256,7 @@ def test_get_recommended_plugins_returns_empty_when_no_active_plugins(mocker, ra mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db") mock_db.session.scalars.return_value.all.return_value = [] - result = rag_pipeline_service.get_recommended_plugins("all") + result = rag_pipeline_service.get_recommended_plugins("all", _make_account(), "t1") assert result == { "installed_recommended_plugins": [], @@ -1201,11 +1265,10 @@ def test_get_recommended_plugins_returns_empty_when_no_active_plugins(mocker, ra def test_get_recommended_plugins_returns_installed_and_uninstalled(mocker, rag_pipeline_service) -> None: - plugin_a = SimpleNamespace(plugin_id="plugin-a") - plugin_b = SimpleNamespace(plugin_id="plugin-b") + plugin_a = _make_recommended_plugin("plugin-a") + plugin_b = _make_recommended_plugin("plugin-b") mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db") mock_db.session.scalars.return_value.all.return_value = [plugin_a, plugin_b] - mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1")) mocker.patch( "services.rag_pipeline.rag_pipeline.BuiltinToolManageService.list_builtin_tools", return_value=[SimpleNamespace(plugin_id="plugin-a", to_dict=lambda: {"plugin_id": "plugin-a"})], @@ -1215,7 +1278,7 @@ def test_get_recommended_plugins_returns_installed_and_uninstalled(mocker, rag_p return_value=[{"plugin_id": "plugin-b", "name": "Plugin B"}], ) - result = rag_pipeline_service.get_recommended_plugins("custom") + result = rag_pipeline_service.get_recommended_plugins("custom", _make_account(), "t1") assert result["installed_recommended_plugins"] == [{"plugin_id": "plugin-a"}] assert result["uninstalled_recommended_plugins"] == [{"plugin_id": "plugin-b", "name": "Plugin B"}] @@ -1229,8 +1292,8 @@ def test_get_node_last_run_delegates_to_repository(mocker, rag_pipeline_service) "services.rag_pipeline.rag_pipeline.DifyAPIRepositoryFactory.create_api_workflow_node_execution_repository", return_value=repo, ) - pipeline = SimpleNamespace(id="p1", tenant_id="t1") - workflow = SimpleNamespace(id="wf1") + pipeline = _make_pipeline() + workflow = _make_workflow(workflow_id="wf1") result = rag_pipeline_service.get_node_last_run(pipeline, workflow, "node-1") @@ -1572,15 +1635,17 @@ def test_publish_customized_pipeline_template_raises_for_missing_pipeline(mocker mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=None) with pytest.raises(ValueError, match="Pipeline not found"): - rag_pipeline_service.publish_customized_pipeline_template("p1", {}) + rag_pipeline_service.publish_customized_pipeline_template("p1", {}, _make_account(), "t1") def test_publish_customized_pipeline_template_raises_for_missing_workflow_id(mocker, rag_pipeline_service) -> None: - pipeline = SimpleNamespace(id="p1", tenant_id="t1", workflow_id=None) + pipeline = _make_pipeline(workflow_id=None) mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=pipeline) with pytest.raises(ValueError, match="Pipeline workflow not found"): - rag_pipeline_service.publish_customized_pipeline_template("p1", {"name": "template-name"}) + rag_pipeline_service.publish_customized_pipeline_template( + "p1", {"name": "template-name"}, _make_account(), "t1" + ) def test_get_pipeline_raises_when_dataset_missing(mocker, rag_pipeline_service) -> None: @@ -1630,13 +1695,12 @@ def test_get_pipeline_templates_builtin_en_us_no_fallback(mocker) -> None: def test_update_customized_pipeline_template_commits_when_name_empty(mocker) -> None: - template = SimpleNamespace(name="old", description="old", icon={}, updated_by=None) + template = _make_customized_template() mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=template) commit = mocker.patch("services.rag_pipeline.rag_pipeline.db.session.commit") - mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1")) info = PipelineTemplateInfoEntity(name="", description="updated", icon_info=IconInfo(icon="i")) - result = RagPipelineService.update_customized_pipeline_template("tpl-1", info) + result = RagPipelineService.update_customized_pipeline_template("tpl-1", info, _make_account(), "t1") assert result.description == "updated" commit.assert_called_once() @@ -1644,7 +1708,7 @@ def test_update_customized_pipeline_template_commits_when_name_empty(mocker) -> def test_get_all_published_workflow_without_filters_has_no_more(rag_pipeline_service) -> None: session = SimpleNamespace(scalars=lambda stmt: SimpleNamespace(all=lambda: ["wf1"])) - pipeline = SimpleNamespace(id="p1", workflow_id="wf-live") + pipeline = _make_pipeline(workflow_id="wf-live") workflows, has_more = rag_pipeline_service.get_all_published_workflow( session=session, @@ -1856,38 +1920,34 @@ def test_run_free_workflow_node_delegates_to_handle_result(mocker, rag_pipeline_ def test_publish_customized_pipeline_template_raises_when_workflow_missing(mocker, rag_pipeline_service) -> None: - pipeline = SimpleNamespace(id="p1", tenant_id="t1", workflow_id="wf-1") + pipeline = _make_pipeline(workflow_id="wf-1") mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", side_effect=[pipeline, None]) with pytest.raises(ValueError, match="Workflow not found"): - rag_pipeline_service.publish_customized_pipeline_template("p1", {}) + rag_pipeline_service.publish_customized_pipeline_template("p1", {}, _make_account(), "t1") def test_publish_customized_pipeline_template_raises_when_dataset_missing(mocker, rag_pipeline_service) -> None: - pipeline = SimpleNamespace(id="p1", tenant_id="t1", workflow_id="wf-1") - workflow = SimpleNamespace(id="wf-1") + pipeline = _make_pipeline(workflow_id="wf-1") + workflow = _make_workflow(workflow_id="wf-1") mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db") mock_db.engine = mocker.Mock() mock_db.session.get.side_effect = [pipeline, workflow] - session_ctx = mocker.MagicMock() - session_ctx.__enter__.return_value = SimpleNamespace() - session_ctx.__exit__.return_value = False - mocker.patch("services.rag_pipeline.rag_pipeline.Session", return_value=session_ctx) - pipeline.retrieve_dataset = lambda session: None + session_factory = mocker.patch("services.rag_pipeline.rag_pipeline.sessionmaker") + session_factory.return_value.begin.return_value.__enter__.return_value.scalar.return_value = None with pytest.raises(ValueError, match="Dataset not found"): - rag_pipeline_service.publish_customized_pipeline_template("p1", {}) + rag_pipeline_service.publish_customized_pipeline_template("p1", {}, _make_account(), "t1") def test_get_recommended_plugins_skips_manifest_when_missing(mocker, rag_pipeline_service) -> None: - plugin = SimpleNamespace(plugin_id="plugin-a") + plugin = _make_recommended_plugin("plugin-a") mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db") mock_db.session.scalars.return_value.all.return_value = [plugin] - mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1")) mocker.patch("services.rag_pipeline.rag_pipeline.BuiltinToolManageService.list_builtin_tools", return_value=[]) mocker.patch("services.rag_pipeline.rag_pipeline.marketplace.batch_fetch_plugin_by_ids", return_value=[]) - result = rag_pipeline_service.get_recommended_plugins("all") + result = rag_pipeline_service.get_recommended_plugins("all", _make_account(), "t1") assert result["installed_recommended_plugins"] == [] assert result["uninstalled_recommended_plugins"] == [] @@ -1918,8 +1978,8 @@ def test_retry_error_document_raises_when_workflow_missing(mocker, rag_pipeline_ def test_get_datasource_plugins_returns_empty_for_non_datasource_nodes(mocker, rag_pipeline_service) -> None: - dataset = SimpleNamespace(pipeline_id="p1") - pipeline = SimpleNamespace(id="p1", tenant_id="t1") + dataset = _make_dataset() + pipeline = _make_pipeline() workflow = SimpleNamespace( graph_dict={"nodes": [{"id": "n1", "data": {"type": "start"}}]}, rag_pipeline_variables=[] ) @@ -2079,8 +2139,8 @@ def test_set_datasource_variables_raises_when_workflow_missing(mocker, rag_pipel def test_get_datasource_plugins_handles_empty_datasource_data_and_non_published(mocker, rag_pipeline_service) -> None: - dataset = SimpleNamespace(pipeline_id="p1") - pipeline = SimpleNamespace(id="p1", tenant_id="t1") + dataset = _make_dataset() + pipeline = _make_pipeline() workflow = SimpleNamespace( graph_dict={"nodes": [{"id": "n1", "data": {"type": "datasource", "datasource_parameters": {}}}]}, rag_pipeline_variables=[{"variable": "v1", "belong_to_node_id": "shared"}], @@ -2097,8 +2157,8 @@ def test_get_datasource_plugins_handles_empty_datasource_data_and_non_published( def test_get_datasource_plugins_extracts_user_inputs_and_credentials(mocker, rag_pipeline_service) -> None: - dataset = SimpleNamespace(pipeline_id="p1") - pipeline = SimpleNamespace(id="p1", tenant_id="t1") + dataset = _make_dataset() + pipeline = _make_pipeline() workflow = SimpleNamespace( graph_dict={ "nodes": [ @@ -2139,8 +2199,8 @@ def test_get_datasource_plugins_extracts_user_inputs_and_credentials(mocker, rag def test_get_pipeline_returns_pipeline_when_found(mocker, rag_pipeline_service) -> None: - dataset = SimpleNamespace(pipeline_id="p1") - pipeline = SimpleNamespace(id="p1") + dataset = _make_dataset() + pipeline = _make_pipeline() mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, pipeline]) result = rag_pipeline_service.get_pipeline("t1", "d1") diff --git a/api/tests/unit_tests/services/test_metadata_bug_complete.py b/api/tests/unit_tests/services/test_metadata_bug_complete.py index fc3a2fc416d..36ea1fac1a4 100644 --- a/api/tests/unit_tests/services/test_metadata_bug_complete.py +++ b/api/tests/unit_tests/services/test_metadata_bug_complete.py @@ -1,65 +1,62 @@ from pathlib import Path -from unittest.mock import Mock, create_autospec, patch +from typing import cast +from unittest.mock import Mock import pytest -from models.account import Account +from models import Account, Tenant from services.entities.knowledge_entities.knowledge_entities import MetadataArgs from services.metadata_service import MetadataService +def _make_account(account_id: str = "user-456", tenant_id: str = "tenant-123") -> Account: + account = Account(name="Test User", email=f"{account_id}@example.com") + account.id = account_id + tenant = Tenant(name="Test Tenant") + tenant.id = tenant_id + account._current_tenant = tenant + return account + + class TestMetadataBugCompleteValidation: """Complete test suite to verify the metadata nullable bug and its fix.""" - def test_1_pydantic_layer_validation(self): + def test_1_pydantic_layer_validation(self) -> None: """Test Layer 1: Pydantic model validation correctly rejects None values.""" # Pydantic should reject None values for required fields with pytest.raises((ValueError, TypeError)): - MetadataArgs(type=None, name=None) + MetadataArgs(type=None, name=None) # pyrefly: ignore[bad-argument-type] with pytest.raises((ValueError, TypeError)): - MetadataArgs(type="string", name=None) + MetadataArgs(type="string", name=None) # pyrefly: ignore[bad-argument-type] with pytest.raises((ValueError, TypeError)): - MetadataArgs(type=None, name="test") + MetadataArgs(type=None, name="test") # pyrefly: ignore[bad-argument-type] # Valid values should work valid_args = MetadataArgs(type="string", name="test_name") assert valid_args.type == "string" assert valid_args.name == "test_name" - def test_2_business_logic_layer_crashes_on_none(self): + def test_2_business_logic_layer_crashes_on_none(self) -> None: """Test Layer 2: Business logic crashes when None values slip through.""" # Create mock that bypasses Pydantic validation mock_metadata_args = Mock() mock_metadata_args.name = None mock_metadata_args.type = "string" - mock_user = create_autospec(Account, instance=True) - mock_user.current_tenant_id = "tenant-123" - mock_user.id = "user-456" - - with patch( - "services.metadata_service.current_account_with_tenant", - return_value=(mock_user, mock_user.current_tenant_id), - ): - # Should crash with TypeError - with pytest.raises(TypeError, match="object of type 'NoneType' has no len"): - MetadataService.create_metadata("dataset-123", mock_metadata_args) + account = _make_account() + # Should crash with TypeError + with pytest.raises(TypeError, match="object of type 'NoneType' has no len"): + MetadataService.create_metadata("dataset-123", mock_metadata_args, account, "tenant-123") # Test update method as well - mock_user = create_autospec(Account, instance=True) - mock_user.current_tenant_id = "tenant-123" - mock_user.id = "user-456" + account = _make_account() + none_name = cast(str, None) + with pytest.raises(TypeError, match="object of type 'NoneType' has no len"): + MetadataService.update_metadata_name("dataset-123", "metadata-456", none_name, account, "tenant-123") - with patch( - "services.metadata_service.current_account_with_tenant", - return_value=(mock_user, mock_user.current_tenant_id), - ): - with pytest.raises(TypeError, match="object of type 'NoneType' has no len"): - MetadataService.update_metadata_name("dataset-123", "metadata-456", None) - - def test_3_database_constraints_verification(self): + def test_3_database_constraints_verification(self) -> None: """Test Layer 3: Verify database model has nullable=False constraints.""" from sqlalchemy import inspect @@ -75,7 +72,7 @@ class TestMetadataBugCompleteValidation: assert type_column.nullable is False, "type column should be nullable=False" assert name_column.nullable is False, "name column should be nullable=False" - def test_4_fixed_api_layer_rejects_null(self): + def test_4_fixed_api_layer_rejects_null(self) -> None: """Test Layer 4: Fixed API configuration properly rejects null values using Pydantic.""" with pytest.raises((ValueError, TypeError)): MetadataArgs.model_validate({"type": None, "name": None}) @@ -86,30 +83,23 @@ class TestMetadataBugCompleteValidation: with pytest.raises((ValueError, TypeError)): MetadataArgs.model_validate({"type": None, "name": "test"}) - def test_5_fixed_api_accepts_valid_values(self): + def test_5_fixed_api_accepts_valid_values(self) -> None: """Test that fixed API still accepts valid non-null values.""" args = MetadataArgs.model_validate({"type": "string", "name": "valid_name"}) assert args.type == "string" assert args.name == "valid_name" - def test_6_simulated_buggy_behavior(self): + def test_6_simulated_buggy_behavior(self) -> None: """Test simulating the original buggy behavior by bypassing Pydantic validation.""" mock_metadata_args = Mock() mock_metadata_args.name = None mock_metadata_args.type = None - mock_user = create_autospec(Account, instance=True) - mock_user.current_tenant_id = "tenant-123" - mock_user.id = "user-456" + account = _make_account() + with pytest.raises(TypeError, match="object of type 'NoneType' has no len"): + MetadataService.create_metadata("dataset-123", mock_metadata_args, account, "tenant-123") - with patch( - "services.metadata_service.current_account_with_tenant", - return_value=(mock_user, mock_user.current_tenant_id), - ): - with pytest.raises(TypeError, match="object of type 'NoneType' has no len"): - MetadataService.create_metadata("dataset-123", mock_metadata_args) - - def test_7_end_to_end_validation_layers(self): + def test_7_end_to_end_validation_layers(self) -> None: """Test all validation layers work together correctly.""" # Layer 1: API should reject null at parameter level (with fix) # Layer 2: Pydantic should reject null at model level @@ -128,7 +118,7 @@ class TestMetadataBugCompleteValidation: assert len(metadata_args.name) <= 255 # This should not crash assert len(metadata_args.type) > 0 # This should not crash - def test_8_verify_specific_fix_locations(self): + def test_8_verify_specific_fix_locations(self) -> None: """Verify that the specific locations mentioned in bug report are fixed.""" # Read the actual files to verify fixes import os @@ -152,7 +142,7 @@ class TestMetadataBugCompleteValidation: class TestMetadataValidationSummary: """Summary tests that demonstrate the complete validation architecture.""" - def test_validation_layer_architecture(self): + def test_validation_layer_architecture(self) -> None: """Document and test the 4-layer validation architecture.""" # Layer 1: API Parameter Validation (Flask-RESTful reqparse) # - Role: First line of defense, validates HTTP request parameters diff --git a/api/tests/unit_tests/services/test_metadata_nullable_bug.py b/api/tests/unit_tests/services/test_metadata_nullable_bug.py index f43f394489a..27570a86f1a 100644 --- a/api/tests/unit_tests/services/test_metadata_nullable_bug.py +++ b/api/tests/unit_tests/services/test_metadata_nullable_bug.py @@ -1,56 +1,53 @@ -from unittest.mock import Mock, create_autospec, patch +from typing import cast +from unittest.mock import Mock import pytest -from models.account import Account +from models import Account, Tenant from services.entities.knowledge_entities.knowledge_entities import MetadataArgs from services.metadata_service import MetadataService +def _make_account(account_id: str = "user-456", tenant_id: str = "tenant-123") -> Account: + account = Account(name="Test User", email=f"{account_id}@example.com") + account.id = account_id + tenant = Tenant(name="Test Tenant") + tenant.id = tenant_id + account._current_tenant = tenant + return account + + class TestMetadataNullableBug: """Test case to reproduce the metadata nullable validation bug.""" - def test_metadata_args_with_none_values_should_fail(self): + def test_metadata_args_with_none_values_should_fail(self) -> None: """Test that MetadataArgs validation should reject None values.""" # This test demonstrates the expected behavior - should fail validation with pytest.raises((ValueError, TypeError)): # This should fail because Pydantic expects non-None values - MetadataArgs(type=None, name=None) + MetadataArgs(type=None, name=None) # pyrefly: ignore[bad-argument-type] - def test_metadata_service_create_with_none_name_crashes(self): + def test_metadata_service_create_with_none_name_crashes(self) -> None: """Test that MetadataService.create_metadata crashes when name is None.""" # Mock the MetadataArgs to bypass Pydantic validation mock_metadata_args = Mock() mock_metadata_args.name = None # This will cause len() to crash mock_metadata_args.type = "string" - mock_user = create_autospec(Account, instance=True) - mock_user.current_tenant_id = "tenant-123" - mock_user.id = "user-456" + account = _make_account() + # This should crash with TypeError when calling len(None) + with pytest.raises(TypeError, match="object of type 'NoneType' has no len"): + MetadataService.create_metadata("dataset-123", mock_metadata_args, account, "tenant-123") - with patch( - "services.metadata_service.current_account_with_tenant", - return_value=(mock_user, mock_user.current_tenant_id), - ): - # This should crash with TypeError when calling len(None) - with pytest.raises(TypeError, match="object of type 'NoneType' has no len"): - MetadataService.create_metadata("dataset-123", mock_metadata_args) - - def test_metadata_service_update_with_none_name_crashes(self): + def test_metadata_service_update_with_none_name_crashes(self) -> None: """Test that MetadataService.update_metadata_name crashes when name is None.""" - mock_user = create_autospec(Account, instance=True) - mock_user.current_tenant_id = "tenant-123" - mock_user.id = "user-456" + account = _make_account() + none_name = cast(str, None) + # This should crash with TypeError when calling len(None) + with pytest.raises(TypeError, match="object of type 'NoneType' has no len"): + MetadataService.update_metadata_name("dataset-123", "metadata-456", none_name, account, "tenant-123") - with patch( - "services.metadata_service.current_account_with_tenant", - return_value=(mock_user, mock_user.current_tenant_id), - ): - # This should crash with TypeError when calling len(None) - with pytest.raises(TypeError, match="object of type 'NoneType' has no len"): - MetadataService.update_metadata_name("dataset-123", "metadata-456", None) - - def test_api_layer_now_uses_pydantic_validation(self): + def test_api_layer_now_uses_pydantic_validation(self) -> None: """Verify that API layer relies on Pydantic validation instead of reqparse.""" invalid_payload = {"type": None, "name": None} with pytest.raises((ValueError, TypeError)): From 84490179b011f7e59b701299fa90255fa6f34800 Mon Sep 17 00:00:00 2001 From: Yunlu Wen Date: Thu, 11 Jun 2026 10:39:59 +0800 Subject: [PATCH 014/122] feat: trace document retrieval (#37283) --- api/core/rag/datasource/retrieval_service.py | 36 +++++++++++++++---- api/core/rag/datasource/vdb/vector_factory.py | 7 +++- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/api/core/rag/datasource/retrieval_service.py b/api/core/rag/datasource/retrieval_service.py index 21eac29f218..85eb06045ac 100644 --- a/api/core/rag/datasource/retrieval_service.py +++ b/api/core/rag/datasource/retrieval_service.py @@ -1,10 +1,12 @@ import concurrent.futures +import functools import logging -from collections.abc import Sequence +from collections.abc import Callable, Sequence from concurrent.futures import ThreadPoolExecutor from typing import Any, NotRequired, TypedDict from flask import Flask, current_app +from opentelemetry import context as otel_context from sqlalchemy import select from sqlalchemy.orm import Session, load_only @@ -25,6 +27,7 @@ from core.rag.rerank.rerank_type import RerankMode from core.rag.retrieval.retrieval_methods import RetrievalMethod from core.tools.signature import sign_upload_file_preview_url from extensions.ext_database import db +from extensions.otel import trace_span from graphon.model_runtime.entities.model_entities import ModelType from models.dataset import ( ChildChunk, @@ -90,9 +93,24 @@ default_retrieval_model: DefaultRetrievalModelDict = { logger = logging.getLogger(__name__) +def _propagate_otel_context[**P, R](func: Callable[P, R]) -> Callable[P, R]: + captured_context = otel_context.get_current() + + @functools.wraps(func) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: + token = otel_context.attach(captured_context) + try: + return func(*args, **kwargs) + finally: + otel_context.detach(token) + + return wrapper + + class RetrievalService: # Cache precompiled regular expressions to avoid repeated compilation @classmethod + @trace_span() def retrieve( cls, retrieval_method: RetrievalMethod, @@ -122,7 +140,7 @@ class RetrievalService: if query: futures.append( executor.submit( - retrieval_service._retrieve, + _propagate_otel_context(retrieval_service._retrieve), flask_app=current_app._get_current_object(), # type: ignore retrieval_method=retrieval_method, dataset=dataset, @@ -142,7 +160,7 @@ class RetrievalService: for attachment_id in attachment_ids: futures.append( executor.submit( - retrieval_service._retrieve, + _propagate_otel_context(retrieval_service._retrieve), flask_app=current_app._get_current_object(), # type: ignore retrieval_method=retrieval_method, dataset=dataset, @@ -264,6 +282,7 @@ class RetrievalService: return session.scalar(select(Dataset).where(Dataset.id == dataset_id).limit(1)) @classmethod + @trace_span() def keyword_search( cls, flask_app: Flask, @@ -291,6 +310,7 @@ class RetrievalService: exceptions.append(str(e)) @classmethod + @trace_span() def embedding_search( cls, flask_app: Flask, @@ -392,6 +412,7 @@ class RetrievalService: exceptions.append(str(e)) @classmethod + @trace_span() def full_text_index_search( cls, flask_app: Flask, @@ -754,6 +775,7 @@ class RetrievalService: db.session.rollback() raise e + @trace_span() def _retrieve( self, flask_app: Flask, @@ -780,7 +802,7 @@ class RetrievalService: if retrieval_method == RetrievalMethod.KEYWORD_SEARCH and query: futures.append( executor.submit( - self.keyword_search, + _propagate_otel_context(self.keyword_search), flask_app=current_app._get_current_object(), # type: ignore dataset_id=dataset.id, query=query, @@ -794,7 +816,7 @@ class RetrievalService: if query: futures.append( executor.submit( - self.embedding_search, + _propagate_otel_context(self.embedding_search), flask_app=current_app._get_current_object(), # type: ignore dataset_id=dataset.id, query=query, @@ -811,7 +833,7 @@ class RetrievalService: if attachment_id: futures.append( executor.submit( - self.embedding_search, + _propagate_otel_context(self.embedding_search), flask_app=current_app._get_current_object(), # type: ignore dataset_id=dataset.id, query=attachment_id, @@ -828,7 +850,7 @@ class RetrievalService: if RetrievalMethod.is_support_fulltext_search(retrieval_method) and query: futures.append( executor.submit( - self.full_text_index_search, + _propagate_otel_context(self.full_text_index_search), flask_app=current_app._get_current_object(), # type: ignore dataset_id=dataset.id, query=query, diff --git a/api/core/rag/datasource/vdb/vector_factory.py b/api/core/rag/datasource/vdb/vector_factory.py index cd73bb9b1ac..4d65951d9a9 100644 --- a/api/core/rag/datasource/vdb/vector_factory.py +++ b/api/core/rag/datasource/vdb/vector_factory.py @@ -18,6 +18,7 @@ from core.rag.models.document import Document from extensions.ext_database import db from extensions.ext_redis import redis_client from extensions.ext_storage import storage +from extensions.otel import trace_span from graphon.model_runtime.entities.model_entities import ModelType from models.dataset import Dataset, Whitelist from models.model import UploadFile @@ -244,6 +245,10 @@ class Vector: def search_by_vector(self, query: str, **kwargs: Any) -> list[Document]: query_vector = self._embeddings.embed_query(query) + return self._search_by_vector_traced(query_vector, **kwargs) + + @trace_span() + def _search_by_vector_traced(self, query_vector: list[float], **kwargs) -> list[Document]: return self._vector_processor.search_by_vector(query_vector, **kwargs) def search_by_file(self, file_id: str, **kwargs: Any) -> list[Document]: @@ -260,7 +265,7 @@ class Vector: "file_id": file_id, } ) - return self._vector_processor.search_by_vector(multimodal_vector, **kwargs) + return self._search_by_vector_traced(multimodal_vector, **kwargs) def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]: return self._vector_processor.search_by_full_text(query, **kwargs) From 117a25b32a2b683d080d150b7b6f535bd67511a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9B=90=E7=B2=92=20Yanli?= Date: Thu, 11 Jun 2026 11:44:26 +0900 Subject: [PATCH 015/122] feat(dify-agent): sync ask-human updates (#37286) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../concepts/run-lifecycle/index.md | 13 +- dify-agent/docs/dify-agent/guide/index.md | 6 +- .../user-manual/ask-human-layer/index.md | 271 +++++++++ dify-agent/mkdocs.yml | 1 + .../dify_agent/layers/ask_human/__init__.py | 48 ++ .../dify_agent/layers/ask_human/configs.py | 136 +++++ .../src/dify_agent/layers/ask_human/layer.py | 275 +++++++++ .../src/dify_agent/layers/ask_human/schema.py | 234 ++++++++ .../src/dify_agent/protocol/__init__.py | 8 +- dify-agent/src/dify_agent/protocol/schemas.py | 121 ++-- .../dify_agent/runtime/compositor_factory.py | 7 +- .../src/dify_agent/runtime/event_sink.py | 35 +- dify-agent/src/dify_agent/runtime/runner.py | 123 ++++- .../layers/ask_human/test_configs.py | 73 +++ .../dify_agent/layers/ask_human/test_layer.py | 152 +++++ .../protocol/test_protocol_schemas.py | 92 ++- .../local/dify_agent/runtime/test_runner.py | 522 +++++++++++++++++- .../dify_agent/test_client_safe_exports.py | 2 + .../dify_agent/test_import_boundaries.py | 3 + 19 files changed, 2022 insertions(+), 100 deletions(-) create mode 100644 dify-agent/docs/dify-agent/user-manual/ask-human-layer/index.md create mode 100644 dify-agent/src/dify_agent/layers/ask_human/__init__.py create mode 100644 dify-agent/src/dify_agent/layers/ask_human/configs.py create mode 100644 dify-agent/src/dify_agent/layers/ask_human/layer.py create mode 100644 dify-agent/src/dify_agent/layers/ask_human/schema.py create mode 100644 dify-agent/tests/local/dify_agent/layers/ask_human/test_configs.py create mode 100644 dify-agent/tests/local/dify_agent/layers/ask_human/test_layer.py diff --git a/dify-agent/docs/dify-agent/concepts/run-lifecycle/index.md b/dify-agent/docs/dify-agent/concepts/run-lifecycle/index.md index dce524e5b37..bbc9bd2fc31 100644 --- a/dify-agent/docs/dify-agent/concepts/run-lifecycle/index.md +++ b/dify-agent/docs/dify-agent/concepts/run-lifecycle/index.md @@ -45,14 +45,15 @@ current `agent run` has ended; the outer `workflow run` is what should be paused The caller should handle this flow as follows: -1. Read the current `agent run` result and detect the HITL (human-in-the-loop) - requirement. +1. Read the current `agent run` result and detect `deferred_tool_call` on the + terminal `run_succeeded` event. 2. Enter workflow HITL handling and pause graphon. 3. Wait for the human input to be completed. -4. When resuming the workflow, insert the human tool response into the same Agent - session's history layer. -5. Start a second `agent run` on the same Agent node and reuse the same history - session. +4. When resuming the workflow, start a second `agent run` on the same Agent node + with the previous `session_snapshot`, matching composition, and + `deferred_tool_results` keyed by the original tool call id. +5. Keep the history layer active so Dify Agent can match the result to the + pending tool call stored in the previous run's message history. In other words, a human tool does not mean “pause this agent run until it is resumed.” It means “this agent run ended with a result that requires human diff --git a/dify-agent/docs/dify-agent/guide/index.md b/dify-agent/docs/dify-agent/guide/index.md index c3662478dbc..27ec96ab349 100644 --- a/dify-agent/docs/dify-agent/guide/index.md +++ b/dify-agent/docs/dify-agent/guide/index.md @@ -136,8 +136,10 @@ Successful runs emit `run_started`, zero or more `pydantic_ai_event`, and `run_succeeded`. Failed runs end with `run_failed`. Event envelopes retain `id`, `run_id`, `type`, `data`, and `created_at`; `data` is typed per event type, including Pydantic AI's `AgentStreamEvent` payload for `pydantic_ai_event` and a -terminal `run_succeeded.data` object containing JSON-safe `output` plus a -`CompositorSessionSnapshot` for resumption. +terminal `run_succeeded.data` object containing a `CompositorSessionSnapshot` for +resumption. A successful run has exactly one active result branch: JSON-safe +`output` for final answers, or `deferred_tool_call` when a layer such as +`dify.ask_human` ends the current agent run with an external deferred tool call. ## Examples diff --git a/dify-agent/docs/dify-agent/user-manual/ask-human-layer/index.md b/dify-agent/docs/dify-agent/user-manual/ask-human-layer/index.md new file mode 100644 index 00000000000..404819398bc --- /dev/null +++ b/dify-agent/docs/dify-agent/user-manual/ask-human-layer/index.md @@ -0,0 +1,271 @@ +# Ask human layer + +The ask human layer exposes one model-visible tool that lets an agent end the +current run with a structured request for human input. This page is for Dify +Agent clients that build `CreateRunRequest` payloads and then interpret terminal +run events. + +The layer type id is `dify.ask_human`. It does not deliver forms, choose +recipients, enforce authorization, or wait inside the agent run. It only gives +the model a safe way to ask for human input and returns that request as a +deferred tool call. + +## Layer contract + +| Property | Value | +| --- | --- | +| Type id | `dify.ask_human` | +| Common layer name | `ask_human` | +| Config DTO | `DifyAskHumanLayerConfig` | +| Model-visible tool | `ask_human` by default, configurable with `tool_name` | +| Tool kind | pydantic-ai `external` deferred tool | +| Terminal event | `run_succeeded` | +| Terminal payload branch | `run_succeeded.data.deferred_tool_call` | + +The agent run does not enter a paused status. When the model calls the ask-human +tool, the current run succeeds with a `deferred_tool_call` instead of normal +`output`. The client is responsible for turning that deferred call into its own +human-facing workflow, collecting a result, and starting another run with +`deferred_tool_results`. + +## Basic usage + +Add the ask human layer to the same composition as the prompt, history, LLM, and +optional structured-output layers: + +```python {test="skip" lint="skip"} +from agenton_collections.layers.plain import PromptLayerConfig +from agenton_collections.layers.pydantic_ai import PYDANTIC_AI_HISTORY_LAYER_TYPE_ID +from dify_agent.layers.ask_human import DIFY_ASK_HUMAN_LAYER_TYPE_ID, DifyAskHumanLayerConfig +from dify_agent.layers.dify_plugin import DifyPluginLLMLayerConfig +from dify_agent.protocol import DIFY_AGENT_HISTORY_LAYER_ID, DIFY_AGENT_MODEL_LAYER_ID +from dify_agent.protocol.schemas import CreateRunRequest, RunComposition, RunLayerSpec + + +request = CreateRunRequest( + composition=RunComposition( + layers=[ + RunLayerSpec( + name="prompt", + type="plain.prompt", + config=PromptLayerConfig( + prefix="You can ask a human only when the missing decision is required to continue.", + user="Review the deployment plan and proceed only after getting the required approval.", + ), + ), + RunLayerSpec( + name=DIFY_AGENT_HISTORY_LAYER_ID, + type=PYDANTIC_AI_HISTORY_LAYER_TYPE_ID, + ), + RunLayerSpec( + name="ask_human", + type=DIFY_ASK_HUMAN_LAYER_TYPE_ID, + config=DifyAskHumanLayerConfig( + max_fields=4, + max_actions=2, + allowed_field_types=["paragraph", "select"], + allow_file_fields=False, + ), + ), + RunLayerSpec( + name=DIFY_AGENT_MODEL_LAYER_ID, + type="dify.plugin.llm", + config=DifyPluginLLMLayerConfig( + plugin_id="langgenius/openai", + model_provider="openai", + model="gpt-5.2", + credentials={"openai_api_key": ""}, + ), + ), + ] + ) +) +``` + +Include a [history layer](../history-layer/index.md) whenever you expect to +resume after a human answer. The pending tool call is stored in pydantic-ai +message history, so the resumed run needs both the returned `session_snapshot` +and the same logical composition with the history layer still present. + +## Config fields + +`DifyAskHumanLayerConfig` controls the model-facing tool identity and guardrails. +It intentionally does not contain delivery settings. + +| Field | Type | Default | Meaning | +| --- | --- | --- | --- | +| `enabled` | `bool` | `True` | When false, the layer exposes neither the tool nor the prompt guidance. | +| `tool_name` | `str` | `"ask_human"` | Model-visible tool name. Must be a valid identifier. | +| `tool_description` | `str \| None` | default description | Optional model-visible tool description. | +| `max_fields` | `int` | `8` | Maximum number of fields the model may request. Use `0` for action-only requests. | +| `max_actions` | `int` | `4` | Maximum number of human actions the model may request. | +| `allowed_field_types` | `list["paragraph" \| "select" \| "file" \| "file-list"]` | `["paragraph", "select"]` | Field types accepted by runtime validation. | +| `allow_file_fields` | `bool` | `False` | File field types are rejected unless this is true and the type is listed in `allowed_field_types`. | +| `max_markdown_chars` | `int` | `8000` | Maximum length for the optional `markdown` body. | +| `max_question_chars` | `int` | `1000` | Maximum length for the required `question`. | +| `max_field_label_chars` | `int` | `120` | Maximum label length for each field. | +| `max_action_label_chars` | `int` | `80` | Maximum label length for each action. | + +Configured limits are also capped by server hard limits. If a config exceeds a +hard cap, request validation fails before the run can execute. + +The layer converts these limits into a prompt hint automatically. Clients do not +need to write a separate system prompt listing the limits, although they may add +business-specific guidance such as when human input is appropriate. + +## What the model can request + +When enabled, the layer exposes an external deferred tool whose argument shape is +`AskHumanToolArgs`: + +| Field | Type | Meaning | +| --- | --- | --- | +| `title` | `str \| None` | Optional short title for the human request. | +| `question` | `str` | Required question/instruction for the human. | +| `markdown` | `str \| None` | Optional longer Markdown body. Treat it as untrusted user-visible content. | +| `fields` | `list[AskHumanField]` | Optional structured fields for the human to fill. | +| `actions` | `list[AskHumanAction]` | Optional action buttons. If omitted, Dify Agent normalizes to a single primary `Submit` action. | +| `urgency` | `"normal" \| "high"` | Hint for downstream systems; it is not a delivery policy. | + +Supported field variants: + +- `paragraph`: free-text input. +- `select`: single-choice input with unique option values. +- `file`: single-file input, only when file fields are allowed. +- `file-list`: multi-file input, only when file fields are allowed. + +Tool arguments are validated again after the model calls the tool. Invalid calls +produce a model retry before a terminal success is emitted. + +## Handling a deferred human request + +Stream or poll run events as usual. A successful final answer has +`event.data.output`. A successful human request has `event.data.deferred_tool_call`. +Exactly one branch is set. + +```python {test="skip" lint="skip"} +deferred_call = None +snapshot = None + +async for event in client.stream_events(run_id): + if event.type != "run_succeeded": + continue + snapshot = event.data.session_snapshot + if event.data.deferred_tool_call is not None: + deferred_call = event.data.deferred_tool_call + else: + final_output = event.data.output + break + +if deferred_call is not None: + # Render your own human-facing form, enqueue notification, pause an outer + # workflow, or store the request for later. Dify Agent does not do that part. + print(deferred_call.tool_call_id, deferred_call.args) +``` + +A typical deferred payload looks like this: + +```json +{ + "tool_call_id": "call_01H...", + "tool_name": "ask_human", + "args": { + "title": "Deployment approval", + "question": "Can we deploy version 2026.06.10 to production now?", + "fields": [ + { + "type": "paragraph", + "name": "comment", + "label": "Approval comment", + "required": false + } + ], + "actions": [ + {"id": "approve", "label": "Approve", "style": "primary"}, + {"id": "reject", "label": "Reject", "style": "destructive"} + ], + "urgency": "normal" + }, + "metadata": { + "layer_type": "dify.ask_human", + "tool_name": "ask_human", + "schema_version": 1 + } +} +``` + +The `args` object is model-generated content. Validate and sanitize it before +rendering it to end users. + +## Resume with a human result + +After your client collects a human answer, create a new run with: + +- the previous `session_snapshot`; +- a matching composition that still includes the history and ask-human layers; +- `deferred_tool_results.calls[tool_call_id]` containing the human result. + +```python {test="skip" lint="skip"} +from dify_agent.layers.ask_human import AskHumanToolResult +from dify_agent.protocol import DeferredToolResultsPayload + + +human_result = AskHumanToolResult( + status="submitted", + action={"id": "approve", "label": "Approve"}, + values={"comment": "Approved for the planned window."}, + message="The human approved the deployment.", +) + +resume_request = CreateRunRequest( + composition=composition_with_same_layer_names_and_order, + session_snapshot=snapshot, + deferred_tool_results=DeferredToolResultsPayload( + calls={deferred_call.tool_call_id: human_result.model_dump(mode="json")}, + ), +) +``` + +Dify Agent passes the supplied result back to pydantic-ai as the return value of +the original external tool call, then the model continues. The resumed run may +produce a final `output`, or it may produce another `deferred_tool_call` if the +agent needs another human turn. + +Timeouts and unavailable humans should also be sent as tool results instead of +being treated as agent-run failures: + +```json +{ + "status": "timeout", + "action": {"id": "__timeout", "label": "Timeout"}, + "values": {}, + "message": "The human did not respond before the workflow timeout." +} +``` + +## Client responsibilities + +The ask human layer deliberately leaves product decisions to the caller. Clients +must decide how to: + +- persist the deferred call and correlate it with a human-facing task; +- render and sanitize the requested fields/actions; +- choose recipients, channels, and timeout policy; +- authorize who may answer; +- transform the human submission into `AskHumanToolResult`; +- resume with the returned `session_snapshot` and matching composition. + +Do not put recipient emails, workspace member ids, public URLs, auth tokens, or +timeout policy in the tool arguments. The model-facing request is untrusted and +should not control delivery or authorization. + +## Troubleshooting + +| Symptom | What to check | +| --- | --- | +| Run fails with `Deferred tool results require a 'history' layer` | Add the `history` layer and resume with the prior snapshot. | +| Run fails with `pending tool call can be resumed` | Keep the history layer active for the initial deferred run. | +| Run fails with `exactly one deferred call` | The MVP supports one ask-human call per run. Ask the model to ask one question at a time. | +| Run fails with `tool name must be ...` | Use the configured `tool_name`; do not rename it only in downstream form code. | +| File fields are rejected | Set `allow_file_fields=True` and include `file` or `file-list` in `allowed_field_types`. | +| `run_succeeded.data.output` is absent | Check `run_succeeded.data.deferred_tool_call`; this is a human-request success, not a failed run. | diff --git a/dify-agent/mkdocs.yml b/dify-agent/mkdocs.yml index 579cffe536a..3993b3618e5 100644 --- a/dify-agent/mkdocs.yml +++ b/dify-agent/mkdocs.yml @@ -21,6 +21,7 @@ nav: - Prompt Layer: dify-agent/user-manual/prompt-layer/index.md - Execution Context Layer: dify-agent/user-manual/execution-context-layer/index.md - Shell Layer: dify-agent/user-manual/shell-layer/index.md + - Ask Human Layer: dify-agent/user-manual/ask-human-layer/index.md - Plugin LLM Layer: dify-agent/user-manual/plugin-llm-layer/index.md - Plugin Tool Layer: dify-agent/user-manual/plugin-tool-layer/index.md - History Layer: dify-agent/user-manual/history-layer/index.md diff --git a/dify-agent/src/dify_agent/layers/ask_human/__init__.py b/dify-agent/src/dify_agent/layers/ask_human/__init__.py new file mode 100644 index 00000000000..0138d7daf62 --- /dev/null +++ b/dify-agent/src/dify_agent/layers/ask_human/__init__.py @@ -0,0 +1,48 @@ +"""Client-safe exports for Dify ask-human layer DTOs and schema types. + +The runtime layer implementation lives in ``layer.py`` and imports server-side + execution helpers. Keep this package root import-safe for client code that only + needs to build run requests or understand deferred payload shapes. +""" + +from dify_agent.layers.ask_human.configs import ( + DEFAULT_ASK_HUMAN_TOOL_DESCRIPTION, + DIFY_ASK_HUMAN_LAYER_TYPE_ID, + DifyAskHumanLayerConfig, +) +from dify_agent.layers.ask_human.schema import ( + AskHumanAction, + AskHumanActionStyle, + AskHumanField, + AskHumanFieldType, + AskHumanFileField, + AskHumanFileListField, + AskHumanParagraphField, + AskHumanResultStatus, + AskHumanSelectField, + AskHumanSelectOption, + AskHumanSelectedAction, + AskHumanToolArgs, + AskHumanToolResult, + AskHumanUrgency, +) + +__all__ = [ + "AskHumanAction", + "AskHumanActionStyle", + "AskHumanField", + "AskHumanFieldType", + "AskHumanFileField", + "AskHumanFileListField", + "AskHumanParagraphField", + "AskHumanResultStatus", + "AskHumanSelectField", + "AskHumanSelectOption", + "AskHumanSelectedAction", + "AskHumanToolArgs", + "AskHumanToolResult", + "AskHumanUrgency", + "DEFAULT_ASK_HUMAN_TOOL_DESCRIPTION", + "DIFY_ASK_HUMAN_LAYER_TYPE_ID", + "DifyAskHumanLayerConfig", +] diff --git a/dify-agent/src/dify_agent/layers/ask_human/configs.py b/dify-agent/src/dify_agent/layers/ask_human/configs.py new file mode 100644 index 00000000000..432d0aad56c --- /dev/null +++ b/dify-agent/src/dify_agent/layers/ask_human/configs.py @@ -0,0 +1,136 @@ +"""Client-safe DTOs for the Dify ask-human layer. + +The public config controls only stable model-facing tool identity and guardrails. +Delivery, recipient selection, timeout policy, and other operational behavior are +intentionally out of scope for this layer and must stay outside the model-facing +tool contract. Setting ``enabled=False`` disables both ask-human tool exposure +and the prompt guidance that tells the model about these limits. Caller-provided +limits are additionally capped by small server hard limits so one composition +cannot widen the public deferred-tool surface arbitrarily. File field variants +are part of the schema vocabulary for forward compatibility, but they remain +invalid unless ``allow_file_fields=True`` and the allowed field-type list also +permits them. +""" + +from __future__ import annotations + +import re +from typing import ClassVar, Final + +from pydantic import ConfigDict, Field, field_validator, model_validator + +from agenton.layers import LayerConfig +from dify_agent.layers.ask_human.schema import AskHumanFieldType + + +DIFY_ASK_HUMAN_LAYER_TYPE_ID: Final[str] = "dify.ask_human" +DEFAULT_ASK_HUMAN_TOOL_DESCRIPTION: Final[str] = ( + "Ask a human for missing information or a decision that is required to continue. " + "Use this only when the answer cannot be inferred from the conversation, available tools, or current context. " + "Provide concise instructions, structured fields, and clear actions for the human." +) + +_TOOL_NAME_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") +_HARD_MAX_FIELDS = 16 +_HARD_MAX_ACTIONS = 8 +_HARD_MAX_MARKDOWN_CHARS = 20_000 +_HARD_MAX_QUESTION_CHARS = 4_000 +_HARD_MAX_FIELD_LABEL_CHARS = 200 +_HARD_MAX_ACTION_LABEL_CHARS = 120 +_FILE_FIELD_TYPES: Final[frozenset[AskHumanFieldType]] = frozenset({"file", "file-list"}) + + +class DifyAskHumanLayerConfig(LayerConfig): + """Public config for the optional ask-human deferred tool layer. + + This DTO describes the exact model-facing guardrail surface that the runtime + will both validate and surface back to the model through prompt guidance. + ``enabled=False`` means callers keep the layer in composition data without + exposing either the tool or its instructions for that run. Numeric limits are + caller-configurable only within the server's hard caps, and file field types + are rejected unless callers opt in with ``allow_file_fields=True``. + """ + + model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid") + + enabled: bool = True + tool_name: str = "ask_human" + tool_description: str | None = None + max_fields: int = Field(default=8, ge=0) + max_actions: int = Field(default=4, ge=1) + allowed_field_types: list[AskHumanFieldType] = Field(default_factory=lambda: ["paragraph", "select"]) + allow_file_fields: bool = False + max_markdown_chars: int = Field(default=8_000, ge=0) + max_question_chars: int = Field(default=1_000, ge=1) + max_field_label_chars: int = Field(default=120, ge=1) + max_action_label_chars: int = Field(default=80, ge=1) + + @property + def effective_tool_description(self) -> str: + """Return the configured description or the proposal default text.""" + return self.tool_description or DEFAULT_ASK_HUMAN_TOOL_DESCRIPTION + + @field_validator("tool_name") + @classmethod + def _validate_tool_name(cls, value: str) -> str: + if not _TOOL_NAME_PATTERN.fullmatch(value): + raise ValueError("tool_name must be a valid tool identifier") + return value + + @field_validator("tool_description") + @classmethod + def _normalize_tool_description(cls, value: str | None) -> str | None: + if value is None: + return None + stripped = value.strip() + return stripped or None + + @field_validator("allowed_field_types") + @classmethod + def _validate_allowed_field_types(cls, value: list[AskHumanFieldType]) -> list[AskHumanFieldType]: + if len(set(value)) != len(value): + raise ValueError("allowed_field_types must not contain duplicates") + return value + + @field_validator( + "max_fields", + "max_actions", + "max_markdown_chars", + "max_question_chars", + "max_field_label_chars", + "max_action_label_chars", + mode="after", + ) + @classmethod + def _validate_hard_limits(cls, value: int, info: object) -> int: + field_name = getattr(info, "field_name", "value") + hard_limits = { + "max_fields": _HARD_MAX_FIELDS, + "max_actions": _HARD_MAX_ACTIONS, + "max_markdown_chars": _HARD_MAX_MARKDOWN_CHARS, + "max_question_chars": _HARD_MAX_QUESTION_CHARS, + "max_field_label_chars": _HARD_MAX_FIELD_LABEL_CHARS, + "max_action_label_chars": _HARD_MAX_ACTION_LABEL_CHARS, + } + hard_limit = hard_limits[field_name] + if value > hard_limit: + raise ValueError(f"{field_name} must be <= {hard_limit}") + return value + + @model_validator(mode="after") + def _validate_file_field_policy(self) -> DifyAskHumanLayerConfig: + if not self.allow_file_fields: + forbidden = [field_type for field_type in self.allowed_field_types if field_type in _FILE_FIELD_TYPES] + if forbidden: + joined = ", ".join(forbidden) + raise ValueError( + f"allowed_field_types cannot include file field types when allow_file_fields is false: {joined}" + ) + return self + + +__all__ = [ + "DEFAULT_ASK_HUMAN_TOOL_DESCRIPTION", + "DIFY_ASK_HUMAN_LAYER_TYPE_ID", + "DifyAskHumanLayerConfig", +] diff --git a/dify-agent/src/dify_agent/layers/ask_human/layer.py b/dify-agent/src/dify_agent/layers/ask_human/layer.py new file mode 100644 index 00000000000..a94de9af72e --- /dev/null +++ b/dify-agent/src/dify_agent/layers/ask_human/layer.py @@ -0,0 +1,275 @@ +"""Runtime ask-human layer built on pydantic-ai external deferred tools. + +The layer contributes one optional external tool plus one prompt hint. The tool +never executes Python during the initial run; instead the model emits an +external deferred tool call that Dify Agent returns through ``run_succeeded`` as +``deferred_tool_call``. Guardrails are enforced in two places: + +* prompt/tool-definition guidance nudges the model toward valid requests, and +* runtime validation normalizes default actions and rejects out-of-policy calls. + +The layer stays product-neutral: downstream systems decide delivery, recipients, +timeouts, and authorization for the human request. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, ClassVar, cast + +from pydantic import JsonValue, ValidationError +from pydantic_ai import Tool +from pydantic_ai.exceptions import ModelRetry +from pydantic_ai.tools import DeferredToolRequests, RunContext, ToolDefinition +from typing_extensions import Self, override + +from agenton.layers import EmptyRuntimeState, NoLayerDeps, PydanticAILayer, PydanticAIPrompt, PydanticAITool +from dify_agent.layers.ask_human.configs import DIFY_ASK_HUMAN_LAYER_TYPE_ID, DifyAskHumanLayerConfig +from dify_agent.layers.ask_human.schema import ( + AskHumanAction, + AskHumanField, + AskHumanToolArgs, +) +from dify_agent.protocol.schemas import DeferredToolCallPayload, RunComposition + + +_ASK_HUMAN_DEFERRED_SCHEMA_VERSION = 1 +_DEFAULT_SUBMIT_ACTION = AskHumanAction(id="submit", label="Submit", style="primary") + + +@dataclass(slots=True) +class DifyAskHumanLayer(PydanticAILayer[NoLayerDeps, object, DifyAskHumanLayerConfig, EmptyRuntimeState]): + """State-free pydantic-ai layer that exposes the ask-human deferred tool.""" + + type_id: ClassVar[str | None] = DIFY_ASK_HUMAN_LAYER_TYPE_ID + + config: DifyAskHumanLayerConfig + + @classmethod + @override + def from_config(cls, config: DifyAskHumanLayerConfig) -> Self: + """Create the layer from validated public config.""" + return cls(config=DifyAskHumanLayerConfig.model_validate(config)) + + @property + @override + def prefix_prompts(self) -> list[PydanticAIPrompt[object]]: + if not self.config.enabled: + return [] + return [self.build_prompt_hint] + + @property + @override + def tools(self) -> list[PydanticAITool[object]]: + if not self.config.enabled: + return [] + return [ + Tool( + self._never_executed_tool, + takes_ctx=True, + name=self.config.tool_name, + description=self.config.effective_tool_description, + prepare=self._prepare_tool_definition, + args_validator=self._validate_tool_args, + sequential=True, + ) + ] + + def build_prompt_hint(self) -> str: + """Return the model-facing instruction text for ask-human guardrails.""" + allowed_field_types = ", ".join(self.config.allowed_field_types) if self.config.allowed_field_types else "none" + file_field_status = "enabled" if self.config.allow_file_fields else "disabled" + if self.config.max_fields == 0: + field_count_hint = "Do not add any fields." + else: + field_count_hint = f"Use at most {self.config.max_fields} field(s)." + return ( + f"You may call the external tool '{self.config.tool_name}' only when human input is required to continue. " + "Do not ask a human for information that can be inferred from the conversation, current context, or other tools.\n\n" + f"Ask-human guardrails:\n" + f"- Allowed field types: {allowed_field_types}.\n" + f"- File upload fields are {file_field_status}.\n" + f"- {field_count_hint}\n" + f"- Use at most {self.config.max_actions} action(s).\n" + f"- Keep 'question' under {self.config.max_question_chars} characters.\n" + f"- Keep 'markdown' under {self.config.max_markdown_chars} characters.\n" + f"- Keep each field label under {self.config.max_field_label_chars} characters.\n" + f"- Keep each action label under {self.config.max_action_label_chars} characters.\n" + "- If you omit actions, the system will add one primary action: Submit.\n" + "Prefer concise, structured requests that stay comfortably within these limits." + ) + + def build_deferred_tool_call_payload(self, requests: DeferredToolRequests) -> DeferredToolCallPayload: + """Validate and normalize the single supported deferred ask-human call.""" + if requests.approvals: + raise ValueError("ask_human does not support approval requests; use external deferred calls only") + + call_count = len(requests.calls) + if call_count != 1: + raise ValueError(f"ask_human supports exactly one deferred call per run in this version; got {call_count}.") + + call = requests.calls[0] + if call.tool_name != self.config.tool_name: + raise ValueError(f"ask_human deferred tool name must be '{self.config.tool_name}', got '{call.tool_name}'.") + + args = self._validate_and_normalize_tool_args( + title=None, + question="", + markdown=None, + fields=[], + actions=[], + urgency="normal", + raw_args=call.args, + ) + return DeferredToolCallPayload( + tool_call_id=call.tool_call_id, + tool_name=call.tool_name, + args=cast(JsonValue, args.model_dump(mode="json")), + metadata={ + "layer_type": self.type_id, + "tool_name": self.config.tool_name, + "schema_version": _ASK_HUMAN_DEFERRED_SCHEMA_VERSION, + }, + ) + + def _prepare_tool_definition(self, _ctx: RunContext[object], tool_def: ToolDefinition) -> ToolDefinition: + """Convert the ask-human tool into a pydantic-ai external deferred tool.""" + del tool_def + return ToolDefinition( + name=self.config.tool_name, + description=self.config.effective_tool_description, + parameters_json_schema=cast(dict[str, Any], AskHumanToolArgs.model_json_schema()), + strict=False, + sequential=True, + kind="external", + ) + + async def _never_executed_tool( + self, + _ctx: RunContext[object], + *, + title: str | None = None, + question: str, + markdown: str | None = None, + fields: list[AskHumanField] | None = None, + actions: list[AskHumanAction] | None = None, + urgency: str = "normal", + ) -> str: + del title, question, markdown, fields, actions, urgency + raise RuntimeError("ask_human is an external deferred tool and should not execute during the initial run") + + def _validate_tool_args( + self, + _ctx: RunContext[object], + *, + title: str | None = None, + question: str, + markdown: str | None = None, + fields: list[AskHumanField] | None = None, + actions: list[AskHumanAction] | None = None, + urgency: str = "normal", + ) -> None: + try: + _ = self._validate_and_normalize_tool_args( + title=title, + question=question, + markdown=markdown, + fields=fields or [], + actions=actions or [], + urgency=urgency, + ) + except (ValidationError, ValueError) as exc: + raise ModelRetry(str(exc)) from exc + + def _validate_and_normalize_tool_args( + self, + *, + title: str | None, + question: str, + markdown: str | None, + fields: list[AskHumanField], + actions: list[AskHumanAction], + urgency: str, + raw_args: str | dict[str, Any] | None = None, + ) -> AskHumanToolArgs: + if raw_args is not None: + args = _validate_tool_args_payload(raw_args) + else: + args = AskHumanToolArgs( + title=title, + question=question, + markdown=markdown, + fields=fields, + actions=actions, + urgency=cast(Any, urgency), + ) + + if len(args.fields) > self.config.max_fields: + raise ValueError(f"ask_human fields must contain at most {self.config.max_fields} item(s)") + + normalized_actions = list(args.actions) + if not normalized_actions: + normalized_actions = [_DEFAULT_SUBMIT_ACTION.model_copy()] + if len(normalized_actions) > self.config.max_actions: + raise ValueError(f"ask_human actions must contain at most {self.config.max_actions} item(s)") + + if len(args.question) > self.config.max_question_chars: + raise ValueError(f"ask_human question must be <= {self.config.max_question_chars} characters") + if args.markdown is not None and len(args.markdown) > self.config.max_markdown_chars: + raise ValueError(f"ask_human markdown must be <= {self.config.max_markdown_chars} characters") + + allowed_field_types = set(self.config.allowed_field_types) + for field in args.fields: + if field.type not in allowed_field_types: + raise ValueError(f"ask_human field type '{field.type}' is not allowed by this layer config") + if len(field.label) > self.config.max_field_label_chars: + raise ValueError( + f"ask_human field label '{field.label}' must be <= {self.config.max_field_label_chars} characters" + ) + if not self.config.allow_file_fields and field.type in {"file", "file-list"}: + raise ValueError("ask_human file fields are disabled by this layer config") + + for action in normalized_actions: + if len(action.label) > self.config.max_action_label_chars: + raise ValueError( + f"ask_human action label '{action.label}' must be <= {self.config.max_action_label_chars} characters" + ) + + return args.model_copy(update={"actions": normalized_actions}) + + +def validate_ask_human_layer_composition(composition: RunComposition) -> None: + """Reject unsupported public ask-human layer graph shapes.""" + ask_human_layers = [layer.name for layer in composition.layers if layer.type == DIFY_ASK_HUMAN_LAYER_TYPE_ID] + if len(ask_human_layers) > 1: + names = ", ".join(ask_human_layers) + raise ValueError(f"Only one '{DIFY_ASK_HUMAN_LAYER_TYPE_ID}' layer is supported. Found layers: {names}.") + + +def get_ask_human_layer(run: Any) -> DifyAskHumanLayer | None: + """Return the active ask-human layer when one is present and enabled.""" + matched: list[DifyAskHumanLayer] = [] + for slot in run.slots.values(): + layer = slot.layer + if isinstance(layer, DifyAskHumanLayer): + matched.append(layer) + if not matched: + return None + if len(matched) > 1: + raise ValueError(f"Only one '{DIFY_ASK_HUMAN_LAYER_TYPE_ID}' layer is supported per run.") + + layer = matched[0] + return layer if layer.config.enabled else None + + +def _validate_tool_args_payload(raw_args: str | dict[str, Any]) -> AskHumanToolArgs: + if isinstance(raw_args, str): + return AskHumanToolArgs.model_validate_json(raw_args or "{}") + return AskHumanToolArgs.model_validate(raw_args or {}) + + +__all__ = [ + "DifyAskHumanLayer", + "get_ask_human_layer", + "validate_ask_human_layer_composition", +] diff --git a/dify-agent/src/dify_agent/layers/ask_human/schema.py b/dify-agent/src/dify_agent/layers/ask_human/schema.py new file mode 100644 index 00000000000..59fb9d3fc70 --- /dev/null +++ b/dify-agent/src/dify_agent/layers/ask_human/schema.py @@ -0,0 +1,234 @@ +"""Product-neutral schemas for the Dify ask-human deferred tool contract. + +These models describe the model-facing tool arguments and the later human result +payload expected by resumed runs. Config-specific guardrails such as maximum +counts, allowed field types, or per-install length limits are enforced by +``dify_agent.layers.ask_human.layer`` so this module stays import-safe for +client code that only needs the stable wire/schema shapes. +""" + +from __future__ import annotations + +import re +from typing import Annotated, ClassVar, Literal + +from pydantic import BaseModel, ConfigDict, Field, JsonValue, ValidationInfo, field_validator, model_validator + + +_IDENTIFIER_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") + +type AskHumanFieldType = Literal["paragraph", "select", "file", "file-list"] +type AskHumanActionStyle = Literal["default", "primary", "destructive"] +type AskHumanUrgency = Literal["normal", "high"] +type AskHumanResultStatus = Literal["submitted", "timeout", "cancelled", "unavailable"] + + +def is_valid_identifier(value: str) -> bool: + """Return whether ``value`` matches the stable ask-human identifier rules.""" + return bool(_IDENTIFIER_PATTERN.fullmatch(value)) + + +def _require_non_blank(value: str, *, label: str) -> str: + if not value.strip(): + raise ValueError(f"{label} must not be blank") + return value + + +class AskHumanSelectOption(BaseModel): + """One selectable option for an ask-human select field.""" + + model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid") + + value: str = Field(min_length=1) + label: str = Field(min_length=1) + + @field_validator("value", "label") + @classmethod + def _validate_non_blank(cls, value: str, info: ValidationInfo) -> str: + return _require_non_blank(value, label=f"select option {info.field_name}") + + +class AskHumanFieldBase(BaseModel): + """Shared field properties for ask-human form fields.""" + + model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid") + + name: str = Field(min_length=1) + label: str = Field(min_length=1) + required: bool = False + + @field_validator("name") + @classmethod + def _validate_name(cls, value: str) -> str: + if not is_valid_identifier(value): + raise ValueError("field name must be a valid identifier") + return value + + @field_validator("label") + @classmethod + def _validate_label(cls, value: str) -> str: + return _require_non_blank(value, label="field label") + + +class AskHumanParagraphField(AskHumanFieldBase): + """Free-text paragraph field.""" + + type: Literal["paragraph"] = "paragraph" + placeholder: str | None = None + default: str | None = None + + +class AskHumanSelectField(AskHumanFieldBase): + """Single-choice select field.""" + + type: Literal["select"] = "select" + options: list[AskHumanSelectOption] = Field(default_factory=list) + default: str | None = None + + @model_validator(mode="after") + def _validate_options(self) -> AskHumanSelectField: + if not self.options: + raise ValueError("select fields must define at least one option") + + seen_values: set[str] = set() + for option in self.options: + if option.value in seen_values: + raise ValueError(f"select field '{self.name}' contains duplicate option value '{option.value}'") + seen_values.add(option.value) + + if self.default is not None and self.default not in seen_values: + raise ValueError(f"select field '{self.name}' default must match one of its option values") + return self + + +class AskHumanFileField(AskHumanFieldBase): + """Single-file upload field.""" + + type: Literal["file"] = "file" + + +class AskHumanFileListField(AskHumanFieldBase): + """Multi-file upload field.""" + + type: Literal["file-list"] = "file-list" + max_files: int | None = Field(default=None, ge=1) + + +type AskHumanField = Annotated[ + AskHumanParagraphField | AskHumanSelectField | AskHumanFileField | AskHumanFileListField, + Field(discriminator="type"), +] + + +class AskHumanAction(BaseModel): + """One human-visible action rendered with an ask-human request.""" + + model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid") + + id: str = Field(min_length=1) + label: str = Field(min_length=1) + style: AskHumanActionStyle = "default" + + @field_validator("id") + @classmethod + def _validate_id(cls, value: str) -> str: + if not is_valid_identifier(value): + raise ValueError("action id must be a valid identifier") + return value + + @field_validator("label") + @classmethod + def _validate_label(cls, value: str) -> str: + return _require_non_blank(value, label="action label") + + +class AskHumanSelectedAction(BaseModel): + """Action metadata returned with a human-submitted result.""" + + model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid") + + id: str = Field(min_length=1) + label: str = Field(min_length=1) + + @field_validator("id") + @classmethod + def _validate_id(cls, value: str) -> str: + if not is_valid_identifier(value): + raise ValueError("selected action id must be a valid identifier") + return value + + @field_validator("label") + @classmethod + def _validate_label(cls, value: str) -> str: + return _require_non_blank(value, label="selected action label") + + +class AskHumanToolArgs(BaseModel): + """Arguments accepted by the ask-human external deferred tool.""" + + model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid") + + title: str | None = None + question: str = Field(min_length=1) + markdown: str | None = None + fields: list[AskHumanField] = Field(default_factory=list) + actions: list[AskHumanAction] = Field(default_factory=list) + urgency: AskHumanUrgency = "normal" + + @field_validator("question") + @classmethod + def _validate_question(cls, value: str) -> str: + return _require_non_blank(value, label="question") + + @field_validator("title") + @classmethod + def _validate_title(cls, value: str | None) -> str | None: + if value is None: + return None + return _require_non_blank(value, label="title") + + @model_validator(mode="after") + def _validate_unique_ids(self) -> AskHumanToolArgs: + field_names: set[str] = set() + for field in self.fields: + if field.name in field_names: + raise ValueError(f"field name '{field.name}' must be unique") + field_names.add(field.name) + + action_ids: set[str] = set() + for action in self.actions: + if action.id in action_ids: + raise ValueError(f"action id '{action.id}' must be unique") + action_ids.add(action.id) + return self + + +class AskHumanToolResult(BaseModel): + """Expected value shape for a later deferred ask-human tool result.""" + + model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid") + + status: AskHumanResultStatus + action: AskHumanSelectedAction | None = None + values: dict[str, JsonValue] = Field(default_factory=dict) + message: str | None = None + rendered_content: str | None = None + + +__all__ = [ + "AskHumanAction", + "AskHumanActionStyle", + "AskHumanField", + "AskHumanFieldType", + "AskHumanFileField", + "AskHumanFileListField", + "AskHumanParagraphField", + "AskHumanResultStatus", + "AskHumanSelectField", + "AskHumanSelectOption", + "AskHumanSelectedAction", + "AskHumanToolArgs", + "AskHumanToolResult", + "AskHumanUrgency", + "is_valid_identifier", +] diff --git a/dify-agent/src/dify_agent/protocol/__init__.py b/dify-agent/src/dify_agent/protocol/__init__.py index c31800e3bf9..e179d96793b 100644 --- a/dify-agent/src/dify_agent/protocol/__init__.py +++ b/dify-agent/src/dify_agent/protocol/__init__.py @@ -14,6 +14,8 @@ from .schemas import ( CancelRunResponse, CreateRunRequest, CreateRunResponse, + DeferredToolCallPayload, + DeferredToolResultsPayload, EmptyRunEventData, LayerExitSignals, PydanticAIStreamRunEvent, @@ -25,8 +27,6 @@ from .schemas import ( RunEventsResponse, RunFailedEvent, RunFailedEventData, - RunPausedEvent, - RunPausedEventData, RunPurpose, RunLayerSpec, RunStartedEvent, @@ -44,6 +44,8 @@ __all__ = [ "CancelRunResponse", "CreateRunRequest", "CreateRunResponse", + "DeferredToolCallPayload", + "DeferredToolResultsPayload", "DIFY_AGENT_HISTORY_LAYER_ID", "DIFY_AGENT_MODEL_LAYER_ID", "DIFY_AGENT_OUTPUT_LAYER_ID", @@ -59,8 +61,6 @@ __all__ = [ "RunEventsResponse", "RunFailedEvent", "RunFailedEventData", - "RunPausedEvent", - "RunPausedEventData", "RunPurpose", "RunLayerSpec", "RunStartedEvent", diff --git a/dify-agent/src/dify_agent/protocol/schemas.py b/dify-agent/src/dify_agent/protocol/schemas.py index 9a989976c71..77501942666 100644 --- a/dify-agent/src/dify_agent/protocol/schemas.py +++ b/dify-agent/src/dify_agent/protocol/schemas.py @@ -5,9 +5,9 @@ producers, storage adapters, and Python client. Create-run requests expose a Dify-friendly ``composition.layers[].config`` shape so callers can describe one layer in one place; the server normalizes that public DTO into Agenton's state-only ``CompositorConfig`` plus node-name keyed per-run configs before -calling ``Compositor.enter(configs=...)``. Session snapshots and ``on_exit`` stay -top-level because they are per-run resume state and exit policy, not graph node -definition. +calling ``Compositor.enter(configs=...)``. Session snapshots, deferred tool +results, and ``on_exit`` stay top-level because they are per-run resume state or +exit policy, not graph node definition. The server still constructs layers only from explicit provider type ids, keeping HTTP input data-only and preventing unsafe import-path construction. Run events @@ -22,21 +22,25 @@ by ``DIFY_AGENT_MODEL_LAYER_ID``, the optional history layer named by by ``DIFY_AGENT_OUTPUT_LAYER_ID``. Request-level ``on_exit`` signals decide whether each active layer is suspended or deleted when the run exits, with suspend as the default so successful terminal events can include resumable -snapshots. Successful runs publish the final JSON-safe agent output and the -resumable Agenton session snapshot together on the terminal ``run_succeeded`` -event so consumers can treat terminal events as complete run summaries. Session -snapshots carry only layer lifecycle/runtime state in compositor order; they do -not persist output-layer config. Resumed structured-output runs therefore must -resubmit the same ``output`` layer in ``composition.layers[]`` so snapshot layer -name/order still matches the composition and the runtime can rebuild the same -structured output contract. +snapshots. Successful runs always publish the resumable Agenton session snapshot +on the terminal ``run_succeeded`` event together with exactly one of the final +JSON-safe ``output`` or a deferred external ``deferred_tool_call`` payload. That +lets consumers treat terminal success events as complete run summaries without a +separate pause protocol. Session snapshots carry only layer lifecycle/runtime +state in compositor order; they do not persist output-layer config. Resumed +structured-output runs therefore must resubmit the same ``output`` layer in +``composition.layers[]`` so snapshot layer name/order still matches the +composition and the runtime can rebuild the same structured output contract. """ +from __future__ import annotations + from datetime import datetime, timezone from typing import Annotated, ClassVar, Final, Literal, TypeAlias -from pydantic import BaseModel, ConfigDict, Field, JsonValue, TypeAdapter +from pydantic import BaseModel, ConfigDict, Field, JsonValue, TypeAdapter, model_serializer, model_validator from pydantic_ai.messages import AgentStreamEvent +from pydantic_ai.tools import DeferredToolResults from agenton.compositor import CompositorConfig, CompositorSessionSnapshot, LayerConfigInput, LayerNodeConfig from agenton.layers import ExitIntent @@ -45,12 +49,11 @@ from agenton.layers import ExitIntent DIFY_AGENT_MODEL_LAYER_ID: Final[str] = "llm" DIFY_AGENT_HISTORY_LAYER_ID: Final[str] = "history" DIFY_AGENT_OUTPUT_LAYER_ID: Final[str] = "output" -RunStatus = Literal["running", "paused", "succeeded", "failed", "cancelled"] +RunStatus = Literal["running", "succeeded", "failed", "cancelled"] RunPurpose = Literal["workflow_node", "single_step", "agent_app", "babysit", "fasten_preview"] RunEventType = Literal[ "run_started", "pydantic_ai_event", - "run_paused", "run_succeeded", "run_failed", "run_cancelled", @@ -121,7 +124,15 @@ class CreateRunRequest(BaseModel): keep snapshot compatibility and rebuild the output schema. Dify tenant, user, and run-correlation identifiers must be submitted through a ``dify.execution_context`` entry in ``composition.layers[]``; there is no - parallel top-level ``execution_context`` request field. + parallel top-level ``execution_context`` request field. External deferred + tool continuation input belongs in the top-level ``deferred_tool_results`` + field rather than inside composition. Resume requests are therefore expected + to pair a prior ``session_snapshot`` with the same logical composition so + Agenton can rebuild the same layers and message history. For ask-human + continuation specifically, the matching pending tool call must still exist + in prior history state; callers should keep the history layer active across + runs so deferred tool results can be matched against the original model + response instead of starting a fresh user-prompt turn. """ composition: RunComposition @@ -129,6 +140,7 @@ class CreateRunRequest(BaseModel): idempotency_key: str | None = None metadata: dict[str, JsonValue] = Field(default_factory=dict) session_snapshot: CompositorSessionSnapshot | None = None + deferred_tool_results: DeferredToolResultsPayload | None = None on_exit: LayerExitSignals = Field(default_factory=LayerExitSignals) model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid") @@ -213,14 +225,59 @@ class EmptyRunEventData(BaseModel): model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid") -class RunSucceededEventData(BaseModel): - """Terminal success payload for final output and resumable session state.""" +class DeferredToolResultsPayload(BaseModel): + """Public JSON-safe DTO for deferred external tool results supplied on resume.""" - output: JsonValue + calls: dict[str, JsonValue] = Field(default_factory=dict) + metadata: dict[str, dict[str, JsonValue]] = Field(default_factory=dict) + + model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid") + + def to_pydantic_ai(self) -> DeferredToolResults: + """Convert the public DTO into pydantic-ai's resume input dataclass.""" + return DeferredToolResults( + calls=dict(self.calls), + metadata={key: dict(value) for key, value in self.metadata.items()}, + ) + + +class DeferredToolCallPayload(BaseModel): + """Terminal success payload for one deferred external tool request.""" + + tool_call_id: str + tool_name: str + args: JsonValue + metadata: dict[str, JsonValue] = Field(default_factory=dict) + + model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid") + + +class RunSucceededEventData(BaseModel): + """Terminal success payload for final output or deferred tool continuation.""" + + output: JsonValue | None = None + deferred_tool_call: DeferredToolCallPayload | None = None session_snapshot: CompositorSessionSnapshot model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid") + @model_validator(mode="after") + def _validate_result_shape(self) -> RunSucceededEventData: + has_output = "output" in self.model_fields_set + has_deferred_tool_call = "deferred_tool_call" in self.model_fields_set + if has_output == has_deferred_tool_call: + raise ValueError("Exactly one of output or deferred_tool_call must be set") + return self + + @model_serializer(mode="plain") + def _serialize_active_result(self) -> dict[str, object]: + data: dict[str, object] = {"session_snapshot": self.session_snapshot} + if "output" in self.model_fields_set: + data["output"] = self.output + if "deferred_tool_call" in self.model_fields_set: + data["deferred_tool_call"] = self.deferred_tool_call + return data + class RunFailedEventData(BaseModel): """Terminal failure payload shown to polling and SSE consumers.""" @@ -231,16 +288,6 @@ class RunFailedEventData(BaseModel): model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid") -class RunPausedEventData(BaseModel): - """Pause payload used for human handoff or other resumable waits.""" - - reason: str - message: str | None = None - session_snapshot: CompositorSessionSnapshot | None = None - - model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid") - - class RunCancelledEventData(BaseModel): """Terminal cancellation payload for explicit user/operator cancellation.""" @@ -288,13 +335,6 @@ class RunFailedEvent(BaseRunEvent): data: RunFailedEventData -class RunPausedEvent(BaseRunEvent): - """Resumable pause event emitted when a run waits for outside input.""" - - type: Literal["run_paused"] = "run_paused" - data: RunPausedEventData - - class RunCancelledEvent(BaseRunEvent): """Terminal cancellation event emitted after an explicit cancel request.""" @@ -303,12 +343,7 @@ class RunCancelledEvent(BaseRunEvent): RunEvent: TypeAlias = Annotated[ - RunStartedEvent - | PydanticAIStreamRunEvent - | RunPausedEvent - | RunSucceededEvent - | RunFailedEvent - | RunCancelledEvent, + RunStartedEvent | PydanticAIStreamRunEvent | RunSucceededEvent | RunFailedEvent | RunCancelledEvent, Field(discriminator="type"), ] RUN_EVENT_ADAPTER: TypeAdapter[RunEvent] = TypeAdapter(RunEvent) @@ -330,6 +365,8 @@ __all__ = [ "CancelRunResponse", "CreateRunRequest", "CreateRunResponse", + "DeferredToolCallPayload", + "DeferredToolResultsPayload", "DIFY_AGENT_HISTORY_LAYER_ID", "DIFY_AGENT_MODEL_LAYER_ID", "DIFY_AGENT_OUTPUT_LAYER_ID", @@ -345,8 +382,6 @@ __all__ = [ "RunEventsResponse", "RunFailedEvent", "RunFailedEventData", - "RunPausedEvent", - "RunPausedEventData", "RunPurpose", "RunStartedEvent", "RunStatus", diff --git a/dify-agent/src/dify_agent/runtime/compositor_factory.py b/dify-agent/src/dify_agent/runtime/compositor_factory.py index 8454513af22..959a1329ac6 100644 --- a/dify-agent/src/dify_agent/runtime/compositor_factory.py +++ b/dify-agent/src/dify_agent/runtime/compositor_factory.py @@ -2,8 +2,9 @@ Only explicitly allowed provider type ids are constructible here. The default provider set contains prompt layers, the optional pydantic-ai history layer, the -state-free Dify structured output layer, the Dify execution-context layer, the -stateful Dify shell layer, and the Dify plugin business-layer family: +state-free Dify structured output layer, the optional Dify ask-human layer, the +Dify execution-context layer, the stateful Dify shell layer, and the Dify +plugin business-layer family: - ``dify.execution_context`` for shared tenant/user/run daemon context, - ``dify.shell`` for shellctl-backed shell job control, @@ -33,6 +34,7 @@ from agenton_collections.layers.plain.basic import PromptLayer from agenton_collections.transformers.pydantic_ai import PYDANTIC_AI_TRANSFORMERS from dify_agent.agent_stub.server.shell_agent_stub_env import ShellAgentStubTokenFactory from dify_agent.agent_stub.server.tokens.agent_stub import AgentStubTokenCodec +from dify_agent.layers.ask_human.layer import DifyAskHumanLayer from dify_agent.layers.dify_plugin.llm_layer import DifyPluginLLMLayer from dify_agent.layers.dify_plugin.tools_layer import DifyPluginToolsLayer from dify_agent.layers.execution_context.configs import DifyExecutionContextLayerConfig @@ -80,6 +82,7 @@ def create_default_layer_providers( LayerProvider.from_layer_type(PromptLayer), LayerProvider.from_layer_type(PydanticAIHistoryLayer), LayerProvider.from_layer_type(DifyOutputLayer), + LayerProvider.from_layer_type(DifyAskHumanLayer), LayerProvider.from_factory( layer_type=DifyExecutionContextLayer, create=lambda config: DifyExecutionContextLayer.from_config_with_settings( diff --git a/dify-agent/src/dify_agent/runtime/event_sink.py b/dify-agent/src/dify_agent/runtime/event_sink.py index 6567189c699..80cbf76cbd9 100644 --- a/dify-agent/src/dify_agent/runtime/event_sink.py +++ b/dify-agent/src/dify_agent/runtime/event_sink.py @@ -3,19 +3,21 @@ The runner only needs append-only event writes and status transitions, so tests can use ``InMemoryRunEventSink`` without Redis. Production storage implements the same protocol with Redis streams in ``dify_agent.storage.redis_run_store``. The -terminal success helper writes the final JSON-safe output and session snapshot in -one event so event consumers can stop at ``run_succeeded`` without correlating -separate payload events. +terminal success helper writes either the final JSON-safe output or one deferred +tool request together with the resumable session snapshot in a single event so +consumers can stop at ``run_succeeded`` without correlating separate payload +events. """ from collections import defaultdict -from typing import Protocol +from typing import Protocol, cast from pydantic import JsonValue from pydantic_ai.messages import AgentStreamEvent from agenton.compositor import CompositorSessionSnapshot from dify_agent.protocol.schemas import ( + DeferredToolCallPayload, EmptyRunEventData, PydanticAIStreamRunEvent, RunEvent, @@ -29,6 +31,9 @@ from dify_agent.protocol.schemas import ( ) +_UNSET = object() + + class RunEventSink(Protocol): """Boundary used by runtime code to publish observable run progress.""" @@ -95,15 +100,31 @@ async def emit_run_succeeded( sink: RunEventSink, *, run_id: str, - output: JsonValue, + output: JsonValue | None | object = _UNSET, + deferred_tool_call: DeferredToolCallPayload | object = _UNSET, session_snapshot: CompositorSessionSnapshot, ) -> str: - """Emit the terminal success event with output and resumable state.""" + """Emit the terminal success event with output or deferred continuation. + + Callers must activate exactly one result branch. ``_UNSET`` is used instead + of ``None`` to preserve the distinction between an omitted inactive branch + and an active ``output`` branch whose JSON value is explicitly ``null``. + Without that sentinel, ``output=None`` would be indistinguishable from + “output field absent”, which would break nullable-success payloads. + """ + data: dict[str, JsonValue | DeferredToolCallPayload | CompositorSessionSnapshot | None] = { + "session_snapshot": session_snapshot, + } + if output is not _UNSET: + data["output"] = cast(JsonValue | None, output) + if deferred_tool_call is not _UNSET: + data["deferred_tool_call"] = cast(DeferredToolCallPayload, deferred_tool_call) + return await emit_run_event( sink, event=RunSucceededEvent( run_id=run_id, - data=RunSucceededEventData(output=output, session_snapshot=session_snapshot), + data=RunSucceededEventData.model_validate(data), created_at=utc_now(), ), ) diff --git a/dify-agent/src/dify_agent/runtime/runner.py b/dify-agent/src/dify_agent/runtime/runner.py index 9458b5e7e33..8a6d7b9bd91 100644 --- a/dify-agent/src/dify_agent/runtime/runner.py +++ b/dify-agent/src/dify_agent/runtime/runner.py @@ -3,36 +3,47 @@ The runner is storage-agnostic: it normalizes the public Dify composition into Agenton's graph/config split, enters a fresh ``CompositorRun`` (or resumes one from a snapshot), renders the current Dify system prompts into temporary -``message_history``, runs pydantic-ai with ``run.user_prompts`` as the current -user input, emits stream events, applies request-level ``on_exit`` signals, and -then publishes a terminal success or failure event. The Pydantic AI model is -resolved from the active Agenton layer named by ``DIFY_AGENT_MODEL_LAYER_ID``. -An optional history layer contributes stored message history only through -session state; successful runs append only ``result.new_messages()`` back into -that layer so current system prompts are not persisted. An optional structured -output layer named by ``DIFY_AGENT_OUTPUT_LAYER_ID`` is read after entry and -resolved into an output contract whose type both exposes the output schema to -the model and performs runtime JSON Schema validation through custom Pydantic -hooks. Invalid structured outputs therefore trigger Pydantic AI's normal -output-validation retry behavior before Dify Agent emits ``run_succeeded``. -Layers still never own the FastAPI lifespan-owned plugin daemon HTTP client. -Successful terminal events contain both the JSON-safe final output and session -snapshot; there are no separate output or snapshot events to correlate. +``message_history``, runs pydantic-ai with either the current ``run.user_prompts`` +or deferred external tool results, emits stream events, applies request-level +``on_exit`` signals, and then publishes a terminal success or failure event. The +Pydantic AI model is resolved from the active Agenton layer named by +``DIFY_AGENT_MODEL_LAYER_ID``. An optional history layer contributes stored +message history only through session state; successful runs append only +``result.new_messages()`` back into that layer so current system prompts are not +persisted. An optional structured output layer named by +``DIFY_AGENT_OUTPUT_LAYER_ID`` is read after entry and resolved into an output +contract whose type both exposes the output schema to the model and performs +runtime JSON Schema validation through custom Pydantic hooks. When the ask-human +layer is active, the runtime also allows ``DeferredToolRequests`` output and +publishes that deferred request through the normal ``run_succeeded`` event as +``deferred_tool_call`` instead of a final ``output``. Invalid structured outputs +or invalid deferred-tool behavior still trigger normal retries/failures before +Dify Agent emits success. Layers still never own the FastAPI lifespan-owned +plugin daemon HTTP client. """ from collections.abc import AsyncIterable from collections import Counter -from typing import Any, cast +from dataclasses import dataclass +from typing import Any, Literal, cast import httpx from pydantic import JsonValue, TypeAdapter from pydantic_ai.messages import AgentStreamEvent +from pydantic_ai.output import OutputSpec +from pydantic_ai.tools import DeferredToolRequests, DeferredToolResults from agenton.compositor import CompositorSessionSnapshot, LayerProviderInput from agenton.layers.types import PydanticAITool +from dify_agent.layers.ask_human.layer import get_ask_human_layer, validate_ask_human_layer_composition from dify_agent.layers.dify_plugin.llm_layer import DifyPluginLLMLayer from dify_agent.layers.dify_plugin.tools_layer import DifyPluginToolsLayer -from dify_agent.protocol.schemas import DIFY_AGENT_MODEL_LAYER_ID, CreateRunRequest, normalize_composition +from dify_agent.protocol.schemas import ( + CreateRunRequest, + DIFY_AGENT_MODEL_LAYER_ID, + DeferredToolCallPayload, + normalize_composition, +) from dify_agent.runtime.agent_factory import create_agent, normalize_user_input from dify_agent.runtime.agenton_validation import is_agenton_enter_validation_runtime_error from dify_agent.runtime.compositor_factory import build_pydantic_ai_compositor, create_default_layer_providers @@ -61,6 +72,16 @@ class AgentRunValidationError(ValueError): """Raised when a run request is valid JSON but cannot execute.""" +@dataclass(slots=True) +class RunSuccessOutcome: + """Normalized successful runner output before event emission.""" + + result_kind: Literal["output", "deferred_tool_call"] + output: JsonValue | None + deferred_tool_call: DeferredToolCallPayload | None + session_snapshot: CompositorSessionSnapshot + + class AgentRunRunner: """Executes one run and writes only public run events to its sink.""" @@ -92,7 +113,7 @@ class AgentRunRunner: _ = await emit_run_started(self.sink, run_id=self.run_id) try: - output, session_snapshot = await self._run_agent() + outcome = await self._run_agent() except Exception as exc: message = str(exc) or type(exc).__name__ _ = await emit_run_failed(self.sink, run_id=self.run_id, error=message) @@ -102,12 +123,16 @@ class AgentRunRunner: _ = await emit_run_succeeded( self.sink, run_id=self.run_id, - output=output, - session_snapshot=session_snapshot, + **( + {"output": outcome.output} + if outcome.result_kind == "output" + else {"deferred_tool_call": outcome.deferred_tool_call} + ), + session_snapshot=outcome.session_snapshot, ) await self.sink.update_status(self.run_id, "succeeded") - async def _run_agent(self) -> tuple[JsonValue, CompositorSessionSnapshot]: + async def _run_agent(self) -> RunSuccessOutcome: """Run pydantic-ai inside an entered Agenton run. Known request-shaped Agenton enter-time failures are normalized to @@ -128,6 +153,7 @@ class AgentRunRunner: try: validate_output_layer_composition(self.request.composition) validate_history_layer_composition(self.request.composition) + validate_ask_human_layer_composition(self.request.composition) graph_config, layer_configs = normalize_composition(self.request.composition) compositor = build_pydantic_ai_compositor(graph_config, providers=self.layer_providers) validate_layer_exit_signals(compositor, self.request.on_exit) @@ -135,12 +161,16 @@ class AgentRunRunner: raise AgentRunValidationError(str(exc)) from exc entered_run = False + output: JsonValue | None = None + deferred_tool_call: DeferredToolCallPayload | None = None + result_kind: Literal["output", "deferred_tool_call"] | None = None try: async with compositor.enter(configs=layer_configs, session_snapshot=self.request.session_snapshot) as run: entered_run = True apply_layer_exit_signals(run, self.request.on_exit) user_prompts = run.user_prompts - if not has_non_blank_user_prompt(user_prompts): + deferred_tool_results = _resolve_deferred_tool_results(self.request) + if deferred_tool_results is None and not has_non_blank_user_prompt(user_prompts): raise AgentRunValidationError(EMPTY_USER_PROMPTS_ERROR) async def handle_events(_ctx: object, events: AsyncIterable[AgentStreamEvent]) -> None: @@ -154,24 +184,44 @@ class AgentRunRunner: system_prompts=run.prompts, stored_history=history_layer.message_history if history_layer is not None else (), ) + ask_human_layer = get_ask_human_layer(run) llm_layer = run.get_layer(DIFY_AGENT_MODEL_LAYER_ID, DifyPluginLLMLayer) model = llm_layer.get_model(http_client=self.plugin_daemon_http_client) tools = await _resolve_run_tools(run, http_client=self.plugin_daemon_http_client) except (KeyError, TypeError, RuntimeError, ValueError) as exc: raise AgentRunValidationError(str(exc)) from exc + if deferred_tool_results is not None and history_layer is None: + raise AgentRunValidationError( + "Deferred tool results require a 'history' layer with prior message history." + ) + agent = create_agent( model, tools=tools, - output_type=output_contract.output_type, + output_type=_resolve_agent_output_type(output_contract.output_type, ask_human_layer is not None), ) result = await agent.run( - normalize_user_input(user_prompts), + None if deferred_tool_results is not None else normalize_user_input(user_prompts), message_history=message_history, + deferred_tool_results=deferred_tool_results, event_stream_handler=handle_events, ) - output = _serialize_agent_output(result.output) append_successful_run_history(history_layer, result.new_messages()) + if isinstance(result.output, DeferredToolRequests): + if ask_human_layer is None: + raise AgentRunValidationError( + "Deferred tool requests were returned, but no active ask_human layer is available for validation." + ) + if history_layer is None: + raise AgentRunValidationError( + "ask_human deferred tool requests require a 'history' layer so the pending tool call can be resumed." + ) + deferred_tool_call = ask_human_layer.build_deferred_tool_call_payload(result.output) + result_kind = "deferred_tool_call" + else: + output = _serialize_agent_output(result.output) + result_kind = "output" except RuntimeError as exc: if not entered_run and is_agenton_enter_validation_runtime_error(exc): raise AgentRunValidationError(str(exc)) from exc @@ -183,8 +233,15 @@ class AgentRunRunner: if run.session_snapshot is None: raise RuntimeError("Agenton run did not produce a session snapshot after exit.") + if result_kind is None: + raise RuntimeError("Agent run did not resolve either a final output or a deferred tool call.") - return output, run.session_snapshot + return RunSuccessOutcome( + result_kind=result_kind, + output=output, + deferred_tool_call=deferred_tool_call, + session_snapshot=run.session_snapshot, + ) def _serialize_agent_output(output: object) -> JsonValue: @@ -192,6 +249,20 @@ def _serialize_agent_output(output: object) -> JsonValue: return cast(JsonValue, _AGENT_OUTPUT_ADAPTER.dump_python(output, mode="json")) +def _resolve_agent_output_type(output_type: OutputSpec[object], allow_deferred_tools: bool) -> OutputSpec[object]: + """Return the run output type, optionally augmented with deferred-tool support.""" + if not allow_deferred_tools: + return output_type + return cast(OutputSpec[object], [output_type, DeferredToolRequests]) + + +def _resolve_deferred_tool_results(request: CreateRunRequest) -> DeferredToolResults | None: + """Convert public deferred tool results into the pydantic-ai resume input.""" + if request.deferred_tool_results is None: + return None + return request.deferred_tool_results.to_pydantic_ai() + + async def _resolve_run_tools( run: Any, *, diff --git a/dify-agent/tests/local/dify_agent/layers/ask_human/test_configs.py b/dify-agent/tests/local/dify_agent/layers/ask_human/test_configs.py new file mode 100644 index 00000000000..f9b42220e03 --- /dev/null +++ b/dify-agent/tests/local/dify_agent/layers/ask_human/test_configs.py @@ -0,0 +1,73 @@ +import pytest +from pydantic import ValidationError + +import dify_agent.layers.ask_human as ask_human_exports +from dify_agent.layers.ask_human import DIFY_ASK_HUMAN_LAYER_TYPE_ID, DifyAskHumanLayerConfig +from dify_agent.layers.ask_human.schema import AskHumanToolArgs + + +def test_ask_human_package_exports_client_safe_symbols_only() -> None: + assert ask_human_exports.DIFY_ASK_HUMAN_LAYER_TYPE_ID == "dify.ask_human" + assert ask_human_exports.__all__ == [ + "AskHumanAction", + "AskHumanActionStyle", + "AskHumanField", + "AskHumanFieldType", + "AskHumanFileField", + "AskHumanFileListField", + "AskHumanParagraphField", + "AskHumanResultStatus", + "AskHumanSelectField", + "AskHumanSelectOption", + "AskHumanSelectedAction", + "AskHumanToolArgs", + "AskHumanToolResult", + "AskHumanUrgency", + "DEFAULT_ASK_HUMAN_TOOL_DESCRIPTION", + "DIFY_ASK_HUMAN_LAYER_TYPE_ID", + "DifyAskHumanLayerConfig", + ] + assert not hasattr(ask_human_exports, "DifyAskHumanLayer") + + +def test_ask_human_layer_config_defaults_and_effective_description() -> None: + config = DifyAskHumanLayerConfig() + + assert DIFY_ASK_HUMAN_LAYER_TYPE_ID == "dify.ask_human" + assert config.model_dump(mode="json") == { + "enabled": True, + "tool_name": "ask_human", + "tool_description": None, + "max_fields": 8, + "max_actions": 4, + "allowed_field_types": ["paragraph", "select"], + "allow_file_fields": False, + "max_markdown_chars": 8000, + "max_question_chars": 1000, + "max_field_label_chars": 120, + "max_action_label_chars": 80, + } + assert "Ask a human for missing information" in config.effective_tool_description + + +def test_ask_human_layer_config_rejects_invalid_tool_name() -> None: + with pytest.raises(ValidationError, match="tool_name must be a valid tool identifier"): + _ = DifyAskHumanLayerConfig(tool_name="ask-human") + + +def test_ask_human_layer_config_rejects_file_field_types_when_disabled() -> None: + with pytest.raises(ValidationError, match="cannot include file field types"): + _ = DifyAskHumanLayerConfig(allowed_field_types=["paragraph", "file"]) + + +def test_ask_human_tool_args_reject_duplicate_field_names() -> None: + with pytest.raises(ValidationError, match="field name 'comment' must be unique"): + _ = AskHumanToolArgs.model_validate( + { + "question": "Need a reply", + "fields": [ + {"type": "paragraph", "name": "comment", "label": "Comment"}, + {"type": "paragraph", "name": "comment", "label": "Another comment"}, + ], + } + ) diff --git a/dify-agent/tests/local/dify_agent/layers/ask_human/test_layer.py b/dify-agent/tests/local/dify_agent/layers/ask_human/test_layer.py new file mode 100644 index 00000000000..903a3164a01 --- /dev/null +++ b/dify-agent/tests/local/dify_agent/layers/ask_human/test_layer.py @@ -0,0 +1,152 @@ +import asyncio +import inspect +from collections.abc import Awaitable +from typing import Any, cast + +import pytest +from pydantic_ai.messages import ToolCallPart +from pydantic_ai.tools import DeferredToolRequests, ToolDefinition + +from dify_agent.layers.ask_human import DifyAskHumanLayerConfig +from dify_agent.layers.ask_human.schema import AskHumanToolArgs +from dify_agent.layers.ask_human.layer import DifyAskHumanLayer + + +async def _await_tool_definition(value: Awaitable[ToolDefinition | None]) -> ToolDefinition | None: + return await value + + +def test_ask_human_layer_exposes_one_external_tool_and_prompt_hint() -> None: + config = DifyAskHumanLayerConfig( + tool_name="human_gate", + tool_description="Collect a human decision.", + max_fields=2, + max_actions=3, + allowed_field_types=["paragraph"], + allow_file_fields=False, + max_question_chars=240, + max_markdown_chars=512, + max_field_label_chars=32, + max_action_label_chars=16, + ) + layer = DifyAskHumanLayer.from_config(config) + + prompt_hint = layer.build_prompt_hint() + tool = layer.tools[0] + prepare = tool.prepare + assert prepare is not None + prepared_or_awaitable = prepare( + cast(Any, None), + ToolDefinition( + name=tool.name, description=tool.description, parameters_json_schema=tool.function_schema.json_schema + ), + ) + prepared = ( + asyncio.run(_await_tool_definition(cast(Awaitable[ToolDefinition | None], prepared_or_awaitable))) + if inspect.isawaitable(prepared_or_awaitable) + else prepared_or_awaitable + ) + + assert len(layer.prefix_prompts) == 1 + assert len(layer.tools) == 1 + assert "Allowed field types: paragraph." in prompt_hint + assert "File upload fields are disabled." in prompt_hint + assert "Use at most 2 field(s)." in prompt_hint + assert "Use at most 3 action(s)." in prompt_hint + assert "Keep 'question' under 240 characters." in prompt_hint + assert "Keep 'markdown' under 512 characters." in prompt_hint + assert "Keep each field label under 32 characters." in prompt_hint + assert "Keep each action label under 16 characters." in prompt_hint + assert prepared is not None + assert prepared.name == "human_gate" + assert prepared.description == "Collect a human decision." + assert prepared.kind == "external" + assert prepared.parameters_json_schema == AskHumanToolArgs.model_json_schema() + + +def test_ask_human_layer_normalizes_default_action_in_deferred_payload() -> None: + layer = DifyAskHumanLayer.from_config(DifyAskHumanLayerConfig()) + + payload = layer.build_deferred_tool_call_payload( + DeferredToolRequests( + calls=[ + ToolCallPart( + tool_name="ask_human", + args={ + "question": "Need a human answer", + "fields": [{"type": "paragraph", "name": "comment", "label": "Comment"}], + }, + tool_call_id="call-1", + ) + ] + ) + ) + + assert payload.tool_call_id == "call-1" + assert payload.tool_name == "ask_human" + assert payload.args == { + "title": None, + "question": "Need a human answer", + "markdown": None, + "fields": [ + { + "type": "paragraph", + "name": "comment", + "label": "Comment", + "required": False, + "placeholder": None, + "default": None, + } + ], + "actions": [{"id": "submit", "label": "Submit", "style": "primary"}], + "urgency": "normal", + } + assert payload.metadata == { + "layer_type": "dify.ask_human", + "tool_name": "ask_human", + "schema_version": 1, + } + + +def test_ask_human_layer_rejects_disallowed_field_types_in_deferred_payload() -> None: + layer = DifyAskHumanLayer.from_config(DifyAskHumanLayerConfig(allowed_field_types=["paragraph"])) + + with pytest.raises(ValueError, match="field type 'select' is not allowed"): + _ = layer.build_deferred_tool_call_payload( + DeferredToolRequests( + calls=[ + ToolCallPart( + tool_name="ask_human", + args={ + "question": "Need a choice", + "fields": [ + { + "type": "select", + "name": "decision", + "label": "Decision", + "options": [{"value": "yes", "label": "Yes"}], + } + ], + }, + tool_call_id="call-2", + ) + ] + ) + ) + + +def test_ask_human_layer_rejects_tool_name_mismatch_in_deferred_payload() -> None: + layer = DifyAskHumanLayer.from_config(DifyAskHumanLayerConfig(tool_name="human_gate")) + + with pytest.raises(ValueError, match="deferred tool name must be 'human_gate', got 'ask_human'"): + _ = layer.build_deferred_tool_call_payload( + DeferredToolRequests( + calls=[ + ToolCallPart( + tool_name="ask_human", + args={"question": "Need a human answer"}, + tool_call_id="call-3", + ) + ] + ) + ) diff --git a/dify-agent/tests/local/dify_agent/protocol/test_protocol_schemas.py b/dify-agent/tests/local/dify_agent/protocol/test_protocol_schemas.py index 76606b9132c..b58e0818229 100644 --- a/dify-agent/tests/local/dify_agent/protocol/test_protocol_schemas.py +++ b/dify-agent/tests/local/dify_agent/protocol/test_protocol_schemas.py @@ -6,6 +6,7 @@ from agenton.compositor import CompositorSessionSnapshot from agenton.layers import ExitIntent from agenton_collections.layers.plain import PLAIN_PROMPT_LAYER_TYPE_ID, PromptLayerConfig import dify_agent.protocol as protocol_exports +from dify_agent.layers.ask_human import DifyAskHumanLayerConfig from dify_agent.layers.execution_context import DIFY_EXECUTION_CONTEXT_LAYER_TYPE_ID, DifyExecutionContextLayerConfig from dify_agent.layers.dify_plugin import DIFY_PLUGIN_LLM_LAYER_TYPE_ID, DIFY_PLUGIN_TOOLS_LAYER_TYPE_ID from dify_agent.layers.output import DIFY_OUTPUT_LAYER_TYPE_ID, DifyOutputLayerConfig @@ -13,6 +14,7 @@ from dify_agent.protocol import DIFY_AGENT_HISTORY_LAYER_ID, DIFY_AGENT_MODEL_LA from dify_agent.protocol.schemas import ( RUN_EVENT_ADAPTER, CreateRunRequest, + DeferredToolCallPayload, LayerExitSignals, PydanticAIStreamRunEvent, RunCancelledEvent, @@ -21,8 +23,6 @@ from dify_agent.protocol.schemas import ( RunFailedEvent, RunFailedEventData, RunLayerSpec, - RunPausedEvent, - RunPausedEventData, RunStartedEvent, RunSucceededEvent, RunSucceededEventData, @@ -49,15 +49,19 @@ def test_run_event_adapter_round_trips_typed_variants() -> None: session_snapshot=CompositorSessionSnapshot(layers=[]), ), ), - RunFailedEvent(run_id="run-1", data=RunFailedEventData(error="boom", reason="shutdown")), - RunPausedEvent( - run_id="run-1", - data=RunPausedEventData( - reason="human_handoff", - message="Need review", + RunSucceededEvent( + run_id="run-2", + data=RunSucceededEventData( + deferred_tool_call=DeferredToolCallPayload( + tool_call_id="tool-call-1", + tool_name="ask_human", + args={"question": "Need approval"}, + metadata={"layer_type": "dify.ask_human"}, + ), session_snapshot=CompositorSessionSnapshot(layers=[]), ), ), + RunFailedEvent(run_id="run-1", data=RunFailedEventData(error="boom", reason="shutdown")), RunCancelledEvent(run_id="run-1", data=RunCancelledEventData(reason="user_cancelled")), ] @@ -204,6 +208,35 @@ def test_create_run_request_accepts_dto_first_public_composition_and_normalizes_ } +def test_create_run_request_accepts_deferred_tool_results_payload() -> None: + request = CreateRunRequest.model_validate( + { + "composition": { + "layers": [ + {"name": "prompt", "type": PLAIN_PROMPT_LAYER_TYPE_ID, "config": {"user": "hello"}}, + {"name": "ask_human", "type": "dify.ask_human", "config": DifyAskHumanLayerConfig().model_dump()}, + ] + }, + "deferred_tool_results": { + "calls": { + "tool-call-1": { + "status": "submitted", + "action": {"id": "submit", "label": "Submit"}, + "values": {"comment": "Looks good."}, + } + } + }, + } + ) + + assert request.deferred_tool_results is not None + assert request.deferred_tool_results.calls["tool-call-1"] == { + "status": "submitted", + "action": {"id": "submit", "label": "Submit"}, + "values": {"comment": "Looks good."}, + } + + def test_create_run_request_accepts_plugin_tools_layer_with_prepared_parameters_and_schema() -> None: request = CreateRunRequest.model_validate( { @@ -327,6 +360,49 @@ def test_on_exit_default_to_suspend_and_are_public() -> None: assert request.on_exit.layers == {} +def test_run_succeeded_event_data_requires_exactly_one_result_variant() -> None: + snapshot = CompositorSessionSnapshot(layers=[]) + + with pytest.raises(ValidationError, match="Exactly one of output or deferred_tool_call must be set"): + _ = RunSucceededEventData(session_snapshot=snapshot) + + with pytest.raises(ValidationError, match="Exactly one of output or deferred_tool_call must be set"): + _ = RunSucceededEventData( + output="done", + deferred_tool_call=DeferredToolCallPayload( + tool_call_id="tool-call-1", + tool_name="ask_human", + args={"question": "Need approval"}, + ), + session_snapshot=snapshot, + ) + + +def test_run_succeeded_event_data_allows_explicit_json_null_output() -> None: + snapshot = CompositorSessionSnapshot(layers=[]) + + data = RunSucceededEventData(output=None, session_snapshot=snapshot) + + assert data.output is None + assert data.deferred_tool_call is None + + +def test_run_succeeded_event_round_trips_explicit_json_null_output() -> None: + event = RunSucceededEvent( + run_id="run-null-output", + data=RunSucceededEventData(output=None, session_snapshot=CompositorSessionSnapshot(layers=[])), + ) + + payload = RUN_EVENT_ADAPTER.dump_json(event) + decoded = RUN_EVENT_ADAPTER.validate_json(payload) + + assert isinstance(decoded, RunSucceededEvent) + assert decoded.data.output is None + assert decoded.data.deferred_tool_call is None + assert b'"output":null' in payload + assert b'"deferred_tool_call"' not in payload + + def test_on_exit_accept_layer_overrides() -> None: request = CreateRunRequest.model_validate( { diff --git a/dify-agent/tests/local/dify_agent/runtime/test_runner.py b/dify-agent/tests/local/dify_agent/runtime/test_runner.py index 93f18446d55..c910b7c3dd9 100644 --- a/dify-agent/tests/local/dify_agent/runtime/test_runner.py +++ b/dify-agent/tests/local/dify_agent/runtime/test_runner.py @@ -1,5 +1,5 @@ import asyncio -from collections.abc import Mapping +from collections.abc import Iterable, Mapping from typing import Any, ClassVar, cast import httpx @@ -8,6 +8,7 @@ from pydantic import JsonValue from pydantic_ai import Tool from pydantic_ai.exceptions import UnexpectedModelBehavior from pydantic_ai.messages import ( + ToolReturnPart, ModelMessage, ModelRequest, ModelResponse, @@ -18,12 +19,14 @@ from pydantic_ai.messages import ( ) from pydantic_ai.models import ModelRequestParameters from pydantic_ai.models.test import TestModel +from pydantic_ai.tools import DeferredToolRequests, DeferredToolResults from pydantic_ai.settings import ModelSettings from agenton.compositor import CompositorSessionSnapshot, LayerProvider, LayerSessionSnapshot from agenton.layers import ExitIntent, LifecycleState from agenton_collections.layers.pydantic_ai import PYDANTIC_AI_HISTORY_LAYER_TYPE_ID, PydanticAIHistoryRuntimeState from agenton_collections.layers.plain import PromptLayerConfig, ToolsLayer +from dify_agent.layers.ask_human import DIFY_ASK_HUMAN_LAYER_TYPE_ID, DifyAskHumanLayerConfig from dify_agent.layers.execution_context import DIFY_EXECUTION_CONTEXT_LAYER_TYPE_ID, DifyExecutionContextLayerConfig from dify_agent.layers.shell import DIFY_SHELL_LAYER_TYPE_ID, DifyShellLayerConfig from dify_agent.layers.shell.layer import DifyShellLayer @@ -42,6 +45,7 @@ from dify_agent.layers.output import DIFY_OUTPUT_LAYER_TYPE_ID, DifyOutputLayerC from dify_agent.protocol import DIFY_AGENT_HISTORY_LAYER_ID, DIFY_AGENT_MODEL_LAYER_ID, DIFY_AGENT_OUTPUT_LAYER_ID from dify_agent.protocol.schemas import ( CreateRunRequest, + DeferredToolResultsPayload, LayerExitSignals, RunComposition, RunLayerSpec, @@ -54,7 +58,7 @@ from shell_session_manager.shellctl.shared import DeleteJobResponse, JobResult, class StaticToolsTestLayer(ToolsLayer): - type_id: ClassVar[str] = "test.static.tools" + type_id: ClassVar[str | None] = "test.static.tools" class FakeRunnerShellctlClient: @@ -115,6 +119,8 @@ def _request( user: str | list[str] = "hello", *, include_history: bool = False, + include_ask_human: bool = False, + ask_human_config: DifyAskHumanLayerConfig | None = None, llm_layer_name: str = DIFY_AGENT_MODEL_LAYER_ID, execution_context_layer_name: str = "execution_context", on_exit: LayerExitSignals | None = None, @@ -131,6 +137,17 @@ def _request( if include_history else [] ), + *( + [ + RunLayerSpec( + name="ask_human", + type=DIFY_ASK_HUMAN_LAYER_TYPE_ID, + config=ask_human_config or DifyAskHumanLayerConfig(), + ) + ] + if include_ask_human + else [] + ), RunLayerSpec( name=execution_context_layer_name, type=DIFY_EXECUTION_CONTEXT_LAYER_TYPE_ID, @@ -270,6 +287,7 @@ class RecordingTestModel(TestModel): def _history_session_snapshot( messages: list[ModelMessage], *, + include_ask_human: bool = False, include_output: bool = False, ) -> CompositorSessionSnapshot: layers = [ @@ -279,6 +297,11 @@ def _history_session_snapshot( lifecycle_state=LifecycleState.SUSPENDED, runtime_state=PydanticAIHistoryRuntimeState(messages=messages).model_dump(mode="json"), ), + *( + [LayerSessionSnapshot(name="ask_human", lifecycle_state=LifecycleState.SUSPENDED, runtime_state={})] + if include_ask_human + else [] + ), LayerSessionSnapshot(name="execution_context", lifecycle_state=LifecycleState.SUSPENDED, runtime_state={}), LayerSessionSnapshot( name=DIFY_AGENT_MODEL_LAYER_ID, lifecycle_state=LifecycleState.SUSPENDED, runtime_state={} @@ -302,6 +325,18 @@ def _flatten_message_parts(messages: list[ModelMessage]) -> list[object]: return [part for message in messages for part in message.parts] +class FakeAgentRunResult: + output: object + _new_messages: list[ModelMessage] + + def __init__(self, output: object, new_messages: list[ModelMessage]) -> None: + self.output = output + self._new_messages = new_messages + + def new_messages(self) -> list[ModelMessage]: + return list(self._new_messages) + + def test_runner_emits_terminal_success_and_snapshot(monkeypatch: pytest.MonkeyPatch) -> None: seen_clients: list[httpx.AsyncClient] = [] @@ -350,6 +385,489 @@ def test_runner_emits_terminal_success_and_snapshot(monkeypatch: pytest.MonkeyPa assert sink.statuses["run-1"] == "succeeded" +def test_runner_preserves_explicit_json_null_output(monkeypatch: pytest.MonkeyPatch) -> None: + def fake_get_model(_self: DifyPluginLLMLayer, *, http_client: httpx.AsyncClient): + assert http_client.is_closed is False + return TestModel(custom_output_text="unused") # pyright: ignore[reportReturnType] + + class FakeAgent: + async def run(self, *_args: object, **_kwargs: object) -> FakeAgentRunResult: + return FakeAgentRunResult(None, []) + + monkeypatch.setattr(DifyPluginLLMLayer, "get_model", fake_get_model) + monkeypatch.setattr("dify_agent.runtime.runner.create_agent", lambda *_args, **_kwargs: FakeAgent()) + request = _request() + sink = InMemoryRunEventSink() + + async def scenario() -> None: + async with httpx.AsyncClient() as client: + await AgentRunRunner( + sink=sink, + request=request, + run_id="run-null-output", + plugin_daemon_http_client=client, + ).run() + + asyncio.run(scenario()) + + terminal = sink.events["run-null-output"][-1] + assert isinstance(terminal, RunSucceededEvent) + assert terminal.data.output is None + assert terminal.data.deferred_tool_call is None + assert sink.statuses["run-null-output"] == "succeeded" + + +def test_runner_emits_deferred_tool_call_and_persists_pending_history(monkeypatch: pytest.MonkeyPatch) -> None: + captured_output_types: list[object] = [] + captured_user_prompts: list[object] = [] + pending_tool_call = ToolCallPart( + tool_name="ask_human", + args={ + "question": "Which deployment window should we use?", + "fields": [{"type": "paragraph", "name": "window", "label": "Deployment window"}], + }, + tool_call_id="tool-call-1", + ) + + def fake_get_model(_self: DifyPluginLLMLayer, *, http_client: httpx.AsyncClient): + assert http_client.is_closed is False + return TestModel(custom_output_text="unused") # pyright: ignore[reportReturnType] + + class FakeAgent: + async def run(self, user_prompt: object, **kwargs: object) -> FakeAgentRunResult: + captured_user_prompts.append(user_prompt) + assert kwargs["deferred_tool_results"] is None + return FakeAgentRunResult( + DeferredToolRequests(calls=[pending_tool_call]), + [ + ModelRequest(parts=[UserPromptPart(content="current user")]), + ModelResponse(parts=[pending_tool_call]), + ], + ) + + def fake_create_agent(model: object, *, tools: list[Tool[object]], output_type: object) -> FakeAgent: + del model, tools + captured_output_types.append(output_type) + return FakeAgent() + + monkeypatch.setattr(DifyPluginLLMLayer, "get_model", fake_get_model) + monkeypatch.setattr("dify_agent.runtime.runner.create_agent", fake_create_agent) + request = _request("current user", include_history=True, include_ask_human=True) + sink = InMemoryRunEventSink() + + async def scenario() -> None: + async with httpx.AsyncClient() as client: + await AgentRunRunner( + sink=sink, + request=request, + run_id="run-ask-human", + plugin_daemon_http_client=client, + ).run() + + asyncio.run(scenario()) + + terminal = sink.events["run-ask-human"][-1] + assert isinstance(terminal, RunSucceededEvent) + assert captured_user_prompts == ["current user"] + assert any(item is DeferredToolRequests for item in cast(Iterable[object], captured_output_types[0])) + assert terminal.data.output is None + assert terminal.data.deferred_tool_call is not None + assert terminal.data.deferred_tool_call.tool_call_id == "tool-call-1" + assert terminal.data.deferred_tool_call.tool_name == "ask_human" + assert terminal.data.deferred_tool_call.args == { + "title": None, + "question": "Which deployment window should we use?", + "markdown": None, + "fields": [ + { + "type": "paragraph", + "name": "window", + "label": "Deployment window", + "required": False, + "placeholder": None, + "default": None, + } + ], + "actions": [{"id": "submit", "label": "Submit", "style": "primary"}], + "urgency": "normal", + } + saved_history = _history_messages_from_snapshot(terminal.data.session_snapshot) + assert isinstance(saved_history[-1], ModelResponse) + assert saved_history[-1].parts == [pending_tool_call] + + +def test_runner_resumes_with_deferred_tool_results_and_no_user_prompt(monkeypatch: pytest.MonkeyPatch) -> None: + seen_user_prompts: list[object] = [] + seen_deferred_results: list[object] = [] + pending_tool_call = ToolCallPart( + tool_name="ask_human", + args={"question": "Need approval"}, + tool_call_id="tool-call-1", + ) + + def fake_get_model(_self: DifyPluginLLMLayer, *, http_client: httpx.AsyncClient): + assert http_client.is_closed is False + return TestModel(custom_output_text="unused") # pyright: ignore[reportReturnType] + + class FakeAgent: + async def run(self, user_prompt: object, **kwargs: object) -> FakeAgentRunResult: + seen_user_prompts.append(user_prompt) + seen_deferred_results.append(kwargs.get("deferred_tool_results")) + if kwargs.get("deferred_tool_results") is None: + return FakeAgentRunResult( + DeferredToolRequests(calls=[pending_tool_call]), + [ + ModelRequest(parts=[UserPromptPart(content="current user")]), + ModelResponse(parts=[pending_tool_call]), + ], + ) + + deferred_tool_results = cast(DeferredToolResults, kwargs["deferred_tool_results"]) + assert deferred_tool_results is not None + submitted_result = cast(dict[str, object], deferred_tool_results.calls["tool-call-1"]) + assert submitted_result["status"] == "submitted" + return FakeAgentRunResult( + "done after human", + [ + ModelRequest( + parts=[ + ToolReturnPart( + tool_name="ask_human", + content={"status": "submitted", "values": {"comment": "Ship it"}}, + tool_call_id="tool-call-1", + ) + ] + ), + ModelResponse(parts=[TextPart(content="done after human")]), + ], + ) + + def fake_create_agent(model: object, *, tools: list[Tool[object]], output_type: object) -> FakeAgent: + del model, tools, output_type + return FakeAgent() + + monkeypatch.setattr(DifyPluginLLMLayer, "get_model", fake_get_model) + monkeypatch.setattr("dify_agent.runtime.runner.create_agent", fake_create_agent) + request = _request("current user", include_history=True, include_ask_human=True) + sink = InMemoryRunEventSink() + + async def scenario() -> None: + async with httpx.AsyncClient() as client: + await AgentRunRunner( + sink=sink, + request=request, + run_id="run-ask-human-initial", + plugin_daemon_http_client=client, + ).run() + + initial_terminal = sink.events["run-ask-human-initial"][-1] + assert isinstance(initial_terminal, RunSucceededEvent) + + resumed_request = request.model_copy(deep=True) + resumed_request.session_snapshot = initial_terminal.data.session_snapshot + resumed_request.deferred_tool_results = DeferredToolResultsPayload.model_validate( + { + "calls": { + "tool-call-1": { + "status": "submitted", + "action": {"id": "submit", "label": "Submit"}, + "values": {"comment": "Ship it"}, + } + } + } + ) + + await AgentRunRunner( + sink=sink, + request=resumed_request, + run_id="run-ask-human-resume", + plugin_daemon_http_client=client, + ).run() + + asyncio.run(scenario()) + + resumed_terminal = sink.events["run-ask-human-resume"][-1] + assert isinstance(resumed_terminal, RunSucceededEvent) + assert resumed_terminal.data.output == "done after human" + assert resumed_terminal.data.deferred_tool_call is None + assert seen_user_prompts == ["current user", None] + assert seen_deferred_results[0] is None + assert seen_deferred_results[1] is not None + + +def test_runner_can_emit_second_deferred_tool_call_after_resume(monkeypatch: pytest.MonkeyPatch) -> None: + seen_user_prompts: list[object] = [] + first_pending_tool_call = ToolCallPart( + tool_name="ask_human", + args={"question": "Need deployment owner"}, + tool_call_id="tool-call-1", + ) + second_pending_tool_call = ToolCallPart( + tool_name="ask_human", + args={"question": "Need final go-live confirmation"}, + tool_call_id="tool-call-2", + ) + + def fake_get_model(_self: DifyPluginLLMLayer, *, http_client: httpx.AsyncClient): + assert http_client.is_closed is False + return TestModel(custom_output_text="unused") # pyright: ignore[reportReturnType] + + class FakeAgent: + async def run(self, user_prompt: object, **kwargs: object) -> FakeAgentRunResult: + seen_user_prompts.append(user_prompt) + deferred_tool_results = kwargs.get("deferred_tool_results") + if deferred_tool_results is None: + return FakeAgentRunResult( + DeferredToolRequests(calls=[first_pending_tool_call]), + [ + ModelRequest(parts=[UserPromptPart(content="current user")]), + ModelResponse(parts=[first_pending_tool_call]), + ], + ) + + return FakeAgentRunResult( + DeferredToolRequests(calls=[second_pending_tool_call]), + [ + ModelRequest( + parts=[ + ToolReturnPart( + tool_name="ask_human", + content={"status": "submitted", "values": {"owner": "ops"}}, + tool_call_id="tool-call-1", + ) + ] + ), + ModelResponse(parts=[second_pending_tool_call]), + ], + ) + + def fake_create_agent(model: object, *, tools: list[Tool[object]], output_type: object) -> FakeAgent: + del model, tools, output_type + return FakeAgent() + + monkeypatch.setattr(DifyPluginLLMLayer, "get_model", fake_get_model) + monkeypatch.setattr("dify_agent.runtime.runner.create_agent", fake_create_agent) + request = _request("current user", include_history=True, include_ask_human=True) + sink = InMemoryRunEventSink() + + async def scenario() -> None: + async with httpx.AsyncClient() as client: + await AgentRunRunner( + sink=sink, + request=request, + run_id="run-ask-human-turn-1", + plugin_daemon_http_client=client, + ).run() + + first_terminal = sink.events["run-ask-human-turn-1"][-1] + assert isinstance(first_terminal, RunSucceededEvent) + + resumed_request = request.model_copy(deep=True) + resumed_request.session_snapshot = first_terminal.data.session_snapshot + resumed_request.deferred_tool_results = DeferredToolResultsPayload.model_validate( + { + "calls": { + "tool-call-1": { + "status": "submitted", + "action": {"id": "submit", "label": "Submit"}, + "values": {"owner": "ops"}, + } + } + } + ) + + await AgentRunRunner( + sink=sink, + request=resumed_request, + run_id="run-ask-human-turn-2", + plugin_daemon_http_client=client, + ).run() + + asyncio.run(scenario()) + + second_terminal = sink.events["run-ask-human-turn-2"][-1] + assert isinstance(second_terminal, RunSucceededEvent) + assert second_terminal.data.output is None + assert second_terminal.data.deferred_tool_call is not None + assert second_terminal.data.deferred_tool_call.tool_call_id == "tool-call-2" + assert seen_user_prompts == ["current user", None] + saved_history = _history_messages_from_snapshot(second_terminal.data.session_snapshot) + assert isinstance(saved_history[1], ModelResponse) + assert saved_history[1].parts == [first_pending_tool_call] + assert isinstance(saved_history[2], ModelRequest) + assert len(saved_history[2].parts) == 1 + assert isinstance(saved_history[2].parts[0], ToolReturnPart) + assert saved_history[2].parts[0].tool_name == "ask_human" + assert saved_history[2].parts[0].tool_call_id == "tool-call-1" + assert saved_history[2].parts[0].content == {"status": "submitted", "values": {"owner": "ops"}} + assert isinstance(saved_history[3], ModelResponse) + assert saved_history[3].parts == [second_pending_tool_call] + + +def test_runner_rejects_deferred_tool_call_without_history_layer(monkeypatch: pytest.MonkeyPatch) -> None: + def fake_get_model(_self: DifyPluginLLMLayer, *, http_client: httpx.AsyncClient): + assert http_client.is_closed is False + return TestModel(custom_output_text="unused") # pyright: ignore[reportReturnType] + + class FakeAgent: + async def run(self, *_args: object, **_kwargs: object) -> FakeAgentRunResult: + return FakeAgentRunResult( + DeferredToolRequests( + calls=[ + ToolCallPart(tool_name="ask_human", args={"question": "Need owner"}, tool_call_id="tool-call-1") + ] + ), + [], + ) + + monkeypatch.setattr(DifyPluginLLMLayer, "get_model", fake_get_model) + monkeypatch.setattr("dify_agent.runtime.runner.create_agent", lambda *args, **kwargs: FakeAgent()) + request = _request("current user", include_history=False, include_ask_human=True) + sink = InMemoryRunEventSink() + + async def scenario() -> None: + async with httpx.AsyncClient() as client: + with pytest.raises( + AgentRunValidationError, + match="ask_human deferred tool requests require a 'history' layer so the pending tool call can be resumed", + ): + await AgentRunRunner( + sink=sink, + request=request, + run_id="run-ask-human-no-history", + plugin_daemon_http_client=client, + ).run() + + asyncio.run(scenario()) + + assert [event.type for event in sink.events["run-ask-human-no-history"]] == ["run_started", "run_failed"] + + +def test_runner_rejects_resume_with_deferred_tool_results_without_history_layer( + monkeypatch: pytest.MonkeyPatch, +) -> None: + agent_run_called = False + + def fake_get_model(_self: DifyPluginLLMLayer, *, http_client: httpx.AsyncClient): + assert http_client.is_closed is False + return TestModel(custom_output_text="unused") # pyright: ignore[reportReturnType] + + class FakeAgent: + async def run(self, *_args: object, **_kwargs: object) -> FakeAgentRunResult: + nonlocal agent_run_called + agent_run_called = True + return FakeAgentRunResult("unexpected", []) + + monkeypatch.setattr(DifyPluginLLMLayer, "get_model", fake_get_model) + monkeypatch.setattr("dify_agent.runtime.runner.create_agent", lambda *args, **kwargs: FakeAgent()) + request = _request("current user", include_history=False, include_ask_human=True) + request.deferred_tool_results = DeferredToolResultsPayload.model_validate( + { + "calls": { + "tool-call-1": { + "status": "submitted", + "action": {"id": "submit", "label": "Submit"}, + "values": {"owner": "ops"}, + } + } + } + ) + sink = InMemoryRunEventSink() + + async def scenario() -> None: + async with httpx.AsyncClient() as client: + with pytest.raises( + AgentRunValidationError, + match="Deferred tool results require a 'history' layer with prior message history", + ): + await AgentRunRunner( + sink=sink, + request=request, + run_id="run-ask-human-resume-no-history", + plugin_daemon_http_client=client, + ).run() + + asyncio.run(scenario()) + + assert agent_run_called is False + assert [event.type for event in sink.events["run-ask-human-resume-no-history"]] == ["run_started", "run_failed"] + + +def test_runner_rejects_multiple_deferred_tool_calls(monkeypatch: pytest.MonkeyPatch) -> None: + def fake_get_model(_self: DifyPluginLLMLayer, *, http_client: httpx.AsyncClient): + assert http_client.is_closed is False + return TestModel(custom_output_text="unused") # pyright: ignore[reportReturnType] + + class FakeAgent: + async def run(self, *_args: object, **_kwargs: object) -> FakeAgentRunResult: + return FakeAgentRunResult( + DeferredToolRequests( + calls=[ + ToolCallPart(tool_name="ask_human", args={"question": "One"}, tool_call_id="tool-call-1"), + ToolCallPart(tool_name="ask_human", args={"question": "Two"}, tool_call_id="tool-call-2"), + ] + ), + [], + ) + + monkeypatch.setattr(DifyPluginLLMLayer, "get_model", fake_get_model) + monkeypatch.setattr("dify_agent.runtime.runner.create_agent", lambda *args, **kwargs: FakeAgent()) + request = _request("current user", include_history=True, include_ask_human=True) + sink = InMemoryRunEventSink() + + async def scenario() -> None: + async with httpx.AsyncClient() as client: + with pytest.raises(ValueError, match="supports exactly one deferred call per run"): + await AgentRunRunner( + sink=sink, + request=request, + run_id="run-ask-human-multi", + plugin_daemon_http_client=client, + ).run() + + asyncio.run(scenario()) + + assert [event.type for event in sink.events["run-ask-human-multi"]] == ["run_started", "run_failed"] + + +def test_runner_rejects_deferred_approval_requests(monkeypatch: pytest.MonkeyPatch) -> None: + def fake_get_model(_self: DifyPluginLLMLayer, *, http_client: httpx.AsyncClient): + assert http_client.is_closed is False + return TestModel(custom_output_text="unused") # pyright: ignore[reportReturnType] + + class FakeAgent: + async def run(self, *_args: object, **_kwargs: object) -> FakeAgentRunResult: + return FakeAgentRunResult( + DeferredToolRequests( + approvals=[ + ToolCallPart( + tool_name="ask_human", args={"question": "Need approval"}, tool_call_id="tool-call-1" + ) + ] + ), + [], + ) + + monkeypatch.setattr(DifyPluginLLMLayer, "get_model", fake_get_model) + monkeypatch.setattr("dify_agent.runtime.runner.create_agent", lambda *args, **kwargs: FakeAgent()) + request = _request("current user", include_history=True, include_ask_human=True) + sink = InMemoryRunEventSink() + + async def scenario() -> None: + async with httpx.AsyncClient() as client: + with pytest.raises(ValueError, match="does not support approval requests"): + await AgentRunRunner( + sink=sink, + request=request, + run_id="run-ask-human-approval", + plugin_daemon_http_client=client, + ).run() + + asyncio.run(scenario()) + + assert [event.type for event in sink.events["run-ask-human-approval"]] == ["run_started", "run_failed"] + + def test_runner_passes_dynamic_dify_plugin_tools_to_agent(monkeypatch: pytest.MonkeyPatch) -> None: seen_tools: list[Tool[object]] = [] diff --git a/dify-agent/tests/local/dify_agent/test_client_safe_exports.py b/dify-agent/tests/local/dify_agent/test_client_safe_exports.py index 7bcf5515935..4c64e6209a6 100644 --- a/dify-agent/tests/local/dify_agent/test_client_safe_exports.py +++ b/dify-agent/tests/local/dify_agent/test_client_safe_exports.py @@ -74,6 +74,7 @@ def test_client_public_exports_work_with_default_dependencies_only(tmp_path: Pat shell_module = importlib.import_module("dify_agent.layers.shell") execution_context_module = importlib.import_module("dify_agent.layers.execution_context") plugin_module = importlib.import_module("dify_agent.layers.dify_plugin") + ask_human_module = importlib.import_module("dify_agent.layers.ask_human") output_module = importlib.import_module("dify_agent.layers.output") assert agenton_layers.ExitIntent is not None @@ -92,6 +93,7 @@ def test_client_public_exports_work_with_default_dependencies_only(tmp_path: Pat assert shell_module.DifyShellLayerConfig is not None assert execution_context_module.DifyExecutionContextLayerConfig is not None assert plugin_module.DifyPluginLLMLayerConfig is not None + assert ask_human_module.DifyAskHumanLayerConfig is not None assert output_module.DifyOutputLayerConfig is not None grpc_error = importlib.import_module("dify_agent.agent_stub.client._errors").AgentStubMissingGRPCDependencyError diff --git a/dify-agent/tests/local/dify_agent/test_import_boundaries.py b/dify-agent/tests/local/dify_agent/test_import_boundaries.py index 19f1c6c7316..b1e0207873f 100644 --- a/dify-agent/tests/local/dify_agent/test_import_boundaries.py +++ b/dify-agent/tests/local/dify_agent/test_import_boundaries.py @@ -80,6 +80,7 @@ def test_protocol_and_dify_plugin_exports_do_not_import_server_only_modules() -> "anthropic", "dify_agent.adapters.llm", "dify_agent.layers.execution_context.layer", + "dify_agent.layers.ask_human.layer", "dify_agent.layers.dify_plugin.llm_layer", "dify_agent.layers.dify_plugin.tools_layer", "dify_agent.layers.output.output_layer", @@ -98,6 +99,7 @@ def test_protocol_and_dify_plugin_exports_do_not_import_server_only_modules() -> imports=[ "dify_agent.protocol", "dify_agent.layers.execution_context", + "dify_agent.layers.ask_human", "dify_agent.layers.dify_plugin", "dify_agent.layers.output", "dify_agent.layers.shell", @@ -105,6 +107,7 @@ def test_protocol_and_dify_plugin_exports_do_not_import_server_only_modules() -> assertions=[ "assert hasattr(dify_agent_protocol, 'PydanticAIStreamRunEvent')", "assert dify_agent_layers_execution_context.__all__ == ['DIFY_EXECUTION_CONTEXT_LAYER_TYPE_ID', 'DifyExecutionContextAgentMode', 'DifyExecutionContextInvokeFrom', 'DifyExecutionContextLayerConfig', 'DifyExecutionContextUserFrom']", + "assert dify_agent_layers_ask_human.__all__ == ['AskHumanAction', 'AskHumanActionStyle', 'AskHumanField', 'AskHumanFieldType', 'AskHumanFileField', 'AskHumanFileListField', 'AskHumanParagraphField', 'AskHumanResultStatus', 'AskHumanSelectField', 'AskHumanSelectOption', 'AskHumanSelectedAction', 'AskHumanToolArgs', 'AskHumanToolResult', 'AskHumanUrgency', 'DEFAULT_ASK_HUMAN_TOOL_DESCRIPTION', 'DIFY_ASK_HUMAN_LAYER_TYPE_ID', 'DifyAskHumanLayerConfig']", "assert dify_agent_layers_dify_plugin.__all__ == ['DIFY_PLUGIN_LLM_LAYER_TYPE_ID', 'DIFY_PLUGIN_TOOLS_LAYER_TYPE_ID', 'DifyPluginCredentialValue', 'DifyPluginLLMLayerConfig', 'DifyPluginToolCredentialType', 'DifyPluginToolConfig', 'DifyPluginToolOption', 'DifyPluginToolParameter', 'DifyPluginToolParameterForm', 'DifyPluginToolParameterType', 'DifyPluginToolsLayerConfig', 'DifyPluginToolValue']", "assert dify_agent_layers_output.__all__ == ['DIFY_OUTPUT_LAYER_TYPE_ID', 'DifyOutputLayerConfig']", "assert dify_agent_layers_shell.__all__ == ['DIFY_SHELL_LAYER_TYPE_ID', 'DifyShellCliToolConfig', 'DifyShellEnvVarConfig', 'DifyShellLayerConfig', 'DifyShellSandboxConfig', 'DifyShellSecretRefConfig']", From 49c97a3f6152d9a5ced1975e95186bd5b2d156e5 Mon Sep 17 00:00:00 2001 From: Jingyi Date: Wed, 10 Jun 2026 20:24:39 -0700 Subject: [PATCH 016/122] fix(web): show plugin auth permission hint (#37310) --- .../__tests__/plugin-auth.spec.tsx | 92 ++++++++++++++++--- .../plugins/plugin-auth/plugin-auth.tsx | 58 ++++++++++-- web/i18n/ar-TN/plugin.json | 3 + web/i18n/de-DE/plugin.json | 3 + web/i18n/en-US/plugin.json | 3 + web/i18n/es-ES/plugin.json | 3 + web/i18n/fa-IR/plugin.json | 3 + web/i18n/fr-FR/plugin.json | 3 + web/i18n/hi-IN/plugin.json | 3 + web/i18n/id-ID/plugin.json | 3 + web/i18n/it-IT/plugin.json | 3 + web/i18n/ja-JP/plugin.json | 3 + web/i18n/ko-KR/plugin.json | 3 + web/i18n/nl-NL/plugin.json | 3 + web/i18n/pl-PL/plugin.json | 3 + web/i18n/pt-BR/plugin.json | 3 + web/i18n/ro-RO/plugin.json | 3 + web/i18n/ru-RU/plugin.json | 3 + web/i18n/sl-SI/plugin.json | 3 + web/i18n/th-TH/plugin.json | 3 + web/i18n/tr-TR/plugin.json | 3 + web/i18n/uk-UA/plugin.json | 3 + web/i18n/vi-VN/plugin.json | 3 + web/i18n/zh-Hans/plugin.json | 3 + web/i18n/zh-Hant/plugin.json | 3 + 25 files changed, 200 insertions(+), 19 deletions(-) diff --git a/web/app/components/plugins/plugin-auth/__tests__/plugin-auth.spec.tsx b/web/app/components/plugins/plugin-auth/__tests__/plugin-auth.spec.tsx index bd30b782d39..5b07c571156 100644 --- a/web/app/components/plugins/plugin-auth/__tests__/plugin-auth.spec.tsx +++ b/web/app/components/plugins/plugin-auth/__tests__/plugin-auth.spec.tsx @@ -1,22 +1,16 @@ -import { cleanup, render, screen } from '@testing-library/react' +import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants' import PluginAuth from '../plugin-auth' import { AuthCategory } from '../types' const mockUsePluginAuth = vi.fn() +const mockSetShowAccountSettingModal = vi.fn() + vi.mock('../hooks/use-plugin-auth', () => ({ usePluginAuth: (...args: unknown[]) => mockUsePluginAuth(...args), })) -vi.mock('../authorize', () => ({ - default: ({ pluginPayload }: { pluginPayload: { provider: string } }) => ( -
- Authorize: - {pluginPayload.provider} -
- ), -})) - vi.mock('../authorized', () => ({ default: ({ pluginPayload }: { pluginPayload: { provider: string } }) => (
@@ -26,6 +20,12 @@ vi.mock('../authorized', () => ({ ), })) +vi.mock('@/context/modal-context', () => ({ + useModalContext: () => ({ + setShowAccountSettingModal: mockSetShowAccountSettingModal, + }), +})) + const defaultPayload = { category: AuthCategory.tool, provider: 'test-provider', @@ -52,7 +52,7 @@ describe('PluginAuth', () => { }) render() - expect(screen.getByTestId('authorize')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'plugin.auth.useApiAuth' })).toBeEnabled() expect(screen.queryByTestId('authorized')).not.toBeInTheDocument() }) @@ -136,4 +136,74 @@ describe('PluginAuth', () => { render() expect(mockUsePluginAuth).toHaveBeenCalledWith(defaultPayload, true) }) + + it('renders permission hint when authorization configuration is disabled by workspace permissions', () => { + mockUsePluginAuth.mockReturnValue({ + isAuthorized: false, + canOAuth: false, + canApiKey: true, + credentials: [], + disabled: true, + invalidPluginCredentialInfo: vi.fn(), + notAllowCustomCredential: false, + }) + + render() + + expect(screen.getByRole('button', { name: 'plugin.auth.useApiAuth' })).toBeDisabled() + expect(screen.getByText('plugin.auth.permissionHint.title')).toBeInTheDocument() + expect(screen.getByText('plugin.auth.permissionHint.description')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'plugin.auth.permissionHint.action' })).toBeInTheDocument() + }) + + it('opens members settings when permission hint action is clicked', () => { + mockUsePluginAuth.mockReturnValue({ + isAuthorized: false, + canOAuth: false, + canApiKey: true, + credentials: [], + disabled: true, + invalidPluginCredentialInfo: vi.fn(), + notAllowCustomCredential: false, + }) + + render() + fireEvent.click(screen.getByRole('button', { name: 'plugin.auth.permissionHint.action' })) + + expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ + payload: ACCOUNT_SETTING_TAB.MEMBERS, + }) + }) + + it('does not render permission hint for datasource authorization', () => { + mockUsePluginAuth.mockReturnValue({ + isAuthorized: false, + canOAuth: false, + canApiKey: true, + credentials: [], + disabled: true, + invalidPluginCredentialInfo: vi.fn(), + notAllowCustomCredential: false, + }) + + render() + + expect(screen.queryByText('plugin.auth.permissionHint.title')).not.toBeInTheDocument() + }) + + it('does not render permission hint when custom credentials are unavailable', () => { + mockUsePluginAuth.mockReturnValue({ + isAuthorized: false, + canOAuth: false, + canApiKey: true, + credentials: [], + disabled: true, + invalidPluginCredentialInfo: vi.fn(), + notAllowCustomCredential: true, + }) + + render() + + expect(screen.queryByText('plugin.auth.permissionHint.title')).not.toBeInTheDocument() + }) }) diff --git a/web/app/components/plugins/plugin-auth/plugin-auth.tsx b/web/app/components/plugins/plugin-auth/plugin-auth.tsx index 2ddb03fae1e..c0d9fe54080 100644 --- a/web/app/components/plugins/plugin-auth/plugin-auth.tsx +++ b/web/app/components/plugins/plugin-auth/plugin-auth.tsx @@ -1,9 +1,13 @@ import type { PluginPayload } from './types' import { cn } from '@langgenius/dify-ui/cn' import { memo } from 'react' +import { useTranslation } from 'react-i18next' +import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants' +import { useModalContext } from '@/context/modal-context' import Authorize from './authorize' import Authorized from './authorized' import { usePluginAuth } from './hooks/use-plugin-auth' +import { AuthCategory } from './types' type PluginAuthProps = { pluginPayload: PluginPayload @@ -15,6 +19,8 @@ const PluginAuth = ({ children, className, }: PluginAuthProps) => { + const { t } = useTranslation() + const { setShowAccountSettingModal } = useModalContext() const { isAuthorized, canOAuth, @@ -24,19 +30,55 @@ const PluginAuth = ({ invalidPluginCredentialInfo, notAllowCustomCredential, } = usePluginAuth(pluginPayload, !!pluginPayload.provider) + const showPermissionHint = !isAuthorized + && disabled + && !notAllowCustomCredential + && pluginPayload.category === AuthCategory.tool + && (canOAuth || canApiKey) + const authorizeContent = ( + + ) return (
{ !isAuthorized && ( - + <> + {authorizeContent} + { + showPermissionHint && ( +
+
+ +
+
+ {t('auth.permissionHint.title', { ns: 'plugin' })} +
+
+ {t('auth.permissionHint.description', { ns: 'plugin' })} +
+
+ +
+
+
+
+ ) + } + ) } { diff --git a/web/i18n/ar-TN/plugin.json b/web/i18n/ar-TN/plugin.json index 648f3a93e64..a784c02e7f0 100644 --- a/web/i18n/ar-TN/plugin.json +++ b/web/i18n/ar-TN/plugin.json @@ -26,6 +26,9 @@ "auth.oauthClientSettings": "إعدادات عميل OAuth", "auth.onlyAtCreationHint": "لا يمكن تحديده مرة أخرى بعد التبديل", "auth.onlyAtCreationHintTooltip": "تم تكوينه بواسطة أعضاء آخرين · لا يمكن اختياره مرة أخرى بعد التبديل.", + "auth.permissionHint.action": "من يمكنه المساعدة؟", + "auth.permissionHint.description": "تتم مشاركة تفويض المكوّن الإضافي عبر مساحة العمل. تواصل مع عضو لديه الأذونات المطلوبة للحصول على المساعدة.", + "auth.permissionHint.title": "ليس لديك إذن لتكوين التفويض.", "auth.personal": "شخصي", "auth.saveAndAuth": "حفظ وتفويض", "auth.saveOnly": "حفظ فقط", diff --git a/web/i18n/de-DE/plugin.json b/web/i18n/de-DE/plugin.json index 0e4a9024c04..80a810160e0 100644 --- a/web/i18n/de-DE/plugin.json +++ b/web/i18n/de-DE/plugin.json @@ -26,6 +26,9 @@ "auth.oauthClientSettings": "OAuth-Client-Einstellungen", "auth.onlyAtCreationHint": "Nach dem Umschalten nicht erneut wählbar", "auth.onlyAtCreationHintTooltip": "Von anderen Mitgliedern konfiguriert · Kann nach dem Wechsel nicht erneut ausgewählt werden.", + "auth.permissionHint.action": "Wer kann helfen?", + "auth.permissionHint.description": "Die Plugin-Autorisierung wird im gesamten Workspace geteilt. Wende dich an ein Mitglied mit den erforderlichen Berechtigungen.", + "auth.permissionHint.title": "Du hast keine Berechtigung, die Autorisierung zu konfigurieren.", "auth.personal": "Persönlich", "auth.saveAndAuth": "Speichern und autorisieren", "auth.saveOnly": "Nur speichern", diff --git a/web/i18n/en-US/plugin.json b/web/i18n/en-US/plugin.json index 26e4be5445b..233d6a75597 100644 --- a/web/i18n/en-US/plugin.json +++ b/web/i18n/en-US/plugin.json @@ -26,6 +26,9 @@ "auth.oauthClientSettings": "OAuth Client Settings", "auth.onlyAtCreationHint": "Cannot be selected again after switching", "auth.onlyAtCreationHintTooltip": "Configured by other members · Cannot be selected again after switching.", + "auth.permissionHint.action": "Who can help?", + "auth.permissionHint.description": "Plugin authorization is shared across the workspace. Contact a member with the required permissions for help.", + "auth.permissionHint.title": "You don’t have permission to configure authorization.", "auth.personal": "Personal", "auth.saveAndAuth": "Save and Authorize", "auth.saveOnly": "Save only", diff --git a/web/i18n/es-ES/plugin.json b/web/i18n/es-ES/plugin.json index ac19d18d2c0..ec77e8c1e97 100644 --- a/web/i18n/es-ES/plugin.json +++ b/web/i18n/es-ES/plugin.json @@ -26,6 +26,9 @@ "auth.oauthClientSettings": "Configuración del cliente OAuth", "auth.onlyAtCreationHint": "No se puede volver a seleccionar después de cambiar", "auth.onlyAtCreationHintTooltip": "Configurado por otros miembros · No se puede volver a seleccionar después de cambiar.", + "auth.permissionHint.action": "¿Quién puede ayudar?", + "auth.permissionHint.description": "La autorización del plugin se comparte en todo el espacio de trabajo. Contacta con un miembro que tenga los permisos necesarios.", + "auth.permissionHint.title": "No tienes permiso para configurar la autorización.", "auth.personal": "personales", "auth.saveAndAuth": "Guardar y autorizar", "auth.saveOnly": "Guardar solo", diff --git a/web/i18n/fa-IR/plugin.json b/web/i18n/fa-IR/plugin.json index bc21dd9255d..b00896bd026 100644 --- a/web/i18n/fa-IR/plugin.json +++ b/web/i18n/fa-IR/plugin.json @@ -26,6 +26,9 @@ "auth.oauthClientSettings": "تنظیمات کلاینت اوتور", "auth.onlyAtCreationHint": "بعد از تعویض نمی توان دوباره انتخاب کرد", "auth.onlyAtCreationHintTooltip": "پیکربندی شده توسط سایر اعضا · پس از تعویض دوباره انتخاب نمی شود.", + "auth.permissionHint.action": "چه کسی می‌تواند کمک کند؟", + "auth.permissionHint.description": "مجوزدهی افزونه در سراسر فضای کاری مشترک است. برای دریافت کمک با عضوی که مجوزهای لازم را دارد تماس بگیرید.", + "auth.permissionHint.title": "شما اجازه پیکربندی مجوزدهی را ندارید.", "auth.personal": "شخصی", "auth.saveAndAuth": "ذخیره و تأیید", "auth.saveOnly": "فقط ذخیره کنید", diff --git a/web/i18n/fr-FR/plugin.json b/web/i18n/fr-FR/plugin.json index fd153fe3ff2..7a645f65193 100644 --- a/web/i18n/fr-FR/plugin.json +++ b/web/i18n/fr-FR/plugin.json @@ -26,6 +26,9 @@ "auth.oauthClientSettings": "Paramètres du client OAuth", "auth.onlyAtCreationHint": "Ne peut pas être sélectionné après le changement", "auth.onlyAtCreationHintTooltip": "Configuré par d'autres membres · Ne peut pas être sélectionné après le changement.", + "auth.permissionHint.action": "Qui peut aider ?", + "auth.permissionHint.description": "L’autorisation du plugin est partagée dans tout l’espace de travail. Contactez un membre disposant des autorisations requises.", + "auth.permissionHint.title": "Vous n’avez pas l’autorisation de configurer l’autorisation.", "auth.personal": "Personnel", "auth.saveAndAuth": "Enregistrer et autoriser", "auth.saveOnly": "Sauvegarder seulement", diff --git a/web/i18n/hi-IN/plugin.json b/web/i18n/hi-IN/plugin.json index f88c97dd7f5..d0cf6609e16 100644 --- a/web/i18n/hi-IN/plugin.json +++ b/web/i18n/hi-IN/plugin.json @@ -26,6 +26,9 @@ "auth.oauthClientSettings": "OAuth क्लाइंट सेटिंग्स", "auth.onlyAtCreationHint": "स्विच करने के बाद दोबारा चयन नहीं किया जा सकता", "auth.onlyAtCreationHintTooltip": "अन्य सदस्यों द्वारा कॉन्फ़िगर किया गया · स्विच करने के बाद दोबारा नहीं चुना जा सकता।", + "auth.permissionHint.action": "कौन मदद कर सकता है?", + "auth.permissionHint.description": "प्लगइन authorization पूरे workspace में साझा होता है। मदद के लिए आवश्यक अनुमतियों वाले सदस्य से संपर्क करें।", + "auth.permissionHint.title": "आपके पास authorization कॉन्फ़िगर करने की अनुमति नहीं है।", "auth.personal": "निजी", "auth.saveAndAuth": "सहेजें और अधिकृत करें", "auth.saveOnly": "बस सहेजें", diff --git a/web/i18n/id-ID/plugin.json b/web/i18n/id-ID/plugin.json index 13690f99007..5045d3b5cfa 100644 --- a/web/i18n/id-ID/plugin.json +++ b/web/i18n/id-ID/plugin.json @@ -26,6 +26,9 @@ "auth.oauthClientSettings": "Pengaturan Klien OAuth", "auth.onlyAtCreationHint": "Tidak dapat dipilih lagi setelah peralihan", "auth.onlyAtCreationHintTooltip": "Dikonfigurasi oleh anggota lain · Tidak dapat dipilih lagi setelah beralih.", + "auth.permissionHint.action": "Siapa yang bisa membantu?", + "auth.permissionHint.description": "Otorisasi plugin dibagikan di seluruh workspace. Hubungi anggota dengan izin yang diperlukan untuk mendapatkan bantuan.", + "auth.permissionHint.title": "Anda tidak memiliki izin untuk mengonfigurasi otorisasi.", "auth.personal": "Pribadi", "auth.saveAndAuth": "Simpan dan Otorisasi", "auth.saveOnly": "Hanya Hemat", diff --git a/web/i18n/it-IT/plugin.json b/web/i18n/it-IT/plugin.json index 1c8001155db..d4c6609909c 100644 --- a/web/i18n/it-IT/plugin.json +++ b/web/i18n/it-IT/plugin.json @@ -26,6 +26,9 @@ "auth.oauthClientSettings": "Impostazioni del client OAuth", "auth.onlyAtCreationHint": "Non può essere selezionato nuovamente dopo il passaggio", "auth.onlyAtCreationHintTooltip": "Configurato da altri membri · Non può essere selezionato nuovamente dopo il passaggio.", + "auth.permissionHint.action": "Chi può aiutare?", + "auth.permissionHint.description": "L’autorizzazione del plugin è condivisa in tutto il workspace. Contatta un membro con le autorizzazioni richieste per ricevere aiuto.", + "auth.permissionHint.title": "Non hai l’autorizzazione per configurare l’autorizzazione.", "auth.personal": "Personale", "auth.saveAndAuth": "Salva e Autorizza", "auth.saveOnly": "Salva solo", diff --git a/web/i18n/ja-JP/plugin.json b/web/i18n/ja-JP/plugin.json index de2705687da..8f21133b510 100644 --- a/web/i18n/ja-JP/plugin.json +++ b/web/i18n/ja-JP/plugin.json @@ -26,6 +26,9 @@ "auth.oauthClientSettings": "OAuthクライアント設定", "auth.onlyAtCreationHint": "切り替え後は再度選択できません", "auth.onlyAtCreationHintTooltip": "他のメンバーが設定 ・切り替え後は再度選択できません。", + "auth.permissionHint.action": "誰に相談できますか?", + "auth.permissionHint.description": "プラグインの認可はワークスペース全体で共有されます。必要な権限を持つメンバーに相談してください。", + "auth.permissionHint.title": "認可を設定する権限がありません。", "auth.personal": "個人的な", "auth.saveAndAuth": "保存と承認", "auth.saveOnly": "保存のみ", diff --git a/web/i18n/ko-KR/plugin.json b/web/i18n/ko-KR/plugin.json index 868e7207ea3..7e202a67ba7 100644 --- a/web/i18n/ko-KR/plugin.json +++ b/web/i18n/ko-KR/plugin.json @@ -26,6 +26,9 @@ "auth.oauthClientSettings": "OAuth 클라이언트 설정", "auth.onlyAtCreationHint": "전환 후에는 다시 선택할 수 없습니다", "auth.onlyAtCreationHintTooltip": "다른 구성원이 구성 · 전환 후 다시 선택할 수 없습니다.", + "auth.permissionHint.action": "누가 도와줄 수 있나요?", + "auth.permissionHint.description": "플러그인 인증은 워크스페이스 전체에서 공유됩니다. 필요한 권한이 있는 멤버에게 도움을 요청하세요.", + "auth.permissionHint.title": "인증을 구성할 권한이 없습니다.", "auth.personal": "개인", "auth.saveAndAuth": "저장하고 승인하세요", "auth.saveOnly": "저장만 하기", diff --git a/web/i18n/nl-NL/plugin.json b/web/i18n/nl-NL/plugin.json index 3ba084b80df..723477771da 100644 --- a/web/i18n/nl-NL/plugin.json +++ b/web/i18n/nl-NL/plugin.json @@ -26,6 +26,9 @@ "auth.oauthClientSettings": "OAuth Client Settings", "auth.onlyAtCreationHint": "Na overschakeling niet meer selecteerbaar", "auth.onlyAtCreationHintTooltip": "Geconfigureerd door andere leden · Kan na het overstappen niet meer geselecteerd worden.", + "auth.permissionHint.action": "Wie kan helpen?", + "auth.permissionHint.description": "Plugin-autorisatie wordt gedeeld in de hele workspace. Neem contact op met een lid met de vereiste rechten voor hulp.", + "auth.permissionHint.title": "Je hebt geen toestemming om autorisatie te configureren.", "auth.personal": "Persoonlijk", "auth.saveAndAuth": "Save and Authorize", "auth.saveOnly": "Save only", diff --git a/web/i18n/pl-PL/plugin.json b/web/i18n/pl-PL/plugin.json index df894e9377b..d577b331976 100644 --- a/web/i18n/pl-PL/plugin.json +++ b/web/i18n/pl-PL/plugin.json @@ -26,6 +26,9 @@ "auth.oauthClientSettings": "Ustawienia klienta OAuth", "auth.onlyAtCreationHint": "Po przełączeniu nie można wybrać ponownie", "auth.onlyAtCreationHintTooltip": "Skonfigurowane przez innych użytkowników · Nie można wybrać ponownie po przełączeniu.", + "auth.permissionHint.action": "Kto może pomóc?", + "auth.permissionHint.description": "Autoryzacja wtyczki jest współdzielona w całym workspace. Skontaktuj się z członkiem mającym wymagane uprawnienia.", + "auth.permissionHint.title": "Nie masz uprawnień do konfiguracji autoryzacji.", "auth.personal": "Osobiste", "auth.saveAndAuth": "Zapisz i autoryzuj", "auth.saveOnly": "Zapisz tylko", diff --git a/web/i18n/pt-BR/plugin.json b/web/i18n/pt-BR/plugin.json index d1cc5c5e048..681ec3d3451 100644 --- a/web/i18n/pt-BR/plugin.json +++ b/web/i18n/pt-BR/plugin.json @@ -26,6 +26,9 @@ "auth.oauthClientSettings": "Configurações do Cliente OAuth", "auth.onlyAtCreationHint": "Não pode ser selecionado novamente após a troca", "auth.onlyAtCreationHintTooltip": "Configurado por outros membros · Não pode ser selecionado novamente após a troca.", + "auth.permissionHint.action": "Quem pode ajudar?", + "auth.permissionHint.description": "A autorização do plugin é compartilhada em todo o workspace. Entre em contato com um membro com as permissões necessárias para obter ajuda.", + "auth.permissionHint.title": "Você não tem permissão para configurar a autorização.", "auth.personal": "Pessoal", "auth.saveAndAuth": "Salvar e Autorizar", "auth.saveOnly": "Salvar apenas", diff --git a/web/i18n/ro-RO/plugin.json b/web/i18n/ro-RO/plugin.json index a3445d43076..6be16ffa66e 100644 --- a/web/i18n/ro-RO/plugin.json +++ b/web/i18n/ro-RO/plugin.json @@ -26,6 +26,9 @@ "auth.oauthClientSettings": "Setările clientului OAuth", "auth.onlyAtCreationHint": "Nu poate fi selectat din nou după comutare", "auth.onlyAtCreationHintTooltip": "Configurat de alți membri · Nu poate fi selectat din nou după comutare.", + "auth.permissionHint.action": "Cine poate ajuta?", + "auth.permissionHint.description": "Autorizarea pluginului este partajată în întregul workspace. Contactează un membru cu permisiunile necesare pentru ajutor.", + "auth.permissionHint.title": "Nu ai permisiunea de a configura autorizarea.", "auth.personal": "Personalizat", "auth.saveAndAuth": "Salvează și Autorizează", "auth.saveOnly": "Salvează doar", diff --git a/web/i18n/ru-RU/plugin.json b/web/i18n/ru-RU/plugin.json index 023a6b9d7fe..34c8341e942 100644 --- a/web/i18n/ru-RU/plugin.json +++ b/web/i18n/ru-RU/plugin.json @@ -26,6 +26,9 @@ "auth.oauthClientSettings": "Настройки клиента OAuth", "auth.onlyAtCreationHint": "Невозможно выбрать снова после переключения", "auth.onlyAtCreationHintTooltip": "Настроено другими участниками · Невозможно выбрать повторно после переключения.", + "auth.permissionHint.action": "Кто может помочь?", + "auth.permissionHint.description": "Авторизация плагина действует для всего рабочего пространства. Обратитесь за помощью к участнику с нужными правами.", + "auth.permissionHint.title": "У вас нет разрешения на настройку авторизации.", "auth.personal": "Персональный", "auth.saveAndAuth": "Сохранить и авторизовать", "auth.saveOnly": "Сохранить только", diff --git a/web/i18n/sl-SI/plugin.json b/web/i18n/sl-SI/plugin.json index b77b05504b8..50d1ab9e749 100644 --- a/web/i18n/sl-SI/plugin.json +++ b/web/i18n/sl-SI/plugin.json @@ -26,6 +26,9 @@ "auth.oauthClientSettings": "Nastavitve odjemalca OAuth", "auth.onlyAtCreationHint": "Po preklopu ga ni mogoče znova izbrati", "auth.onlyAtCreationHintTooltip": "Konfigurirali drugi člani · Po zamenjavi ni mogoče znova izbrati.", + "auth.permissionHint.action": "Kdo lahko pomaga?", + "auth.permissionHint.description": "Avtorizacija vtičnika je v skupni rabi v celotnem workspaceu. Za pomoč se obrnite na člana z zahtevanimi dovoljenji.", + "auth.permissionHint.title": "Nimate dovoljenja za konfiguracijo avtorizacije.", "auth.personal": "Osebno", "auth.saveAndAuth": "Shrani in pooblasti", "auth.saveOnly": "Shrani samo", diff --git a/web/i18n/th-TH/plugin.json b/web/i18n/th-TH/plugin.json index d3813222dcb..a5acf0e7479 100644 --- a/web/i18n/th-TH/plugin.json +++ b/web/i18n/th-TH/plugin.json @@ -26,6 +26,9 @@ "auth.oauthClientSettings": "การตั้งค่าไคลเอนต์ OAuth", "auth.onlyAtCreationHint": "ไม่สามารถเลือกได้อีกหลังจากเปลี่ยนแล้ว", "auth.onlyAtCreationHintTooltip": "กำหนดค่าโดยสมาชิกรายอื่น · ไม่สามารถเลือกได้อีกหลังจากเปลี่ยนแล้ว", + "auth.permissionHint.action": "ใครช่วยได้บ้าง?", + "auth.permissionHint.description": "การอนุญาตของปลั๊กอินใช้ร่วมกันทั้ง workspace ติดต่อสมาชิกที่มีสิทธิ์ที่จำเป็นเพื่อขอความช่วยเหลือ", + "auth.permissionHint.title": "คุณไม่มีสิทธิ์กำหนดค่าการอนุญาต", "auth.personal": "ส่วนตัว", "auth.saveAndAuth": "บันทึกและอนุญาต", "auth.saveOnly": "บันทึกเฉพาะ", diff --git a/web/i18n/tr-TR/plugin.json b/web/i18n/tr-TR/plugin.json index e85d54a16f3..e4db0c83577 100644 --- a/web/i18n/tr-TR/plugin.json +++ b/web/i18n/tr-TR/plugin.json @@ -26,6 +26,9 @@ "auth.oauthClientSettings": "OAuth İstemci Ayarları", "auth.onlyAtCreationHint": "Geçişten sonra tekrar seçilemez", "auth.onlyAtCreationHintTooltip": "Diğer üyeler tarafından yapılandırıldı · Geçişten sonra tekrar seçilemez.", + "auth.permissionHint.action": "Kim yardımcı olabilir?", + "auth.permissionHint.description": "Eklenti yetkilendirmesi tüm workspace genelinde paylaşılır. Yardım için gerekli izinlere sahip bir üyeyle iletişime geçin.", + "auth.permissionHint.title": "Yetkilendirmeyi yapılandırma izniniz yok.", "auth.personal": "Kişisel", "auth.saveAndAuth": "Kaydet ve Yetkilendir", "auth.saveOnly": "Sadece kaydet", diff --git a/web/i18n/uk-UA/plugin.json b/web/i18n/uk-UA/plugin.json index be9267c9747..8535a7384ae 100644 --- a/web/i18n/uk-UA/plugin.json +++ b/web/i18n/uk-UA/plugin.json @@ -26,6 +26,9 @@ "auth.oauthClientSettings": "Налаштування клієнта OAuth", "auth.onlyAtCreationHint": "Не можна вибрати знову після перемикання", "auth.onlyAtCreationHintTooltip": "Налаштовано іншими учасниками · Неможливо вибрати знову після перемикання.", + "auth.permissionHint.action": "Хто може допомогти?", + "auth.permissionHint.description": "Авторизація плагіна спільна для всього workspace. Зверніться по допомогу до учасника з потрібними дозволами.", + "auth.permissionHint.title": "У вас немає дозволу налаштовувати авторизацію.", "auth.personal": "Особистий", "auth.saveAndAuth": "Зберегти та авторизувати", "auth.saveOnly": "Зберегти лише", diff --git a/web/i18n/vi-VN/plugin.json b/web/i18n/vi-VN/plugin.json index c0339b91c56..0ed0c7c36e4 100644 --- a/web/i18n/vi-VN/plugin.json +++ b/web/i18n/vi-VN/plugin.json @@ -26,6 +26,9 @@ "auth.oauthClientSettings": "Cài đặt khách hàng OAuth", "auth.onlyAtCreationHint": "Không thể chọn lại sau khi chuyển đổi", "auth.onlyAtCreationHintTooltip": "Được cấu hình bởi các thành viên khác. Không thể chọn lại sau khi chuyển đổi.", + "auth.permissionHint.action": "Ai có thể giúp?", + "auth.permissionHint.description": "Ủy quyền plugin được chia sẻ trong toàn bộ workspace. Liên hệ với thành viên có quyền cần thiết để được trợ giúp.", + "auth.permissionHint.title": "Bạn không có quyền cấu hình ủy quyền.", "auth.personal": "Riêng tư", "auth.saveAndAuth": "Lưu và Xác nhận", "auth.saveOnly": "Chỉ lưu lại", diff --git a/web/i18n/zh-Hans/plugin.json b/web/i18n/zh-Hans/plugin.json index 619a5657986..02f9b2b2b05 100644 --- a/web/i18n/zh-Hans/plugin.json +++ b/web/i18n/zh-Hans/plugin.json @@ -26,6 +26,9 @@ "auth.oauthClientSettings": "OAuth 客户端设置", "auth.onlyAtCreationHint": "切换后不可再选回", "auth.onlyAtCreationHintTooltip": "由其他成员配置 · 切换后不可再选回", + "auth.permissionHint.action": "谁可以帮忙?", + "auth.permissionHint.description": "插件授权在整个工作区共享。请联系拥有所需权限的成员获取帮助。", + "auth.permissionHint.title": "你没有配置授权的权限。", "auth.personal": "个人", "auth.saveAndAuth": "保存并授权", "auth.saveOnly": "仅保存", diff --git a/web/i18n/zh-Hant/plugin.json b/web/i18n/zh-Hant/plugin.json index a850f747b8f..28ce7916593 100644 --- a/web/i18n/zh-Hant/plugin.json +++ b/web/i18n/zh-Hant/plugin.json @@ -26,6 +26,9 @@ "auth.oauthClientSettings": "OAuth 客戶端設置", "auth.onlyAtCreationHint": "切換後無法再次選擇", "auth.onlyAtCreationHintTooltip": "由其他成員配置 · 切換後無法再次選擇。", + "auth.permissionHint.action": "誰可以幫忙?", + "auth.permissionHint.description": "外掛授權會在整個工作區共用。請聯絡擁有所需權限的成員取得協助。", + "auth.permissionHint.title": "你沒有設定授權的權限。", "auth.personal": "個人的", "auth.saveAndAuth": "保存並授權", "auth.saveOnly": "僅保存", From d3977cea772d1de1b5034e43427d99e5e8c80480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9B=90=E7=B2=92=20Yanli?= Date: Thu, 11 Jun 2026 14:18:43 +0900 Subject: [PATCH 017/122] fix(api): handle agent deferred tool events (#37319) --- api/clients/agent_backend/__init__.py | 4 +- api/clients/agent_backend/event_adapter.py | 57 ++++++++---- api/clients/agent_backend/fake_client.py | 23 +++-- .../workflow/nodes/agent_v2/agent_node.py | 15 ++- .../workflow/nodes/agent_v2/output_adapter.py | 14 +-- .../agent_backend/test_event_adapter.py | 91 +++++++++++++++++-- .../clients/agent_backend/test_fake_client.py | 12 +-- .../nodes/agent_v2/test_output_adapter.py | 19 ---- 8 files changed, 154 insertions(+), 81 deletions(-) diff --git a/api/clients/agent_backend/__init__.py b/api/clients/agent_backend/__init__.py index bbee164d5cb..f8d15f85676 100644 --- a/api/clients/agent_backend/__init__.py +++ b/api/clients/agent_backend/__init__.py @@ -16,12 +16,12 @@ from clients.agent_backend.errors import ( AgentBackendValidationError, ) from clients.agent_backend.event_adapter import ( + AgentBackendDeferredToolCallInternalEvent, AgentBackendInternalEvent, AgentBackendInternalEventType, AgentBackendRunCancelledInternalEvent, AgentBackendRunEventAdapter, AgentBackendRunFailedInternalEvent, - AgentBackendRunPausedInternalEvent, AgentBackendRunStartedInternalEvent, AgentBackendRunSucceededInternalEvent, AgentBackendStreamInternalEvent, @@ -51,6 +51,7 @@ __all__ = [ "WORKFLOW_NODE_JOB_PROMPT_LAYER_ID", "WORKFLOW_USER_PROMPT_LAYER_ID", "AgentBackendAgentAppRunInput", + "AgentBackendDeferredToolCallInternalEvent", "AgentBackendError", "AgentBackendHTTPError", "AgentBackendInternalEvent", @@ -63,7 +64,6 @@ __all__ = [ "AgentBackendRunEventAdapter", "AgentBackendRunFailedError", "AgentBackendRunFailedInternalEvent", - "AgentBackendRunPausedInternalEvent", "AgentBackendRunRequestBuilder", "AgentBackendRunStartedInternalEvent", "AgentBackendRunSucceededInternalEvent", diff --git a/api/clients/agent_backend/event_adapter.py b/api/clients/agent_backend/event_adapter.py index 02b30e6c6b3..e983aa336d7 100644 --- a/api/clients/agent_backend/event_adapter.py +++ b/api/clients/agent_backend/event_adapter.py @@ -2,7 +2,9 @@ The adapter does not define a new cross-service event contract. It consumes ``dify_agent.protocol.RunEvent`` and produces small API-internal models that the -future workflow Agent Node can map to Graphon/AppQueue events in phase 3. +workflow Agent Node maps to Graphon/AppQueue events. Deferred external tool calls +remain Dify Agent ``run_succeeded`` payloads on the wire; API code turns them +into an internal event so workflow pause/session handling stays local to API. """ from __future__ import annotations @@ -12,11 +14,11 @@ from typing import Annotated, Literal, cast from agenton.compositor import CompositorSessionSnapshot from dify_agent.protocol import ( + DeferredToolCallPayload, PydanticAIStreamRunEvent, RunCancelledEvent, RunEvent, RunFailedEvent, - RunPausedEvent, RunStartedEvent, RunSucceededEvent, ) @@ -30,7 +32,7 @@ class AgentBackendInternalEventType(StrEnum): RUN_STARTED = "run_started" STREAM_EVENT = "stream_event" - RUN_PAUSED = "run_paused" + DEFERRED_TOOL_CALL = "deferred_tool_call" RUN_SUCCEEDED = "run_succeeded" RUN_FAILED = "run_failed" RUN_CANCELLED = "run_cancelled" @@ -67,13 +69,13 @@ class AgentBackendRunSucceededInternalEvent(AgentBackendInternalEventBase): session_snapshot: CompositorSessionSnapshot -class AgentBackendRunPausedInternalEvent(AgentBackendInternalEventBase): - """API-internal resumable pause event for human handoff and Babysit flows.""" +class AgentBackendDeferredToolCallInternalEvent(AgentBackendInternalEventBase): + """API-internal representation of a Dify Agent deferred external tool call.""" - type: Literal[AgentBackendInternalEventType.RUN_PAUSED] = AgentBackendInternalEventType.RUN_PAUSED - reason: str + type: Literal[AgentBackendInternalEventType.DEFERRED_TOOL_CALL] = AgentBackendInternalEventType.DEFERRED_TOOL_CALL + deferred_tool_call: DeferredToolCallPayload message: str | None = None - session_snapshot: CompositorSessionSnapshot | None = None + session_snapshot: CompositorSessionSnapshot class AgentBackendRunFailedInternalEvent(AgentBackendInternalEventBase): @@ -95,7 +97,7 @@ class AgentBackendRunCancelledInternalEvent(AgentBackendInternalEventBase): type AgentBackendInternalEvent = Annotated[ AgentBackendRunStartedInternalEvent | AgentBackendStreamInternalEvent - | AgentBackendRunPausedInternalEvent + | AgentBackendDeferredToolCallInternalEvent | AgentBackendRunSucceededInternalEvent | AgentBackendRunFailedInternalEvent | AgentBackendRunCancelledInternalEvent, @@ -128,6 +130,18 @@ class AgentBackendRunEventAdapter: ) ] case RunSucceededEvent(): + if "deferred_tool_call" in event.data.model_fields_set: + if event.data.deferred_tool_call is None: + raise TypeError("run_succeeded deferred_tool_call branch is missing payload") + return [ + AgentBackendDeferredToolCallInternalEvent( + run_id=event.run_id, + source_event_id=event.id, + deferred_tool_call=event.data.deferred_tool_call, + message=_deferred_tool_call_message(event.data.deferred_tool_call), + session_snapshot=event.data.session_snapshot, + ) + ] return [ AgentBackendRunSucceededInternalEvent( run_id=event.run_id, @@ -136,16 +150,6 @@ class AgentBackendRunEventAdapter: session_snapshot=event.data.session_snapshot, ) ] - case RunPausedEvent(): - return [ - AgentBackendRunPausedInternalEvent( - run_id=event.run_id, - source_event_id=event.id, - reason=event.data.reason, - message=event.data.message, - session_snapshot=event.data.session_snapshot, - ) - ] case RunFailedEvent(): return [ AgentBackendRunFailedInternalEvent( @@ -165,3 +169,18 @@ class AgentBackendRunEventAdapter: ) ] raise TypeError(f"unsupported agent backend run event: {type(event).__name__}") + + +def _deferred_tool_call_message(payload: DeferredToolCallPayload) -> str: + """Return a concise workflow pause message from deferred-tool arguments.""" + args = payload.args + if isinstance(args, dict): + question = args.get("question") + if isinstance(question, str) and question.strip(): + return question + + title = args.get("title") + if isinstance(title, str) and title.strip(): + return title + + return f"Agent backend requested external input via deferred tool '{payload.tool_name}'." diff --git a/api/clients/agent_backend/fake_client.py b/api/clients/agent_backend/fake_client.py index a768777039d..11de90c94b7 100644 --- a/api/clients/agent_backend/fake_client.py +++ b/api/clients/agent_backend/fake_client.py @@ -17,11 +17,10 @@ from dify_agent.protocol import ( CancelRunResponse, CreateRunRequest, CreateRunResponse, + DeferredToolCallPayload, RunEvent, RunFailedEvent, RunFailedEventData, - RunPausedEvent, - RunPausedEventData, RunStartedEvent, RunStatusResponse, RunSucceededEvent, @@ -32,7 +31,11 @@ _FIXED_TIME = datetime(2026, 1, 1, tzinfo=UTC) class FakeAgentBackendScenario(StrEnum): - """Deterministic fake scenarios for API-side integration tests.""" + """Deterministic fake scenarios for API-side integration tests. + + ``PAUSED`` represents the API workflow effect. On the Dify Agent wire + protocol it is a succeeded run carrying a deferred external tool call. + """ SUCCESS = "success" FAILED = "failed" @@ -95,7 +98,7 @@ class FakeAgentBackendRunClient: case FakeAgentBackendScenario.PAUSED: return RunStatusResponse( run_id=run_id, - status="paused", + status="succeeded", created_at=_FIXED_TIME, updated_at=_FIXED_TIME, ) @@ -128,13 +131,17 @@ class FakeAgentBackendRunClient: case FakeAgentBackendScenario.PAUSED: return ( RunStartedEvent(id="1-0", run_id=run_id, created_at=_FIXED_TIME), - RunPausedEvent( + RunSucceededEvent( id="2-0", run_id=run_id, created_at=_FIXED_TIME, - data=RunPausedEventData( - reason="human_input_required", - message="Agent requested human input.", + data=RunSucceededEventData( + deferred_tool_call=DeferredToolCallPayload( + tool_call_id="fake-ask-human-1", + tool_name="ask_human", + args={"question": "Agent requested human input."}, + metadata={"layer_type": "dify.ask_human", "schema_version": 1}, + ), session_snapshot=CompositorSessionSnapshot(layers=[]), ), ), diff --git a/api/core/workflow/nodes/agent_v2/agent_node.py b/api/core/workflow/nodes/agent_v2/agent_node.py index bdcbb776267..f15f431dc80 100644 --- a/api/core/workflow/nodes/agent_v2/agent_node.py +++ b/api/core/workflow/nodes/agent_v2/agent_node.py @@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Any, override from agenton.compositor import CompositorSessionSnapshot from clients.agent_backend import ( + AgentBackendDeferredToolCallInternalEvent, AgentBackendError, AgentBackendHTTPError, AgentBackendInternalEventType, @@ -14,7 +15,6 @@ from clients.agent_backend import ( AgentBackendRunClient, AgentBackendRunEventAdapter, AgentBackendRunFailedInternalEvent, - AgentBackendRunPausedInternalEvent, AgentBackendRunSucceededInternalEvent, AgentBackendStreamError, AgentBackendStreamInternalEvent, @@ -62,7 +62,7 @@ _TerminalAgentBackendEvent = ( AgentBackendRunSucceededInternalEvent | AgentBackendRunFailedInternalEvent | AgentBackendRunCancelledInternalEvent - | AgentBackendRunPausedInternalEvent + | AgentBackendDeferredToolCallInternalEvent ) @@ -250,7 +250,7 @@ class DifyAgentNode(Node[DifyAgentNodeData]): ) return - if isinstance(terminal_event, AgentBackendRunPausedInternalEvent): + if isinstance(terminal_event, AgentBackendDeferredToolCallInternalEvent): self._save_session_snapshot( session_scope=session_scope, backend_run_id=terminal_event.run_id, @@ -394,16 +394,15 @@ class DifyAgentNode(Node[DifyAgentNodeData]): **dict(metadata.get("agent_backend") or {}), "stream_event_count": stream_event_count, } - # Narrow to the 4 known terminal event types so the caller - # can hand the result to ``build_failure_result`` (which is - # typed against the union). Anything else is a protocol- - # level surprise we surface as a stream error. + # Narrow to the known terminal event types before returning + # to the caller. Deferred-tool events are terminal on the + # Dify Agent wire, then converted into workflow pause locally. if isinstance( internal_event, AgentBackendRunSucceededInternalEvent | AgentBackendRunFailedInternalEvent | AgentBackendRunCancelledInternalEvent - | AgentBackendRunPausedInternalEvent, + | AgentBackendDeferredToolCallInternalEvent, ): return internal_event, None return None, self._failure_event( diff --git a/api/core/workflow/nodes/agent_v2/output_adapter.py b/api/core/workflow/nodes/agent_v2/output_adapter.py index b679c409bac..0fae0105348 100644 --- a/api/core/workflow/nodes/agent_v2/output_adapter.py +++ b/api/core/workflow/nodes/agent_v2/output_adapter.py @@ -4,11 +4,11 @@ from collections.abc import Mapping, Sequence from typing import Any, Protocol from clients.agent_backend import ( + AgentBackendDeferredToolCallInternalEvent, AgentBackendInternalEvent, AgentBackendInternalEventType, AgentBackendRunCancelledInternalEvent, AgentBackendRunFailedInternalEvent, - AgentBackendRunPausedInternalEvent, AgentBackendRunSucceededInternalEvent, ) from core.app.file_access import DatabaseFileAccessController @@ -85,11 +85,7 @@ class WorkflowAgentOutputAdapter: def build_failure_result( self, *, - event: ( - AgentBackendRunFailedInternalEvent - | AgentBackendRunCancelledInternalEvent - | AgentBackendRunPausedInternalEvent - ), + event: (AgentBackendRunFailedInternalEvent | AgentBackendRunCancelledInternalEvent), inputs: dict[str, Any], process_data: dict[str, Any], metadata: dict[str, Any], @@ -108,10 +104,6 @@ class WorkflowAgentOutputAdapter: error = event.message or "Agent backend run was cancelled." error_type = "agent_backend_run_cancelled" terminal_status = "cancelled" - case AgentBackendRunPausedInternalEvent(): - error = event.message or "Agent backend run paused, but workflow Agent Node pause is not supported yet." - error_type = "agent_backend_paused_unsupported" - terminal_status = "paused" metadata = self._with_terminal_metadata(metadata, event, terminal_status) usage = self._usage_from_metadata(metadata) @@ -339,7 +331,7 @@ class WorkflowAgentOutputAdapter: } ) session_snapshot = None - if isinstance(event, AgentBackendRunSucceededInternalEvent | AgentBackendRunPausedInternalEvent): + if isinstance(event, AgentBackendRunSucceededInternalEvent | AgentBackendDeferredToolCallInternalEvent): session_snapshot = event.session_snapshot if session_snapshot is not None: agent_backend["session_snapshot"] = { diff --git a/api/tests/unit_tests/clients/agent_backend/test_event_adapter.py b/api/tests/unit_tests/clients/agent_backend/test_event_adapter.py index 79f7d14d31e..ab88ba19640 100644 --- a/api/tests/unit_tests/clients/agent_backend/test_event_adapter.py +++ b/api/tests/unit_tests/clients/agent_backend/test_event_adapter.py @@ -1,12 +1,12 @@ +import pytest from agenton.compositor import CompositorSessionSnapshot from dify_agent.protocol import ( + DeferredToolCallPayload, PydanticAIStreamRunEvent, RunCancelledEvent, RunCancelledEventData, RunFailedEvent, RunFailedEventData, - RunPausedEvent, - RunPausedEventData, RunStartedEvent, RunSucceededEvent, RunSucceededEventData, @@ -14,11 +14,11 @@ from dify_agent.protocol import ( from pydantic_ai.messages import FinalResultEvent from clients.agent_backend import ( + AgentBackendDeferredToolCallInternalEvent, AgentBackendInternalEventType, AgentBackendRunCancelledInternalEvent, AgentBackendRunEventAdapter, AgentBackendRunFailedInternalEvent, - AgentBackendRunPausedInternalEvent, AgentBackendRunStartedInternalEvent, AgentBackendRunSucceededInternalEvent, AgentBackendStreamInternalEvent, @@ -92,27 +92,102 @@ def test_event_adapter_maps_run_failed_to_failed_result(): ] -def test_event_adapter_maps_run_paused_to_resumable_pause(): +def test_event_adapter_maps_deferred_tool_call_success_to_internal_event(): snapshot = CompositorSessionSnapshot(layers=[]) + deferred_tool_call = DeferredToolCallPayload( + tool_call_id="tool-call-1", + tool_name="ask_human", + args={"question": "Need review"}, + metadata={"layer_type": "dify.ask_human", "schema_version": 1}, + ) adapted = AgentBackendRunEventAdapter().adapt( - RunPausedEvent( + RunSucceededEvent( id="5-0", run_id="run-1", - data=RunPausedEventData(reason="human_handoff", message="Need review", session_snapshot=snapshot), + data=RunSucceededEventData(deferred_tool_call=deferred_tool_call, session_snapshot=snapshot), ) ) assert adapted == [ - AgentBackendRunPausedInternalEvent( + AgentBackendDeferredToolCallInternalEvent( run_id="run-1", source_event_id="5-0", - reason="human_handoff", + deferred_tool_call=deferred_tool_call, message="Need review", session_snapshot=snapshot, ) ] +def test_event_adapter_rejects_deferred_tool_call_success_without_payload(): + snapshot = CompositorSessionSnapshot(layers=[]) + + with pytest.raises(TypeError, match="deferred_tool_call branch is missing payload"): + _ = AgentBackendRunEventAdapter().adapt( + RunSucceededEvent( + id="5-1", + run_id="run-1", + data=RunSucceededEventData(deferred_tool_call=None, session_snapshot=snapshot), + ) + ) + + +def test_event_adapter_uses_deferred_tool_call_title_as_pause_message_fallback(): + snapshot = CompositorSessionSnapshot(layers=[]) + deferred_tool_call = DeferredToolCallPayload( + tool_call_id="tool-call-1", + tool_name="ask_human", + args={"title": "Review required"}, + metadata={}, + ) + + adapted = AgentBackendRunEventAdapter().adapt( + RunSucceededEvent( + id="5-2", + run_id="run-1", + data=RunSucceededEventData(deferred_tool_call=deferred_tool_call, session_snapshot=snapshot), + ) + ) + + assert adapted == [ + AgentBackendDeferredToolCallInternalEvent( + run_id="run-1", + source_event_id="5-2", + deferred_tool_call=deferred_tool_call, + message="Review required", + session_snapshot=snapshot, + ) + ] + + +def test_event_adapter_uses_generic_deferred_tool_call_pause_message_when_args_have_no_label(): + snapshot = CompositorSessionSnapshot(layers=[]) + deferred_tool_call = DeferredToolCallPayload( + tool_call_id="tool-call-1", + tool_name="ask_human", + args={"question": " ", "title": " "}, + metadata={}, + ) + + adapted = AgentBackendRunEventAdapter().adapt( + RunSucceededEvent( + id="5-3", + run_id="run-1", + data=RunSucceededEventData(deferred_tool_call=deferred_tool_call, session_snapshot=snapshot), + ) + ) + + assert adapted == [ + AgentBackendDeferredToolCallInternalEvent( + run_id="run-1", + source_event_id="5-3", + deferred_tool_call=deferred_tool_call, + message="Agent backend requested external input via deferred tool 'ask_human'.", + session_snapshot=snapshot, + ) + ] + + def test_event_adapter_maps_run_cancelled_to_terminal_cancelled(): adapted = AgentBackendRunEventAdapter().adapt( RunCancelledEvent( diff --git a/api/tests/unit_tests/clients/agent_backend/test_fake_client.py b/api/tests/unit_tests/clients/agent_backend/test_fake_client.py index 9b3e206031e..5862117f622 100644 --- a/api/tests/unit_tests/clients/agent_backend/test_fake_client.py +++ b/api/tests/unit_tests/clients/agent_backend/test_fake_client.py @@ -70,18 +70,18 @@ def test_fake_client_cancel_run_returns_cancelled_status(): assert cancelled.status == "cancelled" -def test_fake_client_paused_scenario_returns_paused_status_and_event(): - """The paused scenario exists for HITL-style flows; both ``wait_run`` and - the event stream must report the pause so consumers can branch on it.""" +def test_fake_client_paused_scenario_returns_deferred_tool_call_success_event(): + """The API pause scenario follows the Dify Agent deferred-tool wire shape.""" client = FakeAgentBackendRunClient(scenario=FakeAgentBackendScenario.PAUSED) status = client.wait_run("fake-run-1") events = list(client.stream_events("fake-run-1")) - assert status.status == "paused" + assert status.status == "succeeded" assert status.error is None - assert events[-1].type == "run_paused" - assert events[-1].data.reason == "human_input_required" + assert events[-1].type == "run_succeeded" + assert events[-1].data.deferred_tool_call is not None + assert events[-1].data.deferred_tool_call.tool_name == "ask_human" def test_fake_client_success_wait_run_returns_succeeded_status(): diff --git a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_output_adapter.py b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_output_adapter.py index 49e24cc6770..8d50021f8fe 100644 --- a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_output_adapter.py +++ b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_output_adapter.py @@ -6,7 +6,6 @@ from agenton.compositor import CompositorSessionSnapshot from clients.agent_backend import ( AgentBackendRunCancelledInternalEvent, AgentBackendRunFailedInternalEvent, - AgentBackendRunPausedInternalEvent, AgentBackendRunSucceededInternalEvent, ) from core.workflow.file_reference import build_file_reference @@ -144,24 +143,6 @@ def test_success_output_adapter_preserves_dict_output(): } -def test_failure_output_adapter_maps_paused_to_unsupported_failure(): - result = WorkflowAgentOutputAdapter().build_failure_result( - event=AgentBackendRunPausedInternalEvent( - run_id="run-1", - source_event_id="2-0", - reason="human", - message=None, - session_snapshot=None, - ), - inputs={}, - process_data={}, - metadata={}, - ) - - assert result.status == WorkflowNodeExecutionStatus.FAILED - assert result.error_type == "agent_backend_paused_unsupported" - - def test_failure_output_adapter_preserves_backend_failed_reason(): result = WorkflowAgentOutputAdapter().build_failure_result( event=AgentBackendRunFailedInternalEvent( From e26214c02dd3aaf9fdf2c97230f2f5bbefc70526 Mon Sep 17 00:00:00 2001 From: KVOJJJin Date: Thu, 11 Jun 2026 14:26:41 +0800 Subject: [PATCH 018/122] fix(web): typo of creator filter (#37321) --- web/i18n/zh-Hans/app.json | 2 +- web/i18n/zh-Hant/app.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/i18n/zh-Hans/app.json b/web/i18n/zh-Hans/app.json index 41c59e6456f..8da944d2c36 100644 --- a/web/i18n/zh-Hans/app.json +++ b/web/i18n/zh-Hans/app.json @@ -241,7 +241,7 @@ "studio.filters.allCreators": "所有创作者", "studio.filters.creators": "创作者", "studio.filters.reset": "重置", - "studio.filters.searchCreators": "搜索创建者...", + "studio.filters.searchCreators": "搜索创作者...", "studio.filters.types": "类型", "studio.filters.you": "你", "studio.viewSnippets": "查看 Snippets", diff --git a/web/i18n/zh-Hant/app.json b/web/i18n/zh-Hant/app.json index 2fc376a734e..75ad8ca2f0f 100644 --- a/web/i18n/zh-Hant/app.json +++ b/web/i18n/zh-Hant/app.json @@ -241,7 +241,7 @@ "studio.filters.allCreators": "所有創作者", "studio.filters.creators": "創作者", "studio.filters.reset": "重置", - "studio.filters.searchCreators": "搜尋創建者...", + "studio.filters.searchCreators": "搜尋創作者...", "studio.filters.types": "類型", "studio.filters.you": "你", "studio.viewSnippets": "看片段", From 632df88228ab5981e97b08f417d1b773b1b0cc9d Mon Sep 17 00:00:00 2001 From: KVOJJJin Date: Thu, 11 Jun 2026 15:01:46 +0800 Subject: [PATCH 019/122] fix(web): correct icon of tag (#37326) --- .../workflow/block-selector/snippets/snippet-tags-filter.tsx | 3 +-- web/features/tag-management/components/tag-filter.tsx | 3 +-- web/features/tag-management/components/tag-search-content.tsx | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/web/app/components/workflow/block-selector/snippets/snippet-tags-filter.tsx b/web/app/components/workflow/block-selector/snippets/snippet-tags-filter.tsx index cab12e218c9..8ee15df1dce 100644 --- a/web/app/components/workflow/block-selector/snippets/snippet-tags-filter.tsx +++ b/web/app/components/workflow/block-selector/snippets/snippet-tags-filter.tsx @@ -10,7 +10,6 @@ import { import { useQuery } from '@tanstack/react-query' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import Tag01Icon from '@/app/components/base/icons/src/vender/line/financeAndECommerce/Tag01' import { consoleQuery } from '@/service/client' type SnippetTagsFilterProps = { @@ -74,7 +73,7 @@ const SnippetTagsFilter = ({ value.length > 0 && 'text-text-secondary', )} > -
) } - if (isFetchingAppInfo || isFetchingAppParams || isFetchingAppMeta) { + if (isLoadingAppInfo || isLoadingAppParams || isLoadingAppMeta || !appInfo || !appParams || !appMeta) { return (
From e32a732812818aa0ab91c22542aabe196a3923f7 Mon Sep 17 00:00:00 2001 From: zyssyz123 <916125788@qq.com> Date: Fri, 12 Jun 2026 11:46:21 +0800 Subject: [PATCH 038/122] refactor: agent draft (#37356) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/controllers/console/agent/roster.py | 2 + .../nodes/agent_v2/binding_resolver.py | 22 ++- .../workflow/nodes/agent_v2/validators.py | 18 ++- api/fields/agent_fields.py | 13 ++ ..._06_12_1100-0b2f2c8a9d1e_add_agent_role.py | 27 ++++ api/models/agent.py | 1 + api/openapi/markdown/console-swagger.md | 21 +++ api/services/agent/composer_service.py | 41 +++-- api/services/agent/roster_service.py | 131 +++++++++++----- .../agent/workflow_publish_service.py | 122 ++++++++++++++- api/services/entities/agent_entities.py | 2 + api/services/workflow_service.py | 6 + .../nodes/agent_v2/test_binding_resolver.py | 21 ++- .../nodes/agent_v2/test_validators.py | 20 ++- .../services/agent/test_agent_services.py | 141 ++++++++++++++++-- .../generated/api/console/agents/types.gen.ts | 19 +++ .../generated/api/console/agents/zod.gen.ts | 22 +++ 17 files changed, 554 insertions(+), 75 deletions(-) create mode 100644 api/migrations/versions/2026_06_12_1100-0b2f2c8a9d1e_add_agent_role.py diff --git a/api/controllers/console/agent/roster.py b/api/controllers/console/agent/roster.py index c305a816eec..1066e51feae 100644 --- a/api/controllers/console/agent/roster.py +++ b/api/controllers/console/agent/roster.py @@ -18,6 +18,7 @@ from fields.agent_fields import ( AgentConfigSnapshotDetailResponse, AgentConfigSnapshotListResponse, AgentInviteOptionsResponse, + AgentPublishedReferenceResponse, AgentRosterListResponse, AgentRosterResponse, ) @@ -48,6 +49,7 @@ register_response_schema_models( AgentConfigSnapshotDetailResponse, AgentConfigSnapshotListResponse, AgentInviteOptionsResponse, + AgentPublishedReferenceResponse, AgentRosterListResponse, AgentRosterResponse, ) diff --git a/api/core/workflow/nodes/agent_v2/binding_resolver.py b/api/core/workflow/nodes/agent_v2/binding_resolver.py index d2f50b0ae4d..8dbabff2b8f 100644 --- a/api/core/workflow/nodes/agent_v2/binding_resolver.py +++ b/api/core/workflow/nodes/agent_v2/binding_resolver.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from sqlalchemy import select from core.db.session_factory import session_factory -from models.agent import Agent, AgentConfigSnapshot, AgentStatus, WorkflowAgentNodeBinding +from models.agent import Agent, AgentConfigSnapshot, AgentStatus, WorkflowAgentBindingType, WorkflowAgentNodeBinding class WorkflowAgentBindingError(Exception): @@ -52,11 +52,6 @@ class WorkflowAgentBindingResolver: ) if binding.agent_id is None: raise WorkflowAgentBindingError("agent_not_available", "Workflow Agent binding has no agent.") - if binding.current_snapshot_id is None: - raise WorkflowAgentBindingError( - "agent_config_snapshot_not_found", - "Workflow Agent binding has no current config snapshot.", - ) agent = session.scalar( select(Agent) @@ -72,19 +67,30 @@ class WorkflowAgentBindingResolver: f"Agent {binding.agent_id} is not available.", ) + snapshot_id = ( + agent.active_config_snapshot_id + if binding.binding_type == WorkflowAgentBindingType.ROSTER_AGENT + else binding.current_snapshot_id + ) + if snapshot_id is None: + raise WorkflowAgentBindingError( + "agent_config_snapshot_not_found", + "Workflow Agent binding has no current config snapshot.", + ) + snapshot = session.scalar( select(AgentConfigSnapshot) .where( AgentConfigSnapshot.tenant_id == tenant_id, AgentConfigSnapshot.agent_id == agent.id, - AgentConfigSnapshot.id == binding.current_snapshot_id, + AgentConfigSnapshot.id == snapshot_id, ) .limit(1) ) if snapshot is None: raise WorkflowAgentBindingError( "agent_config_snapshot_not_found", - f"Agent config snapshot {binding.current_snapshot_id} not found.", + f"Agent config snapshot {snapshot_id} not found.", ) session.expunge(binding) diff --git a/api/core/workflow/nodes/agent_v2/validators.py b/api/core/workflow/nodes/agent_v2/validators.py index 59e6934ba5e..ca3adb5b0d1 100644 --- a/api/core/workflow/nodes/agent_v2/validators.py +++ b/api/core/workflow/nodes/agent_v2/validators.py @@ -8,7 +8,7 @@ from sqlalchemy.orm import Session from core.workflow.graph_topology import WorkflowGraphTopology from graphon.enums import BuiltinNodeTypes -from models.agent import Agent, AgentConfigSnapshot, AgentStatus, WorkflowAgentNodeBinding +from models.agent import Agent, AgentConfigSnapshot, AgentStatus, WorkflowAgentBindingType, WorkflowAgentNodeBinding from models.agent_config_entities import ( AgentFileRefConfig, AgentHumanContactConfig, @@ -102,10 +102,6 @@ class WorkflowAgentNodeValidator: ) -> None: if binding.agent_id is None: raise WorkflowAgentNodeValidationError(f"Workflow Agent node {binding.node_id} is missing agent binding.") - if binding.current_snapshot_id is None: - raise WorkflowAgentNodeValidationError( - f"Workflow Agent node {binding.node_id} is missing config snapshot binding." - ) agent = session.scalar( select(Agent) @@ -120,12 +116,22 @@ class WorkflowAgentNodeValidator: f"Workflow Agent node {binding.node_id} references an unavailable agent." ) + snapshot_id = ( + agent.active_config_snapshot_id + if binding.binding_type == WorkflowAgentBindingType.ROSTER_AGENT + else binding.current_snapshot_id + ) + if snapshot_id is None: + raise WorkflowAgentNodeValidationError( + f"Workflow Agent node {binding.node_id} is missing config snapshot binding." + ) + snapshot = session.scalar( select(AgentConfigSnapshot) .where( AgentConfigSnapshot.tenant_id == binding.tenant_id, AgentConfigSnapshot.agent_id == agent.id, - AgentConfigSnapshot.id == binding.current_snapshot_id, + AgentConfigSnapshot.id == snapshot_id, ) .limit(1) ) diff --git a/api/fields/agent_fields.py b/api/fields/agent_fields.py index 4c9b48094ae..ce4854ce360 100644 --- a/api/fields/agent_fields.py +++ b/api/fields/agent_fields.py @@ -41,10 +41,20 @@ class AgentConfigSnapshotSummaryResponse(ResponseModel): created_at: int | None = None +class AgentPublishedReferenceResponse(ResponseModel): + app_id: str + app_name: str + app_mode: str + workflow_id: str + workflow_version: str + node_ids: list[str] = Field(default_factory=list) + + class AgentRosterResponse(ResponseModel): id: str name: str description: str + role: str = "" icon_type: AgentIconType | None = None icon: str | None = None icon_background: str | None = None @@ -63,6 +73,9 @@ class AgentRosterResponse(ResponseModel): archived_at: int | None = None created_at: int | None = None updated_at: int | None = None + published_reference_count: int = 0 + published_node_reference_count: int = 0 + published_references: list[AgentPublishedReferenceResponse] = Field(default_factory=list) class AgentInviteOptionResponse(AgentRosterResponse): diff --git a/api/migrations/versions/2026_06_12_1100-0b2f2c8a9d1e_add_agent_role.py b/api/migrations/versions/2026_06_12_1100-0b2f2c8a9d1e_add_agent_role.py new file mode 100644 index 00000000000..900f7da06fc --- /dev/null +++ b/api/migrations/versions/2026_06_12_1100-0b2f2c8a9d1e_add_agent_role.py @@ -0,0 +1,27 @@ +"""add agent role + +Revision ID: 0b2f2c8a9d1e +Revises: 7bad07dc267d +Create Date: 2026-06-12 11:00:00.000000 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "0b2f2c8a9d1e" +down_revision = "7bad07dc267d" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("agents", schema=None) as batch_op: + batch_op.add_column(sa.Column("role", sa.String(length=255), nullable=False, server_default="")) + batch_op.alter_column("role", server_default=None) + + +def downgrade(): + with op.batch_alter_table("agents", schema=None) as batch_op: + batch_op.drop_column("role") diff --git a/api/models/agent.py b/api/models/agent.py index 9624bf53359..8487bc18962 100644 --- a/api/models/agent.py +++ b/api/models/agent.py @@ -136,6 +136,7 @@ class Agent(DefaultFieldsMixin, Base): tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[str] = mapped_column(LongText, nullable=False, default="") + role: Mapped[str] = mapped_column(String(255), nullable=False, default="") icon_type: Mapped[AgentIconType | None] = mapped_column(EnumText(AgentIconType, length=32), nullable=True) icon: Mapped[str | None] = mapped_column( String(255), diff --git a/api/openapi/markdown/console-swagger.md b/api/openapi/markdown/console-swagger.md index 02a0109d938..2b66bcab70a 100644 --- a/api/openapi/markdown/console-swagger.md +++ b/api/openapi/markdown/console-swagger.md @@ -12148,6 +12148,10 @@ Supported icon storage formats for Agent roster entries. | in_current_workflow_count | integer | | No | | is_in_current_workflow | boolean | | No | | name | string | | Yes | +| published_node_reference_count | integer | | No | +| published_reference_count | integer | | No | +| published_references | [ [AgentPublishedReferenceResponse](#agentpublishedreferenceresponse) ] | | No | +| role | string | | No | | scope | [AgentScope](#agentscope) | | Yes | | source | [AgentSource](#agentsource) | | Yes | | status | [AgentStatus](#agentstatus) | | Yes | @@ -12255,6 +12259,17 @@ the current roster/workflow APIs scoped to Dify Agent. | state | string | | No | | status | string | | No | +#### AgentPublishedReferenceResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| app_id | string | | Yes | +| app_mode | string | | Yes | +| app_name | string | | Yes | +| node_ids | [ string ] | | No | +| workflow_id | string | | Yes | +| workflow_version | string | | Yes | + #### AgentReferencingWorkflowResponse | Name | Type | Description | Required | @@ -12299,6 +12314,10 @@ the current roster/workflow APIs scoped to Dify Agent. | icon_type | [AgentIconType](#agenticontype) | | No | | id | string | | Yes | | name | string | | Yes | +| published_node_reference_count | integer | | No | +| published_reference_count | integer | | No | +| published_references | [ [AgentPublishedReferenceResponse](#agentpublishedreferenceresponse) ] | | No | +| role | string | | No | | scope | [AgentScope](#agentscope) | | Yes | | source | [AgentSource](#agentsource) | | Yes | | status | [AgentStatus](#agentstatus) | | Yes | @@ -16548,6 +16567,7 @@ Payload for publishing snippet workflow. | icon_background | string | | No | | icon_type | [AgentIconType](#agenticontype) | | No | | name | string | | Yes | +| role | string | | No | | version_note | string | | No | #### RosterAgentUpdatePayload @@ -16559,6 +16579,7 @@ Payload for publishing snippet workflow. | icon_background | string | | No | | icon_type | [AgentIconType](#agenticontype) | | No | | name | string | | No | +| role | string | | No | #### RosterListQuery diff --git a/api/services/agent/composer_service.py b/api/services/agent/composer_service.py index 9458460f512..a2d73929035 100644 --- a/api/services/agent/composer_service.py +++ b/api/services/agent/composer_service.py @@ -2,7 +2,7 @@ import logging import uuid from typing import Any -from sqlalchemy import func, select +from sqlalchemy import func, or_, select from sqlalchemy.exc import IntegrityError from extensions.ext_database import db @@ -74,10 +74,15 @@ class AgentComposerService: return cls._empty_workflow_state(app_id=app_id, workflow_id=workflow.id, node_id=node_id) agent = cls._get_agent_if_present(tenant_id=tenant_id, agent_id=binding.agent_id) + version_id = ( + agent.active_config_snapshot_id + if agent and binding.binding_type == WorkflowAgentBindingType.ROSTER_AGENT + else binding.current_snapshot_id + ) version = cls._get_version_if_present( tenant_id=tenant_id, agent_id=agent.id if agent else None, - version_id=binding.current_snapshot_id, + version_id=version_id, ) return cls._serialize_workflow_state(binding=binding, agent=agent, version=version) @@ -129,10 +134,15 @@ class AgentComposerService: db.session.commit() agent = cls._get_agent_if_present(tenant_id=tenant_id, agent_id=binding.agent_id) + version_id = ( + agent.active_config_snapshot_id + if agent and binding.binding_type == WorkflowAgentBindingType.ROSTER_AGENT + else binding.current_snapshot_id + ) version = cls._get_version_if_present( tenant_id=tenant_id, agent_id=agent.id if agent else None, - version_id=binding.current_snapshot_id, + version_id=version_id, ) state = cls._serialize_workflow_state(binding=binding, agent=agent, version=version) state["validation"] = cls.collect_validation_findings(tenant_id=tenant_id, payload=payload) @@ -489,11 +499,26 @@ class AgentComposerService: @classmethod def calculate_impact(cls, *, tenant_id: str, current_snapshot_id: str) -> dict[str, Any]: + snapshot = db.session.scalar( + select(AgentConfigSnapshot) + .where( + AgentConfigSnapshot.tenant_id == tenant_id, + AgentConfigSnapshot.id == current_snapshot_id, + ) + .limit(1) + ) + agent_id = snapshot.agent_id if snapshot else None + predicates = [WorkflowAgentNodeBinding.current_snapshot_id == current_snapshot_id] + if agent_id: + predicates.append( + (WorkflowAgentNodeBinding.agent_id == agent_id) + & (WorkflowAgentNodeBinding.binding_type == WorkflowAgentBindingType.ROSTER_AGENT) + ) bindings = list( db.session.scalars( select(WorkflowAgentNodeBinding).where( WorkflowAgentNodeBinding.tenant_id == tenant_id, - WorkflowAgentNodeBinding.current_snapshot_id == current_snapshot_id, + or_(*predicates), ) ).all() ) @@ -1003,7 +1028,7 @@ class AgentComposerService: "id": binding.id, "binding_type": binding.binding_type.value, "agent_id": binding.agent_id, - "current_snapshot_id": binding.current_snapshot_id, + "current_snapshot_id": version.id if version else binding.current_snapshot_id, "workflow_id": binding.workflow_id, "node_id": binding.node_id, }, @@ -1022,10 +1047,8 @@ class AgentComposerService: # this is the same list (so callers don't need to special-case). "effective_declared_outputs": cls._serialize_effective_outputs(cls._declared_outputs_from_binding(binding)), "save_options": save_options, - "impact_summary": cls.calculate_impact( - tenant_id=binding.tenant_id, current_snapshot_id=binding.current_snapshot_id - ) - if binding.current_snapshot_id + "impact_summary": cls.calculate_impact(tenant_id=binding.tenant_id, current_snapshot_id=version.id) + if version else None, } diff --git a/api/services/agent/roster_service.py b/api/services/agent/roster_service.py index c4ec16c0b73..ab57e22268a 100644 --- a/api/services/agent/roster_service.py +++ b/api/services/agent/roster_service.py @@ -18,6 +18,7 @@ from models.agent import ( WorkflowAgentNodeBinding, ) from models.agent_config_entities import AgentSoulConfig +from models.enums import AppStatus from models.model import App from models.workflow import Workflow from services.agent.composer_validator import ComposerConfigValidator @@ -37,6 +38,7 @@ class AgentReferencingWorkflow(TypedDict): app_name: str app_mode: str workflow_id: str + workflow_version: str node_ids: list[str] @@ -45,11 +47,17 @@ class AgentRosterService: self._session = session @staticmethod - def serialize_agent(agent: Agent, active_version: AgentConfigSnapshot | None = None) -> dict[str, Any]: + def serialize_agent( + agent: Agent, + active_version: AgentConfigSnapshot | None = None, + published_references: list[AgentReferencingWorkflow] | None = None, + ) -> dict[str, Any]: + published_references = published_references or [] return { "id": agent.id, "name": agent.name, "description": agent.description, + "role": agent.role or "", "icon_type": agent.icon_type.value if agent.icon_type else None, "icon": agent.icon, "icon_background": agent.icon_background, @@ -68,6 +76,9 @@ class AgentRosterService: "archived_at": to_timestamp(agent.archived_at), "created_at": to_timestamp(agent.created_at), "updated_at": to_timestamp(agent.updated_at), + "published_reference_count": len(published_references), + "published_node_reference_count": sum(len(item["node_ids"]) for item in published_references), + "published_references": published_references, } @staticmethod @@ -104,13 +115,23 @@ class AgentRosterService: versions_by_id = self._load_versions_by_id( [agent.active_config_snapshot_id for agent in agents if agent.active_config_snapshot_id] ) + published_references_by_agent_id = self._load_published_references_by_agent_id( + tenant_id=tenant_id, + agent_ids=[agent.id for agent in agents], + ) data = [] for agent in agents: active_version = ( versions_by_id.get(agent.active_config_snapshot_id) if agent.active_config_snapshot_id else None ) - data.append(self.serialize_agent(agent, active_version)) + data.append( + self.serialize_agent( + agent, + active_version, + published_references_by_agent_id.get(agent.id, []), + ) + ) return { "data": data, @@ -170,6 +191,7 @@ class AgentRosterService: tenant_id=tenant_id, name=payload.name, description=payload.description, + role=payload.role, icon_type=payload.icon_type, icon=payload.icon, icon_background=payload.icon_background, @@ -241,6 +263,7 @@ class AgentRosterService: tenant_id=tenant_id, name=name, description=description, + role="", icon_type=icon_type, icon=icon, icon_background=icon_background, @@ -306,48 +329,18 @@ class AgentRosterService: if agent is None: return [] - bindings = self._session.scalars( - select(WorkflowAgentNodeBinding).where( - WorkflowAgentNodeBinding.tenant_id == tenant_id, - WorkflowAgentNodeBinding.agent_id == agent.id, - WorkflowAgentNodeBinding.binding_type == WorkflowAgentBindingType.ROSTER_AGENT, - ) - ).all() - if not bindings: - return [] - - # Collapse the per-version / per-node rows into one entry per workflow app. - node_ids_by_workflow: dict[tuple[str, str], set[str]] = {} - for binding in bindings: - node_ids_by_workflow.setdefault((binding.app_id, binding.workflow_id), set()).add(binding.node_id) - - referenced_app_ids = {workflow_app_id for workflow_app_id, _ in node_ids_by_workflow} - apps = {app.id: app for app in self._session.scalars(select(App).where(App.id.in_(referenced_app_ids))).all()} - - result: list[AgentReferencingWorkflow] = [] - for (workflow_app_id, workflow_id), node_ids in node_ids_by_workflow.items(): - app = apps.get(workflow_app_id) - if app is None: - # Orphaned binding (workflow app deleted): skip rather than 500. - continue - result.append( - AgentReferencingWorkflow( - app_id=workflow_app_id, - app_name=app.name, - app_mode=str(app.mode), - workflow_id=workflow_id, - node_ids=sorted(node_ids), - ) - ) - result.sort(key=lambda item: item["app_name"].lower()) - return result + return self._load_published_references_by_agent_id(tenant_id=tenant_id, agent_ids=[agent.id]).get(agent.id, []) def get_roster_agent_detail(self, *, tenant_id: str, agent_id: str) -> dict[str, Any]: agent = self._get_agent(tenant_id=tenant_id, agent_id=agent_id, roster_only=True) active_version = self._get_version( tenant_id=tenant_id, agent_id=agent.id, version_id=agent.active_config_snapshot_id ) - return self.serialize_agent(agent, active_version) + published_references_by_agent_id = self._load_published_references_by_agent_id( + tenant_id=tenant_id, + agent_ids=[agent.id], + ) + return self.serialize_agent(agent, active_version, published_references_by_agent_id.get(agent.id, [])) def update_roster_agent( self, *, tenant_id: str, agent_id: str, account_id: str, payload: RosterAgentUpdatePayload @@ -450,6 +443,68 @@ class AgentRosterService: raise AgentVersionNotFoundError() return version + def _load_published_references_by_agent_id( + self, *, tenant_id: str, agent_ids: list[str] + ) -> dict[str, list[AgentReferencingWorkflow]]: + if not agent_ids: + return {} + + bindings = list( + self._session.scalars( + select(WorkflowAgentNodeBinding).where( + WorkflowAgentNodeBinding.tenant_id == tenant_id, + WorkflowAgentNodeBinding.agent_id.in_(agent_ids), + WorkflowAgentNodeBinding.binding_type == WorkflowAgentBindingType.ROSTER_AGENT, + WorkflowAgentNodeBinding.workflow_version != Workflow.VERSION_DRAFT, + ) + ).all() + ) + if not bindings: + return {} + + app_ids = {binding.app_id for binding in bindings} + apps = { + app.id: app + for app in self._session.scalars( + select(App).where( + App.tenant_id == tenant_id, + App.id.in_(app_ids), + App.status == AppStatus.NORMAL, + ) + ).all() + } + + grouped: dict[str, dict[tuple[str, str], AgentReferencingWorkflow]] = {} + for binding in bindings: + if not binding.agent_id: + continue + app = apps.get(binding.app_id) + if app is None or app.workflow_id != binding.workflow_id: + continue + by_workflow = grouped.setdefault(binding.agent_id, {}) + key = (binding.app_id, binding.workflow_id) + item = by_workflow.setdefault( + key, + AgentReferencingWorkflow( + app_id=binding.app_id, + app_name=app.name, + app_mode=str(app.mode), + workflow_id=binding.workflow_id, + workflow_version=binding.workflow_version, + node_ids=[], + ), + ) + item["node_ids"].append(binding.node_id) + + result: dict[str, list[AgentReferencingWorkflow]] = {} + for agent_id, by_workflow in grouped.items(): + references = list(by_workflow.values()) + for reference in references: + reference["node_ids"] = sorted(set(reference["node_ids"])) + references.sort(key=lambda item: (item["app_name"].lower(), item["workflow_id"])) + result[agent_id] = references + return result + def _load_versions_by_id(self, version_ids: list[str]) -> dict[str, AgentConfigSnapshot]: if not version_ids: return {} diff --git a/api/services/agent/workflow_publish_service.py b/api/services/agent/workflow_publish_service.py index af3e5112290..13927026c48 100644 --- a/api/services/agent/workflow_publish_service.py +++ b/api/services/agent/workflow_publish_service.py @@ -1,10 +1,13 @@ from __future__ import annotations +from collections.abc import Mapping +from typing import Any + from sqlalchemy import select from sqlalchemy.orm import Session from core.workflow.nodes.agent_v2.validators import WorkflowAgentNodeValidator -from models.agent import WorkflowAgentNodeBinding +from models.agent import Agent, AgentScope, AgentStatus, WorkflowAgentBindingType, WorkflowAgentNodeBinding from models.agent_config_entities import WorkflowNodeJobConfig from models.workflow import Workflow @@ -12,6 +15,9 @@ from models.workflow import Workflow class WorkflowAgentPublishService: """Validate and freeze Workflow Agent v2 bindings during workflow publish.""" + _DRAFT_WORKFLOW_VERSION = Workflow.VERSION_DRAFT + _AGENT_BINDING_KEY = "agent_binding" + @classmethod def validate_agent_nodes_for_publish(cls, *, session: Session, draft_workflow: Workflow) -> None: WorkflowAgentNodeValidator.validate_published_workflow(session=session, workflow=draft_workflow) @@ -20,6 +26,100 @@ class WorkflowAgentPublishService: def validate_agent_nodes_for_draft_sync(cls, *, session: Session, draft_workflow: Workflow) -> None: WorkflowAgentNodeValidator.validate_draft_workflow(session=session, workflow=draft_workflow) + @classmethod + def sync_roster_agent_bindings_for_draft( + cls, + *, + session: Session, + draft_workflow: Workflow, + account_id: str, + ) -> None: + agent_nodes = dict(WorkflowAgentNodeValidator.iter_agent_v2_nodes(draft_workflow.graph_dict)) + existing_bindings = list( + session.scalars( + select(WorkflowAgentNodeBinding).where( + WorkflowAgentNodeBinding.tenant_id == draft_workflow.tenant_id, + WorkflowAgentNodeBinding.app_id == draft_workflow.app_id, + WorkflowAgentNodeBinding.workflow_id == draft_workflow.id, + WorkflowAgentNodeBinding.workflow_version == cls._DRAFT_WORKFLOW_VERSION, + ) + ).all() + ) + existing_by_node_id = {binding.node_id: binding for binding in existing_bindings} + + for binding in existing_bindings: + if binding.node_id not in agent_nodes: + session.delete(binding) + + for node_id, node_data in agent_nodes.items(): + binding_payload = node_data.get(cls._AGENT_BINDING_KEY) + if binding_payload is None: + continue + if not isinstance(binding_payload, Mapping): + raise ValueError(f"Workflow Agent node {node_id} has invalid agent_binding.") + cls._sync_roster_agent_binding_for_node( + session=session, + draft_workflow=draft_workflow, + node_id=node_id, + node_binding=binding_payload, + existing_binding=existing_by_node_id.get(node_id), + account_id=account_id, + ) + session.flush() + + @classmethod + def _sync_roster_agent_binding_for_node( + cls, + *, + session: Session, + draft_workflow: Workflow, + node_id: str, + node_binding: Mapping[str, Any], + existing_binding: WorkflowAgentNodeBinding | None, + account_id: str, + ) -> None: + binding_type = node_binding.get("binding_type") + if binding_type != WorkflowAgentBindingType.ROSTER_AGENT.value: + raise ValueError(f"Workflow Agent node {node_id} only supports roster_agent graph binding.") + agent_id = node_binding.get("agent_id") + if not isinstance(agent_id, str) or not agent_id: + raise ValueError(f"Workflow Agent node {node_id} roster_agent binding requires agent_id.") + + agent = session.scalar( + select(Agent) + .where( + Agent.tenant_id == draft_workflow.tenant_id, + Agent.id == agent_id, + Agent.scope == AgentScope.ROSTER, + Agent.status == AgentStatus.ACTIVE, + ) + .limit(1) + ) + if agent is None: + raise ValueError(f"Workflow Agent node {node_id} references an unavailable roster agent.") + if not agent.active_config_snapshot_id: + raise ValueError(f"Workflow Agent node {node_id} roster agent has no active config snapshot.") + + binding = existing_binding + if binding is None: + binding = WorkflowAgentNodeBinding( + tenant_id=draft_workflow.tenant_id, + app_id=draft_workflow.app_id, + workflow_id=draft_workflow.id, + workflow_version=cls._DRAFT_WORKFLOW_VERSION, + node_id=node_id, + node_job_config=WorkflowNodeJobConfig(), + created_by=account_id, + ) + session.add(binding) + elif not binding.node_job_config: + binding.node_job_config = WorkflowNodeJobConfig() + + binding.binding_type = WorkflowAgentBindingType.ROSTER_AGENT + binding.agent_id = agent.id + binding.current_snapshot_id = agent.active_config_snapshot_id + binding.updated_by = account_id + @classmethod def copy_agent_node_bindings_to_published( cls, @@ -43,8 +143,26 @@ class WorkflowAgentPublishService: WorkflowAgentNodeBinding.node_id.in_(node_ids), ) ).all() + if not bindings: + return + + agents_by_id = { + agent.id: agent + for agent in session.scalars( + select(Agent).where( + Agent.tenant_id == draft_workflow.tenant_id, + Agent.id.in_({binding.agent_id for binding in bindings if binding.agent_id}), + ) + ).all() + } for binding in bindings: + agent = agents_by_id.get(binding.agent_id) if binding.agent_id else None + current_snapshot_id = ( + agent.active_config_snapshot_id + if agent is not None and binding.binding_type == WorkflowAgentBindingType.ROSTER_AGENT + else binding.current_snapshot_id + ) copied = WorkflowAgentNodeBinding( tenant_id=binding.tenant_id, app_id=binding.app_id, @@ -53,7 +171,7 @@ class WorkflowAgentPublishService: node_id=binding.node_id, binding_type=binding.binding_type, agent_id=binding.agent_id, - current_snapshot_id=binding.current_snapshot_id, + current_snapshot_id=current_snapshot_id, node_job_config=WorkflowNodeJobConfig.model_validate(binding.node_job_config_dict), created_by=binding.created_by, updated_by=binding.updated_by, diff --git a/api/services/entities/agent_entities.py b/api/services/entities/agent_entities.py index e2730d1357a..63ae101533d 100644 --- a/api/services/entities/agent_entities.py +++ b/api/services/entities/agent_entities.py @@ -61,6 +61,7 @@ class ComposerSavePayload(BaseModel): class RosterAgentCreatePayload(BaseModel): name: str = Field(min_length=1, max_length=255) description: str = "" + role: str = Field(default="", max_length=255) icon_type: AgentIconType | None = None icon: str | None = Field(default=None, max_length=255) icon_background: str | None = Field(default=None, max_length=255) @@ -71,6 +72,7 @@ class RosterAgentCreatePayload(BaseModel): class RosterAgentUpdatePayload(BaseModel): name: str | None = Field(default=None, min_length=1, max_length=255) description: str | None = None + role: str | None = Field(default=None, max_length=255) icon_type: AgentIconType | None = None icon: str | None = Field(default=None, max_length=255) icon_background: str | None = Field(default=None, max_length=255) diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index 50dd977749b..4be120ac782 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -322,6 +322,12 @@ class WorkflowService: from services.agent.workflow_publish_service import WorkflowAgentPublishService + db.session.flush() + WorkflowAgentPublishService.sync_roster_agent_bindings_for_draft( + session=cast(Session, db.session), + draft_workflow=workflow, + account_id=account.id, + ) WorkflowAgentPublishService.validate_agent_nodes_for_draft_sync( session=cast(Session, db.session), draft_workflow=workflow, diff --git a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_binding_resolver.py b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_binding_resolver.py index 1f75e4c19d9..a628c76b38d 100644 --- a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_binding_resolver.py +++ b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_binding_resolver.py @@ -4,7 +4,7 @@ from core.workflow.nodes.agent_v2.binding_resolver import ( WorkflowAgentBindingError, WorkflowAgentBindingResolver, ) -from models.agent import Agent, AgentConfigSnapshot, AgentStatus, WorkflowAgentNodeBinding +from models.agent import Agent, AgentConfigSnapshot, AgentStatus, WorkflowAgentBindingType, WorkflowAgentNodeBinding from models.agent_config_entities import AgentSoulConfig, AgentSoulModelConfig, WorkflowNodeJobConfig @@ -85,6 +85,25 @@ def test_binding_resolver_returns_detached_binding_bundle(monkeypatch: pytest.Mo assert fake_session.expunge_calls == [bundle.binding, bundle.agent, bundle.snapshot] +def test_binding_resolver_uses_active_snapshot_for_roster_agent(monkeypatch: pytest.MonkeyPatch): + binding = _binding() + binding.binding_type = WorkflowAgentBindingType.ROSTER_AGENT + binding.current_snapshot_id = "old-snapshot" + agent = _agent() + agent.active_config_snapshot_id = "active-snapshot" + snapshot = _snapshot() + snapshot.id = "active-snapshot" + fake_session = FakeSession([binding, agent, snapshot]) + monkeypatch.setattr( + "core.workflow.nodes.agent_v2.binding_resolver.session_factory.create_session", + lambda: fake_session, + ) + + bundle = WorkflowAgentBindingResolver().resolve(**_resolve()) + + assert bundle.snapshot.id == "active-snapshot" + + def test_binding_resolver_raises_when_binding_missing(monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr( "core.workflow.nodes.agent_v2.binding_resolver.session_factory.create_session", diff --git a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_validators.py b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_validators.py index 7237f01319b..440bd49e5c0 100644 --- a/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_validators.py +++ b/api/tests/unit_tests/core/workflow/nodes/agent_v2/test_validators.py @@ -8,7 +8,7 @@ from core.workflow.nodes.agent_v2.validators import ( WorkflowAgentNodeValidationError, WorkflowAgentNodeValidator, ) -from models.agent import Agent, AgentConfigSnapshot, AgentStatus, WorkflowAgentNodeBinding +from models.agent import Agent, AgentConfigSnapshot, AgentStatus, WorkflowAgentBindingType, WorkflowAgentNodeBinding from models.agent_config_entities import AgentSoulConfig, AgentSoulModelConfig, WorkflowNodeJobConfig from models.workflow import Workflow @@ -111,6 +111,24 @@ def test_publish_validation_accepts_upstream_previous_output_ref(): ) +def test_publish_validation_uses_active_snapshot_for_roster_agent(): + node_job = WorkflowNodeJobConfig() + binding = _binding(node_job) + binding.binding_type = WorkflowAgentBindingType.ROSTER_AGENT + binding.current_snapshot_id = "old-snapshot" + agent = _agent() + agent.active_config_snapshot_id = "active-snapshot" + snapshot = _snapshot() + snapshot.id = "active-snapshot" + session = Mock() + session.scalar.side_effect = [binding, agent, snapshot] + + WorkflowAgentNodeValidator.validate_published_workflow( + session=session, + workflow=_workflow(_graph([{"source": "start", "target": "agent-node"}])), + ) + + def test_publish_validation_rejects_non_upstream_previous_output_ref(): node_job = WorkflowNodeJobConfig.model_validate( {"previous_node_output_refs": [{"node_id": "later-node", "output": "text"}]} diff --git a/api/tests/unit_tests/services/agent/test_agent_services.py b/api/tests/unit_tests/services/agent/test_agent_services.py index fd73d65d32d..f7bcde44f50 100644 --- a/api/tests/unit_tests/services/agent/test_agent_services.py +++ b/api/tests/unit_tests/services/agent/test_agent_services.py @@ -14,11 +14,14 @@ from models.agent import ( WorkflowAgentBindingType, WorkflowAgentNodeBinding, ) +from models.agent_config_entities import WorkflowNodeJobConfig +from models.workflow import Workflow from services.agent import composer_service, roster_service from services.agent.composer_service import AgentComposerService from services.agent.composer_validator import ComposerConfigValidator from services.agent.errors import InvalidComposerConfigError from services.agent.roster_service import AgentRosterService +from services.agent.workflow_publish_service import WorkflowAgentPublishService from services.entities.agent_entities import AgentSoulConfig, ComposerSavePayload, ComposerSaveStrategy, ComposerVariant @@ -35,6 +38,7 @@ class FakeSession: self._scalars = list(scalars or []) self._scalar = list(scalar or []) self.added = [] + self.deleted = [] self.commits = 0 self.flushes = 0 self.rollbacks = 0 @@ -52,6 +56,9 @@ class FakeSession: def add(self, value): self.added.append(value) + def delete(self, value): + self.deleted.append(value) + def flush(self): self.flushes += 1 for index, value in enumerate(self.added, start=1): @@ -84,10 +91,18 @@ def test_load_workflow_composer_returns_empty_state(monkeypatch): def test_load_workflow_composer_serializes_existing_binding(monkeypatch): - binding = SimpleNamespace(agent_id="agent-1", current_snapshot_id="version-1") + binding = SimpleNamespace( + agent_id="agent-1", + binding_type=WorkflowAgentBindingType.ROSTER_AGENT, + current_snapshot_id="version-1", + ) monkeypatch.setattr(AgentComposerService, "_get_draft_workflow", lambda **kwargs: SimpleNamespace(id="workflow-1")) monkeypatch.setattr(AgentComposerService, "_get_workflow_binding", lambda **kwargs: binding) - monkeypatch.setattr(AgentComposerService, "_get_agent_if_present", lambda **kwargs: SimpleNamespace(id="agent-1")) + monkeypatch.setattr( + AgentComposerService, + "_get_agent_if_present", + lambda **kwargs: SimpleNamespace(id="agent-1", active_config_snapshot_id="version-1"), + ) monkeypatch.setattr( AgentComposerService, "_get_version_if_present", @@ -116,14 +131,22 @@ def test_load_workflow_composer_serializes_existing_binding(monkeypatch): ) def test_save_workflow_composer_dispatches_save_strategy(monkeypatch, strategy, helper_name): fake_session = FakeSession() - binding = SimpleNamespace(agent_id="agent-1", current_snapshot_id="version-1") + binding = SimpleNamespace( + agent_id="agent-1", + binding_type=WorkflowAgentBindingType.ROSTER_AGENT, + current_snapshot_id="version-1", + ) calls = [] monkeypatch.setattr(composer_service.db, "session", fake_session) monkeypatch.setattr(composer_service.ComposerConfigValidator, "validate_save_payload", lambda payload: None) monkeypatch.setattr(AgentComposerService, "_get_draft_workflow", lambda **kwargs: SimpleNamespace(id="workflow-1")) monkeypatch.setattr(AgentComposerService, "_get_workflow_binding", lambda **kwargs: None) - monkeypatch.setattr(AgentComposerService, "_get_agent_if_present", lambda **kwargs: SimpleNamespace(id="agent-1")) + monkeypatch.setattr( + AgentComposerService, + "_get_agent_if_present", + lambda **kwargs: SimpleNamespace(id="agent-1", active_config_snapshot_id="version-1"), + ) monkeypatch.setattr( AgentComposerService, "_get_version_if_present", @@ -523,6 +546,7 @@ def test_roster_list_and_invite_options(monkeypatch): tenant_id="tenant-1", name="Analyst", description="", + role="researcher", agent_kind=AgentKind.DIFY_AGENT, scope=AgentScope.ROSTER, source=AgentSource.AGENT_APP, @@ -539,11 +563,13 @@ def test_roster_list_and_invite_options(monkeypatch): ) service = AgentRosterService(fake_session) monkeypatch.setattr(service, "_load_versions_by_id", lambda version_ids: {"version-1": version}) + monkeypatch.setattr(service, "_load_published_references_by_agent_id", lambda **kwargs: {}) listed = service.list_roster_agents(tenant_id="tenant-1", page=1, limit=20) invited = service.list_invite_options(tenant_id="tenant-1", page=1, limit=20, app_id="app-1") assert listed["data"][0]["active_config_snapshot"]["id"] == "version-1" + assert listed["data"][0]["role"] == "researcher" assert listed["data"][0]["created_at"] == int(created_at.timestamp()) assert listed["data"][0]["updated_at"] == int(updated_at.timestamp()) assert listed["data"][0]["active_config_snapshot"]["created_at"] == int(version_created_at.timestamp()) @@ -886,13 +912,19 @@ class TestListWorkflowsReferencingAppAgent: def test_groups_bindings_by_workflow_app_and_sorts_by_name(self): agent = SimpleNamespace(id="agent-1") bindings = [ - SimpleNamespace(app_id="wf-app-1", workflow_id="wf-1", node_id="node-b"), - SimpleNamespace(app_id="wf-app-1", workflow_id="wf-1", node_id="node-a"), - SimpleNamespace(app_id="wf-app-2", workflow_id="wf-2", node_id="node-a"), + SimpleNamespace( + agent_id="agent-1", app_id="wf-app-1", workflow_id="wf-1", workflow_version="v1", node_id="node-b" + ), + SimpleNamespace( + agent_id="agent-1", app_id="wf-app-1", workflow_id="wf-1", workflow_version="v1", node_id="node-a" + ), + SimpleNamespace( + agent_id="agent-1", app_id="wf-app-2", workflow_id="wf-2", workflow_version="v2", node_id="node-a" + ), ] apps = [ - SimpleNamespace(id="wf-app-1", name="Beta Flow", mode="workflow"), - SimpleNamespace(id="wf-app-2", name="Alpha Flow", mode="advanced-chat"), + SimpleNamespace(id="wf-app-1", name="Beta Flow", mode="workflow", workflow_id="wf-1"), + SimpleNamespace(id="wf-app-2", name="Alpha Flow", mode="advanced-chat", workflow_id="wf-2"), ] # scalar -> backing agent; scalars -> bindings, then resolved apps. session = FakeSession(scalar=[agent], scalars=[bindings, apps]) @@ -904,6 +936,7 @@ class TestListWorkflowsReferencingAppAgent: beta = next(r for r in result if r["app_id"] == "wf-app-1") assert beta["node_ids"] == ["node-a", "node-b"] # deduped + sorted assert beta["workflow_id"] == "wf-1" + assert beta["workflow_version"] == "v1" def test_returns_empty_when_no_backing_agent(self): session = FakeSession() # scalar() -> None @@ -920,12 +953,100 @@ class TestListWorkflowsReferencingAppAgent: def test_skips_orphaned_binding_whose_app_is_gone(self): agent = SimpleNamespace(id="agent-1") - bindings = [SimpleNamespace(app_id="wf-app-gone", workflow_id="wf-9", node_id="node-a")] + bindings = [ + SimpleNamespace( + agent_id="agent-1", app_id="wf-app-gone", workflow_id="wf-9", workflow_version="v9", node_id="node-a" + ) + ] session = FakeSession(scalar=[agent], scalars=[bindings, []]) # no apps resolved service = AgentRosterService(session) assert service.list_workflows_referencing_app_agent(tenant_id="tenant-1", app_id="app-1") == [] + def test_skips_historical_published_workflow_versions(self): + agent = SimpleNamespace(id="agent-1") + bindings = [ + SimpleNamespace( + agent_id="agent-1", app_id="wf-app-1", workflow_id="old-wf", workflow_version="old", node_id="old" + ), + SimpleNamespace( + agent_id="agent-1", app_id="wf-app-1", workflow_id="current-wf", workflow_version="v2", node_id="new" + ), + ] + apps = [SimpleNamespace(id="wf-app-1", name="Flow", mode="workflow", workflow_id="current-wf")] + session = FakeSession(scalar=[agent], scalars=[bindings, apps]) + service = AgentRosterService(session) + + result = service.list_workflows_referencing_app_agent(tenant_id="tenant-1", app_id="app-1") + + assert len(result) == 1 + assert result[0]["workflow_id"] == "current-wf" + assert result[0]["node_ids"] == ["new"] + + +class TestWorkflowAgentDraftBindingSync: + def test_creates_roster_binding_from_agent_node_graph(self): + workflow = Workflow( + id="workflow-1", + tenant_id="tenant-1", + app_id="app-1", + version=Workflow.VERSION_DRAFT, + graph='{"nodes":[{"id":"agent-node","data":{"type":"agent","version":"2","agent_binding":{"binding_type":"roster_agent","agent_id":"agent-1"}}}]}', + ) + agent = Agent( + id="agent-1", + tenant_id="tenant-1", + name="Agent", + agent_kind=AgentKind.DIFY_AGENT, + scope=AgentScope.ROSTER, + source=AgentSource.AGENT_APP, + status=AgentStatus.ACTIVE, + active_config_snapshot_id="snapshot-2", + ) + session = FakeSession(scalar=[agent], scalars=[[]]) + + WorkflowAgentPublishService.sync_roster_agent_bindings_for_draft( + session=session, + draft_workflow=workflow, + account_id="account-1", + ) + + binding = next(item for item in session.added if isinstance(item, WorkflowAgentNodeBinding)) + assert binding.binding_type == WorkflowAgentBindingType.ROSTER_AGENT + assert binding.agent_id == "agent-1" + assert binding.current_snapshot_id == "snapshot-2" + assert binding.node_job_config_dict == WorkflowNodeJobConfig().model_dump(mode="json") + + def test_deletes_draft_binding_when_agent_node_removed(self): + workflow = Workflow( + id="workflow-1", + tenant_id="tenant-1", + app_id="app-1", + version=Workflow.VERSION_DRAFT, + graph='{"nodes":[]}', + ) + stale_binding = WorkflowAgentNodeBinding( + id="binding-1", + tenant_id="tenant-1", + app_id="app-1", + workflow_id="workflow-1", + workflow_version=Workflow.VERSION_DRAFT, + node_id="removed-node", + binding_type=WorkflowAgentBindingType.ROSTER_AGENT, + agent_id="agent-1", + current_snapshot_id="snapshot-1", + node_job_config=WorkflowNodeJobConfig(), + ) + session = FakeSession(scalars=[[stale_binding]]) + + WorkflowAgentPublishService.sync_roster_agent_bindings_for_draft( + session=session, + draft_workflow=workflow, + account_id="account-1", + ) + + assert session.deleted == [stale_binding] + def test_dataset_rows_filters_malformed_ids(monkeypatch): """Mention ids are user-editable text: a non-UUID id must read as missing diff --git a/packages/contracts/generated/api/console/agents/types.gen.ts b/packages/contracts/generated/api/console/agents/types.gen.ts index 9b3eab813d8..4aa49fc18f6 100644 --- a/packages/contracts/generated/api/console/agents/types.gen.ts +++ b/packages/contracts/generated/api/console/agents/types.gen.ts @@ -19,6 +19,7 @@ export type RosterAgentCreatePayload = { icon_background?: string | null icon_type?: AgentIconType name: string + role?: string version_note?: string | null } @@ -37,6 +38,10 @@ export type AgentRosterResponse = { icon_type?: AgentIconType id: string name: string + published_node_reference_count?: number + published_reference_count?: number + published_references?: Array + role?: string scope: AgentScope source: AgentSource status: AgentStatus @@ -60,6 +65,7 @@ export type RosterAgentUpdatePayload = { icon_background?: string | null icon_type?: AgentIconType name?: string | null + role?: string | null } export type AgentConfigSnapshotListResponse = { @@ -108,6 +114,15 @@ export type AgentConfigSnapshotSummaryResponse = { export type AgentKind = 'dify_agent' +export type AgentPublishedReferenceResponse = { + app_id: string + app_mode: string + app_name: string + node_ids?: Array + workflow_id: string + workflow_version: string +} + export type AgentScope = 'roster' | 'workflow_only' export type AgentSource = 'agent_app' | 'imported' | 'system' | 'workflow' @@ -132,6 +147,10 @@ export type AgentInviteOptionResponse = { in_current_workflow_count?: number is_in_current_workflow?: boolean name: string + published_node_reference_count?: number + published_reference_count?: number + published_references?: Array + role?: string scope: AgentScope source: AgentSource status: AgentStatus diff --git a/packages/contracts/generated/api/console/agents/zod.gen.ts b/packages/contracts/generated/api/console/agents/zod.gen.ts index eeb6c01dc40..35de844cf2d 100644 --- a/packages/contracts/generated/api/console/agents/zod.gen.ts +++ b/packages/contracts/generated/api/console/agents/zod.gen.ts @@ -18,6 +18,7 @@ export const zRosterAgentUpdatePayload = z.object({ icon_background: z.string().max(255).nullish(), icon_type: zAgentIconType.optional(), name: z.string().min(1).max(255).nullish(), + role: z.string().max(255).nullish(), }) /** @@ -50,6 +51,18 @@ export const zAgentConfigSnapshotListResponse = z.object({ */ export const zAgentKind = z.enum(['dify_agent']) +/** + * AgentPublishedReferenceResponse + */ +export const zAgentPublishedReferenceResponse = z.object({ + app_id: z.string(), + app_mode: z.string(), + app_name: z.string(), + node_ids: z.array(z.string()).optional(), + workflow_id: z.string(), + workflow_version: z.string(), +}) + /** * AgentScope * @@ -89,6 +102,10 @@ export const zAgentRosterResponse = z.object({ icon_type: zAgentIconType.optional(), id: z.string(), name: z.string(), + published_node_reference_count: z.int().optional().default(0), + published_reference_count: z.int().optional().default(0), + published_references: z.array(zAgentPublishedReferenceResponse).optional(), + role: z.string().optional().default(''), scope: zAgentScope, source: zAgentSource, status: zAgentStatus, @@ -130,6 +147,10 @@ export const zAgentInviteOptionResponse = z.object({ in_current_workflow_count: z.int().optional().default(0), is_in_current_workflow: z.boolean().optional().default(false), name: z.string(), + published_node_reference_count: z.int().optional().default(0), + published_reference_count: z.int().optional().default(0), + published_references: z.array(zAgentPublishedReferenceResponse).optional(), + role: z.string().optional().default(''), scope: zAgentScope, source: zAgentSource, status: zAgentStatus, @@ -632,6 +653,7 @@ export const zRosterAgentCreatePayload = z.object({ icon_background: z.string().max(255).nullish(), icon_type: zAgentIconType.optional(), name: z.string().min(1).max(255), + role: z.string().max(255).optional().default(''), version_note: z.string().nullish(), }) From 3575a3d1b3538c713e50508a060f11ded6ab15b1 Mon Sep 17 00:00:00 2001 From: jashwanth_reddy_gummula Date: Fri, 12 Jun 2026 09:26:56 +0530 Subject: [PATCH 039/122] chore(api): clean redundant type ignores (Fixes #24494) (#37358) Co-authored-by: JASHWANTH REDDY GUMMULA --- api/app.py | 2 +- api/app_factory.py | 2 +- api/celery_entrypoint.py | 4 ++-- api/gunicorn.conf.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api/app.py b/api/app.py index e53b037be50..7b7fa58c22f 100644 --- a/api/app.py +++ b/api/app.py @@ -55,7 +55,7 @@ else: if __name__ == "__main__": from gevent import pywsgi - from geventwebsocket.handler import WebSocketHandler # type: ignore[reportMissingTypeStubs] + from geventwebsocket.handler import WebSocketHandler log_startup_banner(HOST, PORT) server = pywsgi.WSGIServer((HOST, PORT), socketio_app, handler_class=WebSocketHandler) diff --git a/api/app_factory.py b/api/app_factory.py index 49be0257311..2cea8cfb3f7 100644 --- a/api/app_factory.py +++ b/api/app_factory.py @@ -1,7 +1,7 @@ import logging import time -import socketio # type: ignore[reportMissingTypeStubs] +import socketio from flask import request from opentelemetry.trace import get_current_span from opentelemetry.trace.span import INVALID_SPAN_ID, INVALID_TRACE_ID diff --git a/api/celery_entrypoint.py b/api/celery_entrypoint.py index 28fa0972e8a..339f03c9916 100644 --- a/api/celery_entrypoint.py +++ b/api/celery_entrypoint.py @@ -1,5 +1,5 @@ -import psycogreen.gevent as pscycogreen_gevent # type: ignore -from grpc.experimental import gevent as grpc_gevent # type: ignore +import psycogreen.gevent as pscycogreen_gevent +from grpc.experimental import gevent as grpc_gevent # grpc gevent grpc_gevent.init_gevent() diff --git a/api/gunicorn.conf.py b/api/gunicorn.conf.py index da75d25ba67..140b70c67d0 100644 --- a/api/gunicorn.conf.py +++ b/api/gunicorn.conf.py @@ -1,6 +1,6 @@ -import psycogreen.gevent as pscycogreen_gevent # type: ignore +import psycogreen.gevent as pscycogreen_gevent from gevent import events as gevent_events -from grpc.experimental import gevent as grpc_gevent # type: ignore +from grpc.experimental import gevent as grpc_gevent # WARNING: This module is loaded very early in the Gunicorn worker lifecycle, # before gevent's monkey-patching is applied. Importing modules at the top level here can From c69abf16ae7b0ea0dbae941416df86491b61cd6f Mon Sep 17 00:00:00 2001 From: Jingyi Date: Thu, 11 Jun 2026 21:42:43 -0700 Subject: [PATCH 040/122] feat(workflow): update start node UI (#37348) --- eslint-suppressions.json | 30 -- .../assets/vender/workflow/marketplace.svg | 3 + .../vender/workflow/start-placeholder.svg | 8 + .../assets/vender/workflow/user-input.svg | 7 + .../custom-vender/icons.json | 17 +- .../custom-vender/info.json | 2 +- .../__tests__/workflow-main.spec.tsx | 51 +++ .../workflow-app/components/workflow-main.tsx | 15 +- .../__tests__/use-auto-onboarding.spec.ts | 27 ++ .../use-available-nodes-meta-data.spec.ts | 1 + .../__tests__/use-nodes-sync-draft.spec.ts | 37 ++- ...se-workflow-draft-graph-for-canvas.spec.ts | 103 ++++++ .../hooks/__tests__/use-workflow-init.spec.ts | 132 ++++++-- .../use-workflow-refresh-draft.spec.ts | 79 +++++ .../__tests__/use-workflow-start-run.spec.tsx | 17 + .../__tests__/use-workflow-template.spec.ts | 42 ++- .../workflow-app/hooks/use-auto-onboarding.ts | 4 + .../hooks/use-available-nodes-meta-data.ts | 2 + .../hooks/use-nodes-sync-draft.ts | 11 +- .../use-workflow-draft-graph-for-canvas.ts | 76 +++++ .../workflow-app/hooks/use-workflow-init.ts | 35 +- .../hooks/use-workflow-refresh-draft.ts | 17 +- .../hooks/use-workflow-start-run.tsx | 3 + .../hooks/use-workflow-template.ts | 46 ++- .../workflow/__tests__/block-icon.spec.tsx | 2 +- web/app/components/workflow/block-icon.tsx | 36 +++ .../__tests__/all-start-blocks.spec.tsx | 163 +++++++++- .../__tests__/featured-triggers.spec.tsx | 32 ++ .../block-selector/__tests__/hooks.spec.tsx | 20 +- .../block-selector/__tests__/index.spec.tsx | 2 + .../block-selector/__tests__/main.spec.tsx | 89 +++++- .../__tests__/start-blocks.spec.tsx | 68 +++- .../block-selector/all-start-blocks.tsx | 158 ++++++--- .../block-selector/block-selector-row.tsx | 81 +++++ .../block-selector/featured-triggers.tsx | 74 ++--- .../workflow/block-selector/hooks.ts | 18 +- .../workflow/block-selector/index.tsx | 3 + .../workflow/block-selector/main.tsx | 11 +- .../__tests__/item-list.spec.tsx | 4 +- .../market-place-plugin/item.tsx | 9 +- .../market-place-plugin/list.tsx | 17 +- .../workflow/block-selector/start-blocks.tsx | 115 +++++-- .../workflow/block-selector/tabs.tsx | 38 ++- .../__tests__/trigger-plugin.spec.tsx | 58 ++++ .../trigger-plugin/action-item.tsx | 11 +- .../block-selector/trigger-plugin/item.tsx | 62 +++- .../block-selector/trigger-plugin/list.tsx | 3 + .../__tests__/use-available-blocks.spec.ts | 20 ++ .../hooks/__tests__/use-checklist.spec.ts | 25 ++ .../workflow/hooks/use-available-blocks.ts | 11 +- .../workflow/hooks/use-checklist.ts | 6 +- .../workflow/hooks/use-inspect-vars-crud.ts | 3 +- .../workflow/nodes/__tests__/index.spec.tsx | 62 +++- .../_base/__tests__/node-sections.spec.tsx | 5 +- .../_base/__tests__/node.helpers.spec.ts | 1 + .../_base/components/workflow-panel/index.tsx | 300 ++++++++++-------- .../workflow-panel/last-run/use-last-run.ts | 2 + .../start-placeholder-panel.tsx | 34 ++ .../workflow/nodes/_base/node-sections.tsx | 2 +- .../workflow/nodes/_base/node.helpers.tsx | 2 +- .../components/workflow/nodes/_base/node.tsx | 6 +- .../components/workflow/nodes/components.ts | 4 + web/app/components/workflow/nodes/index.tsx | 2 +- .../start-placeholder/__tests__/node.spec.tsx | 31 ++ .../__tests__/panel.spec.tsx | 139 ++++++++ .../nodes/start-placeholder/default.ts | 29 ++ .../workflow/nodes/start-placeholder/node.tsx | 25 ++ .../nodes/start-placeholder/panel.tsx | 167 ++++++++++ .../workflow/nodes/start-placeholder/types.ts | 3 + .../operator/__tests__/add-block.spec.tsx | 17 + .../workflow/operator/add-block.tsx | 16 +- web/app/components/workflow/types.ts | 1 + web/i18n/ar-TN/workflow.json | 16 + web/i18n/de-DE/workflow.json | 16 + web/i18n/en-US/workflow.json | 16 + web/i18n/es-ES/workflow.json | 16 + web/i18n/fa-IR/workflow.json | 16 + web/i18n/fr-FR/workflow.json | 16 + web/i18n/hi-IN/workflow.json | 16 + web/i18n/id-ID/workflow.json | 16 + web/i18n/it-IT/workflow.json | 16 + web/i18n/ja-JP/workflow.json | 16 + web/i18n/ko-KR/workflow.json | 16 + web/i18n/nl-NL/workflow.json | 16 + web/i18n/pl-PL/workflow.json | 16 + web/i18n/pt-BR/workflow.json | 16 + web/i18n/ro-RO/workflow.json | 16 + web/i18n/ru-RU/workflow.json | 16 + web/i18n/sl-SI/workflow.json | 16 + web/i18n/th-TH/workflow.json | 16 + web/i18n/tr-TR/workflow.json | 16 + web/i18n/uk-UA/workflow.json | 16 + web/i18n/vi-VN/workflow.json | 16 + web/i18n/zh-Hans/workflow.json | 16 + web/i18n/zh-Hant/workflow.json | 16 + 95 files changed, 2673 insertions(+), 447 deletions(-) create mode 100644 packages/iconify-collections/assets/vender/workflow/marketplace.svg create mode 100644 packages/iconify-collections/assets/vender/workflow/start-placeholder.svg create mode 100644 packages/iconify-collections/assets/vender/workflow/user-input.svg create mode 100644 web/app/components/workflow-app/hooks/__tests__/use-workflow-draft-graph-for-canvas.spec.ts create mode 100644 web/app/components/workflow-app/hooks/use-workflow-draft-graph-for-canvas.ts create mode 100644 web/app/components/workflow/block-selector/block-selector-row.tsx create mode 100644 web/app/components/workflow/nodes/_base/components/workflow-panel/start-placeholder-panel.tsx create mode 100644 web/app/components/workflow/nodes/start-placeholder/__tests__/node.spec.tsx create mode 100644 web/app/components/workflow/nodes/start-placeholder/__tests__/panel.spec.tsx create mode 100644 web/app/components/workflow/nodes/start-placeholder/default.ts create mode 100644 web/app/components/workflow/nodes/start-placeholder/node.tsx create mode 100644 web/app/components/workflow/nodes/start-placeholder/panel.tsx create mode 100644 web/app/components/workflow/nodes/start-placeholder/types.ts diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 502ce2933f3..e10b1811574 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -5206,12 +5206,6 @@ "web/app/components/workflow/block-selector/market-place-plugin/item.tsx": { "erasable-syntax-only/enums": { "count": 1 - }, - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 } }, "web/app/components/workflow/block-selector/market-place-plugin/list.tsx": { @@ -5243,14 +5237,6 @@ "count": 1 } }, - "web/app/components/workflow/block-selector/start-blocks.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 - } - }, "web/app/components/workflow/block-selector/tabs.tsx": { "jsx-a11y/click-events-have-key-events": { "count": 2 @@ -5280,22 +5266,6 @@ "count": 2 } }, - "web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 - } - }, - "web/app/components/workflow/block-selector/trigger-plugin/item.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 - } - }, "web/app/components/workflow/block-selector/types.ts": { "erasable-syntax-only/enums": { "count": 4 diff --git a/packages/iconify-collections/assets/vender/workflow/marketplace.svg b/packages/iconify-collections/assets/vender/workflow/marketplace.svg new file mode 100644 index 00000000000..f04fe04e3f4 --- /dev/null +++ b/packages/iconify-collections/assets/vender/workflow/marketplace.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/vender/workflow/start-placeholder.svg b/packages/iconify-collections/assets/vender/workflow/start-placeholder.svg new file mode 100644 index 00000000000..f28e12d7ec3 --- /dev/null +++ b/packages/iconify-collections/assets/vender/workflow/start-placeholder.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/iconify-collections/assets/vender/workflow/user-input.svg b/packages/iconify-collections/assets/vender/workflow/user-input.svg new file mode 100644 index 00000000000..80157ca000e --- /dev/null +++ b/packages/iconify-collections/assets/vender/workflow/user-input.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/iconify-collections/custom-vender/icons.json b/packages/iconify-collections/custom-vender/icons.json index c8427ff479d..f85c44d912d 100644 --- a/packages/iconify-collections/custom-vender/icons.json +++ b/packages/iconify-collections/custom-vender/icons.json @@ -1,6 +1,6 @@ { "prefix": "custom-vender", - "lastModified": 1776670621, + "lastModified": 1781036531, "icons": { "features-citations": { "body": "" @@ -1084,6 +1084,11 @@ "width": 16, "height": 16 }, + "workflow-marketplace": { + "body": "", + "width": 12, + "height": 12 + }, "workflow-parameter-extractor": { "body": "" }, @@ -1095,12 +1100,22 @@ "width": 16, "height": 16 }, + "workflow-start-placeholder": { + "body": "", + "width": 13.3333, + "height": 13.3333 + }, "workflow-templating-transform": { "body": "" }, "workflow-trigger-all": { "body": "" }, + "workflow-user-input": { + "body": "", + "width": 16, + "height": 16 + }, "workflow-variable-x": { "body": "" }, diff --git a/packages/iconify-collections/custom-vender/info.json b/packages/iconify-collections/custom-vender/info.json index 52df22b171f..f08b18fcad6 100644 --- a/packages/iconify-collections/custom-vender/info.json +++ b/packages/iconify-collections/custom-vender/info.json @@ -1,7 +1,7 @@ { "prefix": "custom-vender", "name": "Dify Custom Vender", - "total": 281, + "total": 284, "version": "0.0.0-private", "author": { "name": "LangGenius, Inc.", diff --git a/web/app/components/workflow-app/components/__tests__/workflow-main.spec.tsx b/web/app/components/workflow-app/components/__tests__/workflow-main.spec.tsx index d16da63b93d..956c95fa754 100644 --- a/web/app/components/workflow-app/components/__tests__/workflow-main.spec.tsx +++ b/web/app/components/workflow-app/components/__tests__/workflow-main.spec.tsx @@ -2,6 +2,7 @@ import type { ReactNode } from 'react' import type { WorkflowProps } from '@/app/components/workflow' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type' +import { BlockEnum } from '@/app/components/workflow/types' import WorkflowMain from '../workflow-main' const mockSetFeatures = vi.fn() @@ -91,6 +92,12 @@ vi.mock('@/app/components/workflow/store', () => ({ }), })) +vi.mock('@/app/components/app/store', () => ({ + useStore: (selector: (state: { appDetail: { mode: string } }) => T) => selector({ + appDetail: { mode: 'workflow' }, + }), +})) + vi.mock('reactflow', () => ({ useReactFlow: () => ({ getNodes: () => [], @@ -260,6 +267,18 @@ vi.mock('@/app/components/workflow-app/hooks', () => ({ }), })) +vi.mock('@/app/components/workflow-app/hooks/use-workflow-draft-graph-for-canvas', () => ({ + useWorkflowDraftGraphForCanvas: () => ({ + getWorkflowDraftGraphForCanvas: (graph?: { nodes?: unknown[], edges?: unknown[], viewport?: unknown }) => ({ + nodes: graph?.nodes?.length + ? graph.nodes + : [{ id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } }], + edges: graph?.edges || [], + viewport: graph?.viewport || { x: 0, y: 0, zoom: 1 }, + }), + }), +})) + vi.mock('../workflow-children', () => ({ default: () =>
workflow-children
, })) @@ -460,4 +479,36 @@ describe('WorkflowMain', () => { }) }) }) + + it('restores a local start placeholder for empty collaboration workflow updates', async () => { + collaborationRuntime.isEnabled = true + mockFetchWorkflowDraft.mockResolvedValue({ + features: {}, + conversation_variables: [], + environment_variables: [], + graph: { + nodes: [], + edges: [], + viewport: { x: 0, y: 0, zoom: 1 }, + }, + }) + + render( + , + ) + + await collaborationListeners.workflowUpdate?.() + + await waitFor(() => { + expect(mockHandleUpdateWorkflowCanvas).toHaveBeenCalledWith({ + nodes: [{ id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } }], + edges: [], + viewport: { x: 0, y: 0, zoom: 1 }, + }) + }) + }) }) diff --git a/web/app/components/workflow-app/components/workflow-main.tsx b/web/app/components/workflow-app/components/workflow-main.tsx index 1d28d78d22e..08fc2e8daf6 100644 --- a/web/app/components/workflow-app/components/workflow-main.tsx +++ b/web/app/components/workflow-app/components/workflow-main.tsx @@ -11,9 +11,11 @@ import { useRef, } from 'react' import { useReactFlow } from 'reactflow' +import { useStore as useAppStore } from '@/app/components/app/store' import { useFeaturesStore } from '@/app/components/base/features/hooks' import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants' import { WorkflowWithInnerContext } from '@/app/components/workflow' +import { useWorkflowDraftGraphForCanvas } from '@/app/components/workflow-app/hooks/use-workflow-draft-graph-for-canvas' import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager' import { useCollaboration } from '@/app/components/workflow/collaboration/hooks/use-collaboration' import { useWorkflowUpdate } from '@/app/components/workflow/hooks/use-workflow-interactions' @@ -44,8 +46,10 @@ const WorkflowMain = ({ const featuresStore = useFeaturesStore() const workflowStore = useWorkflowStore() const appId = useStore(s => s.appId) + const appDetail = useAppStore(s => s.appDetail) const containerRef = useRef(null) const reactFlow = useReactFlow() + const { getWorkflowDraftGraphForCanvas } = useWorkflowDraftGraphForCanvas(appDetail?.mode) const reactFlowStore = useMemo(() => ({ getState: () => ({ @@ -175,13 +179,8 @@ const WorkflowMain = ({ handleWorkflowDataUpdate(response) // Update workflow canvas (nodes, edges, viewport) - if (response.graph) { - handleUpdateWorkflowCanvas({ - nodes: response.graph.nodes || [], - edges: response.graph.edges || [], - viewport: response.graph.viewport || { x: 0, y: 0, zoom: 1 }, - }) - } + if (response.graph) + handleUpdateWorkflowCanvas(getWorkflowDraftGraphForCanvas(response.graph)) } catch (error) { console.error('Failed to fetch updated workflow:', error) @@ -189,7 +188,7 @@ const WorkflowMain = ({ }) return unsubscribe - }, [appId, handleWorkflowDataUpdate, handleUpdateWorkflowCanvas, isCollaborationEnabled]) + }, [appId, getWorkflowDraftGraphForCanvas, handleWorkflowDataUpdate, handleUpdateWorkflowCanvas, isCollaborationEnabled]) // Listen for sync requests from other users (only processed by leader) useEffect(() => { diff --git a/web/app/components/workflow-app/hooks/__tests__/use-auto-onboarding.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-auto-onboarding.spec.ts index f8119026186..0d7d2b14994 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-auto-onboarding.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-auto-onboarding.spec.ts @@ -32,6 +32,7 @@ describe('useAutoOnboarding', () => { showOnboarding: false, hasShownOnboarding: false, notInitialWorkflow: false, + isWorkflowDataLoaded: true, setShowOnboarding: mockSetShowOnboarding, setHasShownOnboarding: mockSetHasShownOnboarding, setShouldAutoOpenStartNodeSelector: mockSetShouldAutoOpenStartNodeSelector, @@ -56,11 +57,36 @@ describe('useAutoOnboarding', () => { expect(mockSetShouldAutoOpenStartNodeSelector).toHaveBeenCalledWith(true) }) + it('should skip auto onboarding before workflow data is loaded', () => { + mockWorkflowStore.getState.mockReturnValue({ + showOnboarding: false, + hasShownOnboarding: false, + notInitialWorkflow: false, + isWorkflowDataLoaded: false, + setShowOnboarding: mockSetShowOnboarding, + setHasShownOnboarding: mockSetHasShownOnboarding, + setShouldAutoOpenStartNodeSelector: mockSetShouldAutoOpenStartNodeSelector, + hasSelectedStartNode: false, + setHasSelectedStartNode: mockSetHasSelectedStartNode, + }) + + renderHook(() => useAutoOnboarding()) + + act(() => { + vi.advanceTimersByTime(500) + }) + + expect(mockSetShowOnboarding).not.toHaveBeenCalled() + expect(mockSetHasShownOnboarding).not.toHaveBeenCalled() + expect(mockSetShouldAutoOpenStartNodeSelector).not.toHaveBeenCalled() + }) + it('should skip auto onboarding when it is already visible or the workflow is not initial', () => { mockWorkflowStore.getState.mockReturnValue({ showOnboarding: true, hasShownOnboarding: false, notInitialWorkflow: true, + isWorkflowDataLoaded: true, setShowOnboarding: mockSetShowOnboarding, setHasShownOnboarding: mockSetHasShownOnboarding, setShouldAutoOpenStartNodeSelector: mockSetShouldAutoOpenStartNodeSelector, @@ -84,6 +110,7 @@ describe('useAutoOnboarding', () => { showOnboarding: false, hasShownOnboarding: true, notInitialWorkflow: false, + isWorkflowDataLoaded: true, setShowOnboarding: mockSetShowOnboarding, setHasShownOnboarding: mockSetHasShownOnboarding, setShouldAutoOpenStartNodeSelector: mockSetShouldAutoOpenStartNodeSelector, diff --git a/web/app/components/workflow-app/hooks/__tests__/use-available-nodes-meta-data.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-available-nodes-meta-data.spec.ts index c92e438cb3c..a53f0cac380 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-available-nodes-meta-data.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-available-nodes-meta-data.spec.ts @@ -37,6 +37,7 @@ describe('useAvailableNodesMetaData', () => { expect(result.current.nodesMap?.[BlockEnum.Start]?.metaData.isUndeletable).toBe(false) expect(result.current.nodesMap?.[BlockEnum.End]).toBeDefined() + expect(result.current.nodesMap?.[BlockEnum.StartPlaceholder]).toBeDefined() expect(result.current.nodesMap?.[BlockEnum.TriggerWebhook]).toBeDefined() expect(result.current.nodesMap?.[BlockEnum.TriggerSchedule]).toBeDefined() expect(result.current.nodesMap?.[BlockEnum.TriggerPlugin]).toBeDefined() diff --git a/web/app/components/workflow-app/hooks/__tests__/use-nodes-sync-draft.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-nodes-sync-draft.spec.ts index d9e9cbdb8db..4b54a4e0da7 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-nodes-sync-draft.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-nodes-sync-draft.spec.ts @@ -2,6 +2,7 @@ import { act } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { renderHookWithSystemFeatures } from '@/__tests__/utils/mock-system-features' +import { BlockEnum } from '@/app/components/workflow/types' import { useNodesSyncDraft } from '../use-nodes-sync-draft' const mockGetNodes = vi.fn() @@ -134,7 +135,7 @@ describe('useNodesSyncDraft — handleRefreshWorkflowDraft(true) on 409', () => }, } mockGetNodesReadOnly.mockReturnValue(false) - mockGetNodes.mockReturnValue([{ id: 'n1', position: { x: 0, y: 0 }, data: { type: 'start' } }]) + mockGetNodes.mockReturnValue([{ id: 'n1', position: { x: 0, y: 0 }, data: { type: BlockEnum.Start } }]) mockSyncWorkflowDraft.mockResolvedValue({ hash: 'new', updated_at: 1 }) mockCollaborationIsConnected.mockReturnValue(false) mockCollaborationGetIsLeader.mockReturnValue(true) @@ -199,13 +200,15 @@ describe('useNodesSyncDraft — handleRefreshWorkflowDraft(true) on 409', () => ...reactFlowState, edges: [ { id: 'edge-1', source: 'n1', target: 'n2', data: { _isTemp: false, _private: 'drop', stable: 'keep' } }, + { id: 'placeholder-edge', source: 'start-placeholder', target: 'n1', data: { stable: 'drop' } }, { id: 'temp-edge', source: 'n2', target: 'n3', data: { _isTemp: true } }, ], transform: [10, 20, 1.5], } mockGetNodes.mockReturnValue([ - { id: 'n1', position: { x: 0, y: 0 }, data: { type: 'start', _tempField: 'drop', label: 'Start' } }, - { id: 'temp-node', position: { x: 1, y: 1 }, data: { type: 'answer', _isTempNode: true } }, + { id: 'n1', position: { x: 0, y: 0 }, data: { type: BlockEnum.Start, _tempField: 'drop', label: 'Start' } }, + { id: 'start-placeholder', position: { x: 1, y: 1 }, data: { type: BlockEnum.StartPlaceholder } }, + { id: 'temp-node', position: { x: 2, y: 2 }, data: { type: BlockEnum.Answer, _isTempNode: true } }, ]) workflowStoreState = { ...workflowStoreState, @@ -241,7 +244,7 @@ describe('useNodesSyncDraft — handleRefreshWorkflowDraft(true) on 409', () => url: '/apps/app-1/workflows/draft', params: { graph: { - nodes: [{ id: 'n1', position: { x: 0, y: 0 }, data: { type: 'start', label: 'Start' } }], + nodes: [{ id: 'n1', position: { x: 0, y: 0 }, data: { type: BlockEnum.Start, label: 'Start' } }], edges: [{ id: 'edge-1', source: 'n1', target: 'n2', data: { stable: 'keep' } }], viewport: { x: 10, y: 20, zoom: 1.5 }, }, @@ -292,6 +295,32 @@ describe('useNodesSyncDraft — handleRefreshWorkflowDraft(true) on 409', () => })) }) + it('should not post the local start placeholder when the page closes', () => { + reactFlowState = { + ...reactFlowState, + edges: [ + { id: 'placeholder-edge', source: 'start-placeholder', target: 'n1', data: {} }, + ], + } + mockGetNodes.mockReturnValue([ + { id: 'start-placeholder', position: { x: 0, y: 0 }, data: { type: BlockEnum.StartPlaceholder } }, + { id: 'n1', position: { x: 1, y: 1 }, data: { type: BlockEnum.Start } }, + ]) + + const { result } = renderUseNodesSyncDraft() + + act(() => { + result.current.syncWorkflowDraftWhenPageClose() + }) + + expect(mockPostWithKeepalive).toHaveBeenCalledWith('/api/apps/app-1/workflows/draft', expect.objectContaining({ + graph: expect.objectContaining({ + nodes: [{ id: 'n1', position: { x: 1, y: 1 }, data: { type: BlockEnum.Start } }], + edges: [], + }), + })) + }) + it('should emit sync request instead of syncing when current user is collaboration follower', async () => { isCollaborationEnabled = true mockCollaborationIsConnected.mockReturnValue(true) diff --git a/web/app/components/workflow-app/hooks/__tests__/use-workflow-draft-graph-for-canvas.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-workflow-draft-graph-for-canvas.spec.ts new file mode 100644 index 00000000000..71ac93add53 --- /dev/null +++ b/web/app/components/workflow-app/hooks/__tests__/use-workflow-draft-graph-for-canvas.spec.ts @@ -0,0 +1,103 @@ +import { renderHook } from '@testing-library/react' +import { BlockEnum } from '@/app/components/workflow/types' +import { AppModeEnum } from '@/types/app' +import { useWorkflowDraftGraphForCanvas } from '../use-workflow-draft-graph-for-canvas' + +let generateNewNodeCalls: Array> = [] + +vi.mock('@/app/components/workflow/utils', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + generateNewNode: (args: { data: Record, position: Record }) => { + generateNewNodeCalls.push(args) + return { + newNode: { + id: `generated-${generateNewNodeCalls.length}`, + type: 'custom', + data: args.data, + position: args.position, + }, + } + }, + } +}) + +describe('useWorkflowDraftGraphForCanvas', () => { + beforeEach(() => { + vi.clearAllMocks() + generateNewNodeCalls = [] + }) + + it('should restore a local start placeholder for workflow graphs without an entry node', () => { + const { result } = renderHook(() => useWorkflowDraftGraphForCanvas(AppModeEnum.WORKFLOW)) + + const graph = result.current.getWorkflowDraftGraphForCanvas({ + nodes: [], + edges: [], + }) + + expect(graph).toMatchObject({ + edges: [], + viewport: { x: 0, y: 0, zoom: 1 }, + }) + expect(graph.nodes).toHaveLength(1) + expect(graph.nodes[0]).toMatchObject({ + data: { + type: BlockEnum.StartPlaceholder, + title: 'workflow.blocks.start-placeholder', + desc: '', + selected: true, + }, + }) + }) + + it.each([ + BlockEnum.Start, + BlockEnum.TriggerSchedule, + BlockEnum.TriggerWebhook, + BlockEnum.TriggerPlugin, + BlockEnum.StartPlaceholder, + ])('should preserve existing %s entry nodes', (type) => { + const node = { id: 'entry', data: { type } } + const { result } = renderHook(() => useWorkflowDraftGraphForCanvas(AppModeEnum.WORKFLOW)) + + const graph = result.current.getWorkflowDraftGraphForCanvas({ + nodes: [node] as never, + edges: [], + }) + + expect(graph.nodes).toEqual([node]) + expect(generateNewNodeCalls).toHaveLength(0) + }) + + it('should not restore a start placeholder for non-workflow app modes', () => { + const { result } = renderHook(() => useWorkflowDraftGraphForCanvas(AppModeEnum.ADVANCED_CHAT)) + + const graph = result.current.getWorkflowDraftGraphForCanvas({ + nodes: [], + edges: [], + }) + + expect(graph.nodes).toEqual([]) + expect(generateNewNodeCalls).toHaveLength(0) + }) + + it('should reuse the provided local start placeholder template when available', () => { + const localStartPlaceholder = { id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } } + const draftNode = { id: 'llm', data: { type: BlockEnum.LLM } } + const { result } = renderHook(() => useWorkflowDraftGraphForCanvas(AppModeEnum.WORKFLOW)) + + const graph = result.current.getWorkflowDraftGraphForCanvas({ + nodes: [draftNode] as never, + edges: [], + viewport: { x: 1, y: 2, zoom: 0.5 }, + }, { + localStartPlaceholderNodes: [localStartPlaceholder] as never, + }) + + expect(graph.nodes).toEqual([localStartPlaceholder, draftNode]) + expect(graph.viewport).toEqual({ x: 1, y: 2, zoom: 0.5 }) + expect(generateNewNodeCalls).toHaveLength(0) + }) +}) diff --git a/web/app/components/workflow-app/hooks/__tests__/use-workflow-init.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-workflow-init.spec.ts index 4827c5508c7..47ac39c60e2 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-workflow-init.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-workflow-init.spec.ts @@ -14,6 +14,7 @@ const mockWorkflowStoreSetState = vi.fn() const mockWorkflowStoreGetState = vi.fn() const mockFetchNodesDefaultConfigs = vi.fn() const mockFetchPublishedWorkflow = vi.fn() +const mockSyncWorkflowDraft = vi.fn() let appStoreState: { appDetail: { @@ -43,7 +44,12 @@ vi.mock('@/app/components/app/store', () => ({ })) vi.mock('../use-workflow-template', () => ({ - useWorkflowTemplate: () => ({ nodes: [], edges: [] }), + useWorkflowTemplate: () => ({ + nodes: appStoreState.appDetail.mode === 'workflow' + ? [{ id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } }] + : [{ id: 'start', data: { type: BlockEnum.Start } }], + edges: [], + }), })) vi.mock('@/service/use-workflow', () => ({ @@ -55,7 +61,6 @@ vi.mock('@/service/use-workflow', () => ({ })) const mockFetchWorkflowDraft = vi.fn() -const mockSyncWorkflowDraft = vi.fn() vi.mock('@/service/workflow', () => ({ fetchWorkflowDraft: (...args: unknown[]) => mockFetchWorkflowDraft(...args), @@ -71,13 +76,17 @@ const notExistError = () => ({ const draftResponse = { id: 'draft-id', - graph: { nodes: [], edges: [] }, + graph: { + nodes: [{ id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } }], + edges: [], + }, hash: 'server-hash', created_at: 0, created_by: { id: '', name: '', email: '' }, updated_at: 1, updated_by: { id: '', name: '', email: '' }, tool_published: false, + features: { retriever_resource: { enabled: true } }, environment_variables: [], conversation_variables: [], version: '1', @@ -85,7 +94,7 @@ const draftResponse = { marked_comment: '', } -describe('useWorkflowInit — hash fix (draft_workflow_not_exist)', () => { +describe('useWorkflowInit', () => { beforeEach(() => { vi.clearAllMocks() appStoreState = { @@ -103,32 +112,115 @@ describe('useWorkflowInit — hash fix (draft_workflow_not_exist)', () => { mockFetchPublishedWorkflow.mockResolvedValue({ created_at: 0, graph: { nodes: [], edges: [] } }) mockFetchWorkflowDraft .mockRejectedValueOnce(notExistError()) - .mockResolvedValueOnce(draftResponse) - mockSyncWorkflowDraft.mockResolvedValue({ hash: 'new-hash', updated_at: 1 }) + mockSyncWorkflowDraft.mockReset() }) - it('should call setSyncWorkflowDraftHash with hash returned by syncWorkflowDraft', async () => { - renderHook(() => useWorkflowInit()) - await waitFor(() => expect(mockSetSyncWorkflowDraftHash).toHaveBeenCalledWith('new-hash')) - }) - - it('should store hash BEFORE making the recursive fetchWorkflowDraft call', async () => { - const order: string[] = [] - mockSetSyncWorkflowDraftHash.mockImplementation((h: string) => order.push(`hash:${h}`)) + it('should create an empty backend draft and restore a local start placeholder when the workflow draft does not exist', async () => { mockFetchWorkflowDraft .mockReset() .mockRejectedValueOnce(notExistError()) - .mockImplementationOnce(async () => { - order.push('fetch:2') - return draftResponse + .mockResolvedValueOnce({ + ...draftResponse, + graph: { nodes: [], edges: [] }, + hash: 'new-workflow-hash', }) mockSyncWorkflowDraft.mockResolvedValue({ hash: 'new-hash', updated_at: 1 }) + const { result } = renderHook(() => useWorkflowInit()) + + await waitFor(() => { + expect(result.current.data?.graph.nodes).toEqual([ + { id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } }, + ]) + }) + + expect(mockWorkflowStoreSetState).toHaveBeenCalledWith(expect.objectContaining({ + showOnboarding: false, + shouldAutoOpenStartNodeSelector: false, + hasSelectedStartNode: false, + hasShownOnboarding: true, + })) + expect(mockSyncWorkflowDraft).toHaveBeenCalledWith(expect.objectContaining({ + params: expect.objectContaining({ + graph: { + nodes: [], + edges: [], + }, + }), + })) + expect(mockSetSyncWorkflowDraftHash).toHaveBeenCalledWith('new-hash') + expect(mockSetSyncWorkflowDraftHash).toHaveBeenCalledWith('new-workflow-hash') + }) + + it('should keep creating the first backend draft for advanced chat apps', async () => { + appStoreState = { + appDetail: { id: 'app-1', name: 'Test', mode: 'advanced-chat' }, + } + mockFetchWorkflowDraft + .mockReset() + .mockRejectedValueOnce(notExistError()) + .mockResolvedValueOnce(draftResponse) + mockSyncWorkflowDraft.mockResolvedValue({ hash: 'new-hash', updated_at: 1 }) + renderHook(() => useWorkflowInit()) - await waitFor(() => expect(order).toContain('fetch:2')) - expect(order).toContain('hash:new-hash') - expect(order.indexOf('hash:new-hash')).toBeLessThan(order.indexOf('fetch:2')) + await waitFor(() => expect(mockSyncWorkflowDraft).toHaveBeenCalledWith(expect.objectContaining({ + params: expect.objectContaining({ + graph: { + nodes: [{ id: 'start', data: { type: BlockEnum.Start } }], + edges: [], + }, + }), + }))) + expect(mockWorkflowStoreSetState).toHaveBeenCalledWith(expect.objectContaining({ + showOnboarding: false, + shouldAutoOpenStartNodeSelector: false, + hasShownOnboarding: false, + })) + expect(mockSetSyncWorkflowDraftHash).toHaveBeenCalledWith('new-hash') + }) + + it('should restore a local start placeholder when an existing workflow draft has an empty graph', async () => { + mockFetchWorkflowDraft.mockReset().mockResolvedValue({ + ...draftResponse, + graph: { nodes: [], edges: [] }, + hash: 'empty-draft-hash', + }) + + const { result } = renderHook(() => useWorkflowInit()) + + await waitFor(() => { + expect(result.current.data?.graph.nodes).toEqual([ + { id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } }, + ]) + }) + + expect(mockSyncWorkflowDraft).not.toHaveBeenCalled() + expect(mockSetSyncWorkflowDraftHash).toHaveBeenCalledWith('empty-draft-hash') + }) + + it('should preserve existing draft nodes when restoring the local start placeholder', async () => { + const existingNode = { id: 'llm', data: { type: BlockEnum.LLM } } + const existingEdge = { source: 'llm', target: 'answer' } + mockFetchWorkflowDraft.mockReset().mockResolvedValue({ + ...draftResponse, + graph: { + nodes: [existingNode], + edges: [existingEdge], + }, + }) + + const { result } = renderHook(() => useWorkflowInit()) + + await waitFor(() => { + expect(result.current.data?.graph.nodes).toEqual([ + { id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } }, + existingNode, + ]) + }) + + expect(result.current.data?.graph.edges).toEqual([existingEdge]) + expect(mockSyncWorkflowDraft).not.toHaveBeenCalled() }) it('should hydrate draft state, preload defaults, and derive published workflow metadata on success', async () => { diff --git a/web/app/components/workflow-app/hooks/__tests__/use-workflow-refresh-draft.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-workflow-refresh-draft.spec.ts index 209f9d9c0e5..c77e52bf4d5 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-workflow-refresh-draft.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-workflow-refresh-draft.spec.ts @@ -1,5 +1,7 @@ import { act, renderHook, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { BlockEnum } from '@/app/components/workflow/types' +import { AppModeEnum } from '@/types/app' import { useWorkflowRefreshDraft } from '../use-workflow-refresh-draft' @@ -11,6 +13,11 @@ const mockSetEnvSecrets = vi.fn() const mockSetConversationVariables = vi.fn() const mockSetIsWorkflowDataLoaded = vi.fn() const mockCancel = vi.fn() +let appStoreState: { + appDetail: { + mode: string + } +} let workflowStoreState: { appId: string @@ -30,6 +37,11 @@ vi.mock('@/app/components/workflow/store', () => ({ }), })) +vi.mock('@/app/components/app/store', () => ({ + useStore: (selector: (state: typeof appStoreState) => T): T => + selector(appStoreState), +})) + vi.mock('@/app/components/workflow/hooks', () => ({ useWorkflowUpdate: () => ({ handleUpdateWorkflowCanvas: mockHandleUpdateWorkflowCanvas }), })) @@ -60,6 +72,9 @@ describe('useWorkflowRefreshDraft — notUpdateCanvas parameter', () => { setConversationVariables: mockSetConversationVariables, setIsWorkflowDataLoaded: mockSetIsWorkflowDataLoaded, } + appStoreState = { + appDetail: { mode: AppModeEnum.ADVANCED_CHAT }, + } mockFetchWorkflowDraft.mockResolvedValue(draftResponse) }) @@ -141,6 +156,70 @@ describe('useWorkflowRefreshDraft — notUpdateCanvas parameter', () => { }) }) + it('should restore a local start placeholder for workflow drafts without an entry node', async () => { + appStoreState = { + appDetail: { mode: AppModeEnum.WORKFLOW }, + } + mockFetchWorkflowDraft.mockResolvedValue({ + hash: 'server-hash', + graph: { + nodes: [], + edges: [], + }, + environment_variables: [], + conversation_variables: [], + }) + + const { result } = renderHook(() => useWorkflowRefreshDraft()) + + act(() => { + result.current.handleRefreshWorkflowDraft() + }) + + await waitFor(() => { + expect(mockHandleUpdateWorkflowCanvas).toHaveBeenCalledWith({ + nodes: [ + expect.objectContaining({ + data: expect.objectContaining({ + type: BlockEnum.StartPlaceholder, + title: 'workflow.blocks.start-placeholder', + desc: '', + selected: true, + }), + }), + ], + edges: [], + viewport: { x: 0, y: 0, zoom: 1 }, + }) + }) + }) + + it('should not restore a local start placeholder for non-workflow app modes', async () => { + mockFetchWorkflowDraft.mockResolvedValue({ + hash: 'server-hash', + graph: { + nodes: [], + edges: [], + }, + environment_variables: [], + conversation_variables: [], + }) + + const { result } = renderHook(() => useWorkflowRefreshDraft()) + + act(() => { + result.current.handleRefreshWorkflowDraft() + }) + + await waitFor(() => { + expect(mockHandleUpdateWorkflowCanvas).toHaveBeenCalledWith({ + nodes: [], + edges: [], + viewport: { x: 0, y: 0, zoom: 1 }, + }) + }) + }) + it('should restore loaded state when refresh fails after workflow data was already loaded', async () => { mockFetchWorkflowDraft.mockRejectedValue(new Error('refresh failed')) diff --git a/web/app/components/workflow-app/hooks/__tests__/use-workflow-start-run.spec.tsx b/web/app/components/workflow-app/hooks/__tests__/use-workflow-start-run.spec.tsx index da9c207e82f..34bde7be4c9 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-workflow-start-run.spec.tsx +++ b/web/app/components/workflow-app/hooks/__tests__/use-workflow-start-run.spec.tsx @@ -113,6 +113,23 @@ describe('useWorkflowStartRun', () => { expect(mockSetShowInputsPanel).toHaveBeenCalledWith(false) }) + it('should not sync or run when only the start placeholder exists', async () => { + mockGetNodes.mockReturnValue([ + { id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } }, + ]) + + const { result } = renderHook(() => useWorkflowStartRun()) + + await act(async () => { + await result.current.handleWorkflowStartRunInWorkflow() + }) + + expect(mockDoSyncWorkflowDraft).not.toHaveBeenCalled() + expect(mockHandleRun).not.toHaveBeenCalled() + expect(mockSetShowDebugAndPreviewPanel).not.toHaveBeenCalled() + expect(mockSetShowInputsPanel).not.toHaveBeenCalled() + }) + it('should open the input panel instead of running immediately when start inputs are required', async () => { mockGetNodes.mockReturnValue([ { id: 'inset-s-1', data: { type: BlockEnum.Start, variables: [{ name: 'query' }] } }, diff --git a/web/app/components/workflow-app/hooks/__tests__/use-workflow-template.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-workflow-template.spec.ts index 3e9934df745..ecf285db60a 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-workflow-template.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-workflow-template.spec.ts @@ -1,13 +1,24 @@ import { renderHook } from '@testing-library/react' +import { AppModeEnum } from '@/types/app' import { useWorkflowTemplate } from '../use-workflow-template' const mockUseIsChatMode = vi.fn() let generateNewNodeCalls: Array> = [] +let appStoreState: { + appDetail: { + mode: string + } +} vi.mock('@/app/components/workflow-app/hooks/use-is-chat-mode', () => ({ useIsChatMode: () => mockUseIsChatMode(), })) +vi.mock('@/app/components/app/store', () => ({ + useStore: (selector: (state: typeof appStoreState) => T): T => + selector(appStoreState), +})) + vi.mock('@/app/components/workflow/utils', async (importOriginal) => { const actual = await importOriginal() return { @@ -29,9 +40,12 @@ describe('useWorkflowTemplate', () => { beforeEach(() => { vi.clearAllMocks() generateNewNodeCalls = [] + appStoreState = { + appDetail: { mode: AppModeEnum.WORKFLOW }, + } }) - it('should return only the start node template in workflow mode', () => { + it('should return only the start placeholder template in workflow mode', () => { mockUseIsChatMode.mockReturnValue(false) const { result } = renderHook(() => useWorkflowTemplate()) @@ -39,9 +53,35 @@ describe('useWorkflowTemplate', () => { expect(result.current.nodes).toHaveLength(1) expect(result.current.edges).toEqual([]) expect(generateNewNodeCalls).toHaveLength(1) + expect(generateNewNodeCalls[0]!.data).toMatchObject({ + type: 'start-placeholder', + title: 'workflow.blocks.start-placeholder', + selected: true, + desc: '', + }) + }) + + it('should return the start node template for non-workflow app modes', () => { + appStoreState = { + appDetail: { mode: AppModeEnum.COMPLETION }, + } + mockUseIsChatMode.mockReturnValue(false) + + const { result } = renderHook(() => useWorkflowTemplate()) + + expect(result.current.nodes).toHaveLength(1) + expect(result.current.edges).toEqual([]) + expect(generateNewNodeCalls).toHaveLength(1) + expect(generateNewNodeCalls[0]!.data).toMatchObject({ + type: 'start', + title: 'workflow.blocks.start', + }) }) it('should build start, llm, and answer templates with linked edges in chat mode', () => { + appStoreState = { + appDetail: { mode: AppModeEnum.ADVANCED_CHAT }, + } mockUseIsChatMode.mockReturnValue(true) const { result } = renderHook(() => useWorkflowTemplate()) diff --git a/web/app/components/workflow-app/hooks/use-auto-onboarding.ts b/web/app/components/workflow-app/hooks/use-auto-onboarding.ts index e4f5774adf1..4b43b72894c 100644 --- a/web/app/components/workflow-app/hooks/use-auto-onboarding.ts +++ b/web/app/components/workflow-app/hooks/use-auto-onboarding.ts @@ -12,11 +12,15 @@ export const useAutoOnboarding = () => { showOnboarding, hasShownOnboarding, notInitialWorkflow, + isWorkflowDataLoaded, setShowOnboarding, setHasShownOnboarding, setShouldAutoOpenStartNodeSelector, } = workflowStore.getState() + if (!isWorkflowDataLoaded) + return + // Skip if already showing onboarding or it's the initial workflow creation if (showOnboarding || notInitialWorkflow) return 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 58676dd9b4f..bc468e73e35 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 @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next' import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node' import AnswerDefault from '@/app/components/workflow/nodes/answer/default' import EndDefault from '@/app/components/workflow/nodes/end/default' +import StartPlaceholderDefault from '@/app/components/workflow/nodes/start-placeholder/default' import StartDefault from '@/app/components/workflow/nodes/start/default' import TriggerPluginDefault from '@/app/components/workflow/nodes/trigger-plugin/default' import TriggerScheduleDefault from '@/app/components/workflow/nodes/trigger-schedule/default' @@ -34,6 +35,7 @@ export const useAvailableNodesMetaData = () => { isChatMode ? [AnswerDefault] : [ + StartPlaceholderDefault, EndDefault, TriggerWebhookDefault, TriggerScheduleDefault, diff --git a/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts b/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts index d9eafcfc7da..f7bd2f4c2fb 100644 --- a/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts +++ b/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts @@ -9,6 +9,7 @@ import { collaborationManager } from '@/app/components/workflow/collaboration/co import { useSerialAsyncCallback } from '@/app/components/workflow/hooks/use-serial-async-callback' import { useNodesReadOnly } from '@/app/components/workflow/hooks/use-workflow' import { useWorkflowStore } from '@/app/components/workflow/store' +import { BlockEnum } from '@/app/components/workflow/types' import { API_PREFIX } from '@/config' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { postWithKeepalive } from '@/service/fetch' @@ -32,7 +33,13 @@ export const useNodesSyncDraft = () => { edges, transform, } = store.getState() - const nodes = getNodes().filter(node => !node.data?._isTempNode) + const allNodes = getNodes() + const nodes = allNodes.filter(node => !node.data?._isTempNode && node.data?.type !== BlockEnum.StartPlaceholder) + const skippedNodeIds = new Set( + allNodes + .filter(node => node.data?._isTempNode || node.data?.type === BlockEnum.StartPlaceholder) + .map(node => node.id), + ) const [x, y, zoom] = transform const { appId, @@ -54,7 +61,7 @@ export const useNodesSyncDraft = () => { }) }) }) - const producedEdges = produce(edges.filter(edge => !edge.data?._isTemp), (draft) => { + const producedEdges = produce(edges.filter(edge => !edge.data?._isTemp && !skippedNodeIds.has(edge.source) && !skippedNodeIds.has(edge.target)), (draft) => { draft.forEach((edge) => { Object.keys(edge.data).forEach((key) => { if (key.startsWith('_')) diff --git a/web/app/components/workflow-app/hooks/use-workflow-draft-graph-for-canvas.ts b/web/app/components/workflow-app/hooks/use-workflow-draft-graph-for-canvas.ts new file mode 100644 index 00000000000..aa805e37b01 --- /dev/null +++ b/web/app/components/workflow-app/hooks/use-workflow-draft-graph-for-canvas.ts @@ -0,0 +1,76 @@ +import type { + Node, + WorkflowDataUpdater, +} from '@/app/components/workflow/types' +import { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { START_INITIAL_POSITION } from '@/app/components/workflow/constants' +import startPlaceholderDefault from '@/app/components/workflow/nodes/start-placeholder/default' +import { BlockEnum } from '@/app/components/workflow/types' +import { generateNewNode } from '@/app/components/workflow/utils' +import { AppModeEnum } from '@/types/app' + +type HydrateWorkflowDraftGraphOptions = { + localStartPlaceholderNodes?: Node[] +} + +const hasWorkflowEntryNode = (nodes: Node[] = []): boolean => { + return nodes.some(node => ( + node?.data?.type === BlockEnum.Start + || node?.data?.type === BlockEnum.TriggerSchedule + || node?.data?.type === BlockEnum.TriggerWebhook + || node?.data?.type === BlockEnum.TriggerPlugin + )) +} + +const hasStartPlaceholderNode = (nodes: Node[] = []): boolean => { + return nodes.some(node => node?.data?.type === BlockEnum.StartPlaceholder) +} + +export const useWorkflowDraftGraphForCanvas = (appMode?: AppModeEnum | string) => { + const { t } = useTranslation() + + const getNodesWithLocalStartPlaceholder = useCallback(( + nodes: Node[] = [], + localStartPlaceholderNodes?: Node[], + ) => { + if (appMode !== AppModeEnum.WORKFLOW || hasWorkflowEntryNode(nodes) || hasStartPlaceholderNode(nodes)) + return nodes + + if (localStartPlaceholderNodes?.length) + return [...localStartPlaceholderNodes, ...nodes] + + const { newNode: startPlaceholderNode } = generateNewNode({ + data: { + ...startPlaceholderDefault.defaultValue, + selected: true, + type: startPlaceholderDefault.metaData.type, + title: t(`blocks.${startPlaceholderDefault.metaData.type}`, { ns: 'workflow' }), + desc: '', + }, + position: START_INITIAL_POSITION, + }) + + return [startPlaceholderNode, ...nodes] + }, [appMode, t]) + + const getWorkflowDraftGraphForCanvas = useCallback(( + graph?: Partial, + options?: HydrateWorkflowDraftGraphOptions, + ): WorkflowDataUpdater => { + const nodes = getNodesWithLocalStartPlaceholder( + graph?.nodes || [], + options?.localStartPlaceholderNodes, + ) + + return { + nodes, + edges: graph?.edges || [], + viewport: graph?.viewport || { x: 0, y: 0, zoom: 1 }, + } + }, [getNodesWithLocalStartPlaceholder]) + + return { + getWorkflowDraftGraphForCanvas, + } +} diff --git a/web/app/components/workflow-app/hooks/use-workflow-init.ts b/web/app/components/workflow-app/hooks/use-workflow-init.ts index 548407257e2..7a4ccd63101 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-init.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-init.ts @@ -20,6 +20,7 @@ import { syncWorkflowDraft, } from '@/service/workflow' import { AppModeEnum } from '@/types/app' +import { useWorkflowDraftGraphForCanvas } from './use-workflow-draft-graph-for-canvas' import { useWorkflowTemplate } from './use-workflow-template' const hasConnectedUserInput = (nodes: Node[] = [], edges: Edge[] = []): boolean => { @@ -32,6 +33,7 @@ const hasConnectedUserInput = (nodes: Node[] = [], edges: Edge[] = []): boolean return edges.some(edge => startNodeIds.includes(edge.source)) } + export const useWorkflowInit = () => { const workflowStore = useWorkflowStore() const { @@ -39,6 +41,7 @@ export const useWorkflowInit = () => { edges: edgesTemplate, } = useWorkflowTemplate() const appDetail = useAppStore(state => state.appDetail)! + const { getWorkflowDraftGraphForCanvas } = useWorkflowDraftGraphForCanvas(appDetail.mode) const setSyncWorkflowDraftHash = useStore(s => s.setSyncWorkflowDraftHash) const [data, setData] = useState() const [isLoading, setIsLoading] = useState(true) @@ -58,17 +61,24 @@ export const useWorkflowInit = () => { const handleGetInitialWorkflowData = useCallback(async () => { try { const res = await fetchWorkflowDraft(`/apps/${appDetail.id}/workflows/draft`) - setData(res) + const initialData = { + ...res, + graph: getWorkflowDraftGraphForCanvas(res.graph, { + localStartPlaceholderNodes: nodesTemplate, + }), + } + + setData(initialData) workflowStore.setState({ - envSecrets: (res.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => { + envSecrets: (initialData.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => { acc[env.id] = env.value return acc }, {} as Record), - environmentVariables: res.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [], - conversationVariables: res.conversation_variables || [], + environmentVariables: initialData.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [], + conversationVariables: initialData.conversation_variables || [], isWorkflowDataLoaded: true, }) - setSyncWorkflowDraftHash(res.hash) + setSyncWorkflowDraftHash(initialData.hash) setIsLoading(false) } catch (error: any) { @@ -78,19 +88,18 @@ export const useWorkflowInit = () => { const isAdvancedChat = appDetail.mode === AppModeEnum.ADVANCED_CHAT workflowStore.setState({ notInitialWorkflow: true, - showOnboarding: !isAdvancedChat, - shouldAutoOpenStartNodeSelector: !isAdvancedChat, - hasShownOnboarding: false, + showOnboarding: false, + shouldAutoOpenStartNodeSelector: false, + hasSelectedStartNode: false, + hasShownOnboarding: !isAdvancedChat, }) - const nodesData = isAdvancedChat ? nodesTemplate : [] - const edgesData = isAdvancedChat ? edgesTemplate : [] syncWorkflowDraft({ url: `/apps/${appDetail.id}/workflows/draft`, params: { graph: { - nodes: nodesData, - edges: edgesData, + nodes: isAdvancedChat ? nodesTemplate : [], + edges: isAdvancedChat ? edgesTemplate : [], }, features: { retriever_resource: { enabled: true }, @@ -107,7 +116,7 @@ export const useWorkflowInit = () => { }) } } - }, [appDetail, nodesTemplate, edgesTemplate, workflowStore, setSyncWorkflowDraftHash]) + }, [appDetail, getWorkflowDraftGraphForCanvas, nodesTemplate, edgesTemplate, workflowStore, setSyncWorkflowDraftHash]) useEffect(() => { handleGetInitialWorkflowData() diff --git a/web/app/components/workflow-app/hooks/use-workflow-refresh-draft.ts b/web/app/components/workflow-app/hooks/use-workflow-refresh-draft.ts index a7283c00781..dec94f33bb9 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-refresh-draft.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-refresh-draft.ts @@ -1,12 +1,15 @@ -import type { WorkflowDataUpdater } from '@/app/components/workflow/types' import { useCallback } from 'react' +import { useStore as useAppStore } from '@/app/components/app/store' import { useWorkflowUpdate } from '@/app/components/workflow/hooks' import { useWorkflowStore } from '@/app/components/workflow/store' import { fetchWorkflowDraft } from '@/service/workflow' +import { useWorkflowDraftGraphForCanvas } from './use-workflow-draft-graph-for-canvas' export const useWorkflowRefreshDraft = () => { + const appDetail = useAppStore(s => s.appDetail) const workflowStore = useWorkflowStore() const { handleUpdateWorkflowCanvas } = useWorkflowUpdate() + const { getWorkflowDraftGraphForCanvas } = useWorkflowDraftGraphForCanvas(appDetail?.mode) const handleRefreshWorkflowDraft = useCallback((notUpdateCanvas?: boolean) => { const { @@ -31,14 +34,8 @@ export const useWorkflowRefreshDraft = () => { fetchWorkflowDraft(`/apps/${appId}/workflows/draft`) .then((response) => { // Ensure we have a valid workflow structure with viewport - if (!notUpdateCanvas) { - const workflowData: WorkflowDataUpdater = { - nodes: response.graph?.nodes || [], - edges: response.graph?.edges || [], - viewport: response.graph?.viewport || { x: 0, y: 0, zoom: 1 }, - } - handleUpdateWorkflowCanvas(workflowData) - } + if (!notUpdateCanvas) + handleUpdateWorkflowCanvas(getWorkflowDraftGraphForCanvas(response.graph)) setSyncWorkflowDraftHash(response.hash) setEnvSecrets((response.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => { acc[env.id] = env.value @@ -55,7 +52,7 @@ export const useWorkflowRefreshDraft = () => { .finally(() => { setIsSyncingWorkflowDraft(false) }) - }, [handleUpdateWorkflowCanvas, workflowStore]) + }, [getWorkflowDraftGraphForCanvas, handleUpdateWorkflowCanvas, workflowStore]) return { handleRefreshWorkflowDraft, diff --git a/web/app/components/workflow-app/hooks/use-workflow-start-run.tsx b/web/app/components/workflow-app/hooks/use-workflow-start-run.tsx index 9344a8d1ab6..516a653cd18 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-start-run.tsx +++ b/web/app/components/workflow-app/hooks/use-workflow-start-run.tsx @@ -34,6 +34,9 @@ export const useWorkflowStartRun = () => { const { getNodes } = store.getState() const nodes = getNodes() const startNode = nodes.find(node => node.data.type === BlockEnum.Start) + if (!startNode) + return + const startVariables = startNode?.data.variables || [] const fileSettings = featuresStore!.getState().features.file const { diff --git a/web/app/components/workflow-app/hooks/use-workflow-template.ts b/web/app/components/workflow-app/hooks/use-workflow-template.ts index 5a89d3b3e76..d45c588889a 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-template.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-template.ts @@ -1,29 +1,39 @@ import type { StartNodeType } from '@/app/components/workflow/nodes/start/types' import { useTranslation } from 'react-i18next' +import { useStore as useAppStore } from '@/app/components/app/store' import { NODE_WIDTH_X_OFFSET, START_INITIAL_POSITION, } from '@/app/components/workflow/constants' import answerDefault from '@/app/components/workflow/nodes/answer/default' import llmDefault from '@/app/components/workflow/nodes/llm/default' +import startPlaceholderDefault from '@/app/components/workflow/nodes/start-placeholder/default' import startDefault from '@/app/components/workflow/nodes/start/default' import { generateNewNode } from '@/app/components/workflow/utils' +import { AppModeEnum } from '@/types/app' import { useIsChatMode } from './use-is-chat-mode' export const useWorkflowTemplate = () => { const isChatMode = useIsChatMode() + const appDetail = useAppStore(s => s.appDetail) const { t } = useTranslation() - const { newNode: startNode } = generateNewNode({ - data: { - ...startDefault.defaultValue as StartNodeType, - type: startDefault.metaData.type, - title: t(`blocks.${startDefault.metaData.type}`, { ns: 'workflow' }), - }, - position: START_INITIAL_POSITION, - }) + const createStartNode = () => { + const { newNode: startNode } = generateNewNode({ + data: { + ...startDefault.defaultValue as StartNodeType, + type: startDefault.metaData.type, + title: t(`blocks.${startDefault.metaData.type}`, { ns: 'workflow' }), + }, + position: START_INITIAL_POSITION, + }) + + return startNode + } if (isChatMode) { + const startNode = createStartNode() + const { newNode: llmNode } = generateNewNode({ id: 'llm', data: { @@ -77,10 +87,26 @@ export const useWorkflowTemplate = () => { edges: [startToLlmEdge, llmToAnswerEdge], } } - else { + if (appDetail?.mode === AppModeEnum.WORKFLOW) { + const { newNode: startPlaceholderNode } = generateNewNode({ + data: { + ...startPlaceholderDefault.defaultValue, + selected: true, + type: startPlaceholderDefault.metaData.type, + title: t(`blocks.${startPlaceholderDefault.metaData.type}`, { ns: 'workflow' }), + desc: '', + }, + position: START_INITIAL_POSITION, + }) + return { - nodes: [startNode], + nodes: [startPlaceholderNode], edges: [], } } + + return { + nodes: [createStartNode()], + edges: [], + } } diff --git a/web/app/components/workflow/__tests__/block-icon.spec.tsx b/web/app/components/workflow/__tests__/block-icon.spec.tsx index c3b30a67b60..7bec2b41eff 100644 --- a/web/app/components/workflow/__tests__/block-icon.spec.tsx +++ b/web/app/components/workflow/__tests__/block-icon.spec.tsx @@ -9,7 +9,7 @@ describe('BlockIcon', () => { const iconContainer = container.firstElementChild expect(iconContainer).toHaveClass('w-4', 'h-4', 'bg-util-colors-blue-brand-blue-brand-500', 'extra-class') - expect(iconContainer?.querySelector('svg')).toBeInTheDocument() + expect(iconContainer?.querySelector('.i-custom-vender-workflow-user-input')).toBeInTheDocument() }) it('normalizes protected plugin icon urls for tool-like nodes', () => { diff --git a/web/app/components/workflow/block-icon.tsx b/web/app/components/workflow/block-icon.tsx index 571ea475e25..15292a07049 100644 --- a/web/app/components/workflow/block-icon.tsx +++ b/web/app/components/workflow/block-icon.tsx @@ -45,6 +45,7 @@ const ICON_CONTAINER_CLASSNAME_SIZE_MAP: Record = { const DEFAULT_ICON_MAP: Record> = { [BlockEnum.Start]: Home, + [BlockEnum.StartPlaceholder]: Home, [BlockEnum.LLM]: Llm, [BlockEnum.Code]: Code, [BlockEnum.End]: End, @@ -96,6 +97,7 @@ const normalizeToolIconUrl = (toolIcon: string) => { const ICON_CONTAINER_BG_COLOR_MAP: Record = { [BlockEnum.Start]: 'bg-util-colors-blue-brand-blue-brand-500', + [BlockEnum.StartPlaceholder]: 'bg-util-colors-blue-brand-blue-brand-500', [BlockEnum.LLM]: 'bg-util-colors-indigo-indigo-500', [BlockEnum.Code]: 'bg-util-colors-blue-blue-500', [BlockEnum.End]: 'bg-util-colors-warning-warning-500', @@ -129,12 +131,46 @@ const BlockIcon: FC = ({ className, toolIcon, }) => { + const isStart = type === BlockEnum.Start + const isStartPlaceholder = type === BlockEnum.StartPlaceholder const isToolOrDataSourceOrTriggerPlugin = type === BlockEnum.Tool || type === BlockEnum.DataSource || type === BlockEnum.TriggerPlugin const showDefaultIcon = !isToolOrDataSourceOrTriggerPlugin || !toolIcon const resolvedToolIcon = typeof toolIcon === 'string' ? normalizeToolIconUrl(toolIcon) : toolIcon + if (isStart) { + return ( +
+ +
+ ) + } + + if (isStartPlaceholder) { + return ( +
+ +
+ ) + } + return (
({ useGetLanguage: vi.fn(), @@ -49,7 +50,7 @@ vi.mock('@/utils/var', async (importOriginal) => { const actual = await importOriginal() return { ...actual, - getMarketplaceUrl: () => 'https://marketplace.test/start', + getMarketplaceUrl: (path: string) => `https://marketplace.test${path}`, } }) @@ -186,7 +187,7 @@ describe('AllStartBlocks', () => { const user = userEvent.setup() const onSelect = vi.fn() - render( + const { container } = render( { ) await waitFor(() => { - expect(screen.getByText('workflow.tabs.allTriggers')).toBeInTheDocument() + expect(screen.getByText('workflow.blocks.start')).toBeInTheDocument() }) expect(screen.getByText('workflow.blocks.start')).toBeInTheDocument() + expect(screen.queryByText('workflow.tabs.allTriggers')).not.toBeInTheDocument() + expect(screen.getByText('workflow.blocks.mostCommon')).toBeInTheDocument() expect(screen.getByText('Provider One')).toBeInTheDocument() + expect(container.querySelectorAll('.bg-divider-subtle')).toHaveLength(0) await user.click(screen.getByText('workflow.blocks.start')) expect(onSelect).toHaveBeenCalledWith(BlockEnum.Start) @@ -225,7 +229,150 @@ describe('AllStartBlocks', () => { />, ) - expect(await screen.findByRole('link', { name: /plugin\.findMoreInMarketplace/ })).toHaveAttribute('href', 'https://marketplace.test/start') + const footer = await screen.findByRole('link', { name: /plugin\.findMoreInMarketplace/ }) + expect(footer).toHaveAttribute('href', 'https://marketplace.test/plugins/trigger') + expect(footer).toHaveClass('system-sm-medium', 'h-8', 'rounded-b-lg', 'bg-components-panel-bg-blur', 'text-text-accent-light-mode-only', 'shadow-lg') + expect(footer.querySelector('.i-custom-vender-main-nav-marketplace')).not.toBeInTheDocument() + expect(footer.querySelector('svg')).toBeInTheDocument() + }) + + it('should keep the panel marketplace footer icon style', async () => { + enableMarketplaceForRender = true + + render( + , + ) + + const footer = await screen.findByRole('link', { name: /workflow\.nodes\.startPlaceholder\.browseMoreOnMarketplace/ }) + expect(footer).toHaveAttribute('href', 'https://marketplace.test/plugins/trigger') + expect(footer).toHaveClass('flex-col') + expect(footer.querySelector('.w-8 .bg-divider-subtle')).toBeInTheDocument() + expect(footer.querySelector('.i-custom-vender-workflow-marketplace')).toBeInTheDocument() + expect(footer.querySelector('svg')).not.toBeInTheDocument() + }) + + it('should keep the panel divider between user input and installed triggers', async () => { + const { container } = render( + , + ) + + await waitFor(() => { + expect(screen.getByText('Provider One')).toBeInTheDocument() + }) + + expect(container.querySelectorAll('.px-4.py-1 .bg-divider-subtle')).toHaveLength(1) + }) + + it('should render searched marketplace results after built-in and installed trigger options', async () => { + enableMarketplaceForRender = true + mockUseAllTriggerPlugins.mockReturnValue(createTriggerPluginsQueryResult([ + createTriggerProvider({ + label: { en_US: 'Start Provider', zh_Hans: 'Start Provider' }, + }), + ])) + mockUseMarketplacePlugins.mockReturnValue(createMarketplacePluginsMock({ + plugins: [ + createPlugin({ + name: 'start-marketplace', + label: { en_US: 'Start Marketplace', zh_Hans: 'Start Marketplace' }, + }), + ], + })) + + const { container } = render( + , + ) + + await waitFor(() => { + expect(screen.getByText('Start Marketplace')).toBeInTheDocument() + }) + + const text = container.textContent || '' + expect(text.indexOf('workflow.blocks.start')).toBeLessThan(text.indexOf('Start Provider')) + expect(text.indexOf('Start Provider')).toBeLessThan(text.indexOf('Start Marketplace')) + expect(screen.getAllByRole('link', { name: /plugin\.searchInMarketplace/i })).toHaveLength(1) + expect(container.querySelectorAll('.px-4.py-1 .bg-divider-subtle')).toHaveLength(1) + }) + + it('should show the user input conflict state without allowing another start selection', () => { + const onSelect = vi.fn() + enableMarketplaceForRender = true + mockUseNodes.mockReturnValue([ + { + id: 'start', + data: { + type: BlockEnum.Start, + }, + }, + ] as never) + + render( + , + ) + + expect(screen.getByText('workflow.nodes.startPlaceholder.userInputConflictTip')).toBeInTheDocument() + expect(screen.queryByText('workflow.tabs.allTriggers')).not.toBeInTheDocument() + expect(screen.getByText('workflow.blocks.start')).toBeInTheDocument() + expect(screen.getByText('common.operation.added')).toBeInTheDocument() + const footer = screen.getByRole('link', { name: /plugin\.findMoreInMarketplace/ }) + expect(footer).toHaveClass('system-sm-medium', 'h-8', 'rounded-b-lg', 'bg-components-panel-bg-blur', 'text-text-accent-light-mode-only', 'shadow-lg') + expect(footer.querySelector('.i-custom-vender-main-nav-marketplace')).not.toBeInTheDocument() + expect(footer.querySelector('svg')).toBeInTheDocument() + + fireEvent.click(screen.getByText('workflow.blocks.start')) + fireEvent.click(screen.getByText('Provider One')) + + expect(onSelect).not.toHaveBeenCalled() + }) + + it('should keep user input visible but disabled when another trigger already exists', async () => { + const user = userEvent.setup() + const onSelect = vi.fn() + + render( + , + ) + + expect(screen.queryByText('workflow.tabs.allTriggers')).not.toBeInTheDocument() + expect(screen.getByText('workflow.blocks.start')).toBeInTheDocument() + expect(screen.getByText('workflow.blocks.mostCommon').closest('.opacity-30')).toBeInTheDocument() + expect(screen.getByText('workflow.blocks.start').closest('.cursor-not-allowed')).toBeInTheDocument() + + await user.hover(screen.getByText('workflow.blocks.start')) + + expect(await screen.findByText('workflow.nodes.startPlaceholder.userInputConflictTip')).toBeInTheDocument() + + fireEvent.click(screen.getByText('workflow.blocks.start')) + expect(onSelect).not.toHaveBeenCalled() + + await user.click(screen.getByText('workflow.blocks.trigger-schedule')) + expect(onSelect).toHaveBeenCalledWith(BlockEnum.TriggerSchedule) }) }) @@ -256,11 +403,15 @@ describe('AllStartBlocks', () => { }) }) - expect(screen.getByText('workflow.tabs.noPluginsFound')).toBeInTheDocument() + expect(screen.getByText('workflow.nodes.startPlaceholder.noTriggersFound')).toBeInTheDocument() expect(screen.getByRole('link', { name: 'workflow.tabs.requestToCommunity' })).toHaveAttribute( 'href', 'https://github.com/langgenius/dify-plugins/issues/new?template=plugin_request.yaml', ) + expect(screen.getByRole('link', { name: /plugin\.findMoreInMarketplace/ })).toHaveAttribute( + 'href', + 'https://marketplace.test/plugins/trigger', + ) }) }) }) diff --git a/web/app/components/workflow/block-selector/__tests__/featured-triggers.spec.tsx b/web/app/components/workflow/block-selector/__tests__/featured-triggers.spec.tsx index 5955665f5e1..45767d76078 100644 --- a/web/app/components/workflow/block-selector/__tests__/featured-triggers.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/featured-triggers.spec.tsx @@ -193,5 +193,37 @@ describe('FeaturedTriggers', () => { event_label: 'Created', })) }) + + it('should align featured item icons with the trigger list column', () => { + const provider = createTriggerProvider() + + render( + , + ) + + const installedRow = screen.getByText('Provider One').closest('.select-none') + expect(installedRow).toHaveClass('h-8', 'pr-2', 'pl-3') + expect(installedRow?.parentElement?.parentElement?.parentElement).toHaveClass('p-1') + + const uninstalledRow = screen.getByText('Plugin Two').closest('.group') + expect(uninstalledRow).toHaveClass('h-8', 'pr-2', 'pl-3') + expect(uninstalledRow?.parentElement).toHaveClass('mb-1', 'last-of-type:mb-0') + expect(uninstalledRow?.parentElement?.parentElement).toHaveClass('p-1') + }) }) }) diff --git a/web/app/components/workflow/block-selector/__tests__/hooks.spec.tsx b/web/app/components/workflow/block-selector/__tests__/hooks.spec.tsx index 0ae49213098..7ad791fc499 100644 --- a/web/app/components/workflow/block-selector/__tests__/hooks.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/hooks.spec.tsx @@ -7,14 +7,27 @@ describe('block-selector hooks', () => { vi.clearAllMocks() }) - it('falls back to the first valid tab when the preferred start tab is disabled', () => { + it('keeps the start tab enabled when a configured user input start node exists', () => { const { result } = renderHook(() => useTabs({ noStart: false, - hasUserInputNode: true, defaultActiveTab: TabsEnum.Start, })) - expect(result.current.tabs.find(tab => tab.key === TabsEnum.Start)?.disabled).toBe(true) + expect(result.current.tabs.find(tab => tab.key === TabsEnum.Start)?.disabled).toBeFalsy() + expect(result.current.activeTab).toBe(TabsEnum.Start) + }) + + it('disables the start tab when an unconfigured start placeholder exists', () => { + const { result } = renderHook(() => useTabs({ + noStart: false, + hasStartPlaceholderNode: true, + defaultActiveTab: TabsEnum.Start, + })) + + const startTab = result.current.tabs.find(tab => tab.key === TabsEnum.Start) + expect(startTab?.disabled).toBe(true) + expect(startTab?.disabledTip).toBe('workflow.tabs.unconfiguredStartDisabledTip') + expect(startTab?.disabledTipLinkKey).toBe('startNodesDocs') expect(result.current.activeTab).toBe(TabsEnum.Blocks) }) @@ -22,7 +35,6 @@ describe('block-selector hooks', () => { const props: Parameters[0] = { noBlocks: false, noStart: false, - hasUserInputNode: true, forceEnableStartTab: true, } diff --git a/web/app/components/workflow/block-selector/__tests__/index.spec.tsx b/web/app/components/workflow/block-selector/__tests__/index.spec.tsx index d426b43cfdc..52928540e2d 100644 --- a/web/app/components/workflow/block-selector/__tests__/index.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/index.spec.tsx @@ -65,6 +65,7 @@ describe('NodeSelectorWrapper', () => { availableNodesMetaData: { nodes: [ createBlock(BlockEnum.Start, 'Start'), + createBlock(BlockEnum.StartPlaceholder, 'Start Placeholder'), createBlock(BlockEnum.Tool, 'Tool'), createBlock(BlockEnum.Code, 'Code'), createBlock(BlockEnum.DataSource, 'Data Source'), @@ -79,6 +80,7 @@ describe('NodeSelectorWrapper', () => { expect(await screen.findByText('Code')).toBeInTheDocument() expect(screen.queryByText('Start')).not.toBeInTheDocument() + expect(screen.queryByText('Start Placeholder')).not.toBeInTheDocument() expect(screen.queryByText('Tool')).not.toBeInTheDocument() }) }) diff --git a/web/app/components/workflow/block-selector/__tests__/main.spec.tsx b/web/app/components/workflow/block-selector/__tests__/main.spec.tsx index 1473022ef94..884e45f6259 100644 --- a/web/app/components/workflow/block-selector/__tests__/main.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/main.spec.tsx @@ -7,7 +7,7 @@ import { FlowType } from '@/types/common' import { renderWorkflowComponent } from '../../__tests__/workflow-test-env' import { BlockEnum } from '../../types' import NodeSelector from '../main' -import { BlockClassificationEnum } from '../types' +import { BlockClassificationEnum, TabsEnum } from '../types' vi.mock('reactflow', () => ({ useStoreApi: () => ({ @@ -22,6 +22,22 @@ vi.mock('@/service/use-plugins', () => ({ plugins: [], isLoading: false, }), + useFeaturedTriggersRecommendations: () => ({ + plugins: [], + isLoading: false, + }), +})) + +vi.mock('@/app/components/plugins/marketplace/hooks', () => ({ + useMarketplacePlugins: () => ({ + plugins: [], + queryPluginsWithDebounced: vi.fn(), + }), +})) + +vi.mock('@/service/use-triggers', () => ({ + useAllTriggerPlugins: () => ({ data: [] }), + useInvalidateAllTriggerPlugins: () => vi.fn(), })) vi.mock('@/service/use-tools', () => ({ @@ -45,13 +61,18 @@ const createBlock = (type: BlockEnum, title: string): NodeDefault => ({ checkValid: () => ({ isValid: true }), }) -const renderNodeSelector = (ui: ReactElement) => { +type RenderNodeSelectorOptions = Parameters[1] + +const renderNodeSelector = (ui: ReactElement, options?: RenderNodeSelectorOptions) => { return renderWorkflowComponent(ui, { + ...options, hooksStoreProps: { + ...options?.hooksStoreProps, configsMap: { flowId: 'app-1', flowType: FlowType.appFlow, fileSettings: {} as never, + ...options?.hooksStoreProps?.configsMap, }, }, @@ -230,4 +251,68 @@ describe('NodeSelector', () => { expect(trigger.closest('[aria-haspopup="dialog"]')).toBe(trigger) expect(screen.getByPlaceholderText('workflow.tabs.searchBlock')).toBeInTheDocument() }) + + it('disables the start tab with a setup tooltip when an unconfigured start node is on the canvas', async () => { + const user = userEvent.setup() + + renderNodeSelector( + , + { + initialStoreState: { + nodes: [ + { + id: 'start-placeholder', + data: { + type: BlockEnum.StartPlaceholder, + }, + }, + ] as never, + }, + }, + ) + + await user.hover(screen.getByText('workflow.tabs.start')) + + expect(await screen.findByText('workflow.tabs.unconfiguredStartDisabledTip')).toBeInTheDocument() + expect(screen.getByRole('link', { name: 'workflow.tabs.startDisabledTipLearnMore' })).toHaveAttribute( + 'href', + 'https://docs.dify.ai/en/use-dify/nodes/trigger/overview', + ) + expect(screen.getByPlaceholderText('workflow.tabs.searchBlock')).toBeInTheDocument() + }) + + it('keeps the start tab enabled when a configured user input start node is on the canvas', () => { + renderNodeSelector( + , + { + initialStoreState: { + nodes: [ + { + id: 'start', + data: { + type: BlockEnum.Start, + }, + }, + ] as never, + }, + }, + ) + + expect(screen.getByText('workflow.tabs.start')).toHaveAttribute('aria-disabled', 'false') + expect(screen.getByText('workflow.nodes.startPlaceholder.userInputConflictTip')).toBeInTheDocument() + }) }) diff --git a/web/app/components/workflow/block-selector/__tests__/start-blocks.spec.tsx b/web/app/components/workflow/block-selector/__tests__/start-blocks.spec.tsx index 6bb50aeca30..d7e513c3602 100644 --- a/web/app/components/workflow/block-selector/__tests__/start-blocks.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/start-blocks.spec.tsx @@ -1,5 +1,5 @@ import type { CommonNodeType } from '../../types' -import { render, screen } from '@testing-library/react' +import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import useNodes from '@/app/components/workflow/store/workflow/use-nodes' import { useAvailableNodesMetaData } from '../../../workflow-app/hooks' @@ -76,5 +76,71 @@ describe('StartBlocks', () => { expect(screen.queryByText('workflow.blocks.start')).not.toBeInTheDocument() expect(onContentStateChange).toHaveBeenCalledWith(false) }) + + it('should show most common badge for user input in the start selector content', () => { + render( + , + ) + + expect(screen.getByText('workflow.blocks.mostCommon')).toBeInTheDocument() + expect(screen.queryByText('workflow.blocks.originalStartNode')).not.toBeInTheDocument() + }) + + it('should render built-in start block preview titles and Dify Team author', async () => { + const user = userEvent.setup() + + render( + , + ) + + await user.hover(screen.getByText('workflow.blocks.trigger-webhook')) + + expect(screen.queryByText('workflow.customWebhook')).not.toBeInTheDocument() + await waitFor(() => { + expect(screen.getAllByText('workflow.blocks.trigger-webhook')).toHaveLength(2) + }) + + await user.hover(screen.getByText('workflow.blocks.start')) + + await waitFor(() => { + expect(document.body).toHaveTextContent('tools.author workflow.difyTeam') + }) + }) + + it('should keep disabled user input reachable from the keyboard with the conflict reason', async () => { + const user = userEvent.setup() + const onSelect = vi.fn() + + render( + , + ) + + const userInputButton = screen.getByRole('button', { + name: /workflow\.blocks\.start.*workflow\.nodes\.startPlaceholder\.userInputConflictTip/, + }) + expect(userInputButton).toHaveAttribute('aria-disabled', 'true') + expect(userInputButton.querySelector('.i-custom-vender-workflow-user-input')?.closest('.opacity-30')).toBeInTheDocument() + + await user.tab() + expect(userInputButton).toHaveFocus() + await user.keyboard('{Enter}') + + expect(onSelect).not.toHaveBeenCalled() + }) }) }) diff --git a/web/app/components/workflow/block-selector/all-start-blocks.tsx b/web/app/components/workflow/block-selector/all-start-blocks.tsx index 8f32f5e2d71..2e2e58bb106 100644 --- a/web/app/components/workflow/block-selector/all-start-blocks.tsx +++ b/web/app/components/workflow/block-selector/all-start-blocks.tsx @@ -33,7 +33,20 @@ import PluginList from './market-place-plugin/list' import StartBlocks from './start-blocks' import TriggerPluginList from './trigger-plugin/list' -const marketplaceFooterClassName = 'system-sm-medium z-10 flex h-8 flex-none cursor-pointer items-center rounded-b-lg border-[0.5px] border-t border-components-panel-border bg-components-panel-bg-blur px-4 py-1 text-text-accent-light-mode-only shadow-lg' +const popoverMarketplaceFooterClassName = 'system-sm-medium z-10 flex h-8 flex-none cursor-pointer items-center rounded-b-lg border-[0.5px] border-t border-components-panel-border bg-components-panel-bg-blur px-4 py-1 text-text-accent-light-mode-only shadow-lg' +const panelMarketplaceFooterClassName = 'system-xs-regular z-10 flex flex-none cursor-pointer flex-col items-start gap-2 px-4 pt-2 pb-4 text-text-tertiary hover:text-text-secondary' + +const SectionDivider = () => ( +
+ +
+) + +const MarketplaceFooterDivider = () => ( +
+ +
+) type AllStartBlocksProps = { className?: string @@ -42,6 +55,9 @@ type AllStartBlocksProps = { availableBlocksTypes?: BlockEnum[] tags?: string[] allowUserInputSelection?: boolean // Allow user input option even when trigger node already exists (e.g. when no Start node yet or changing node type). + hasUserInputNode?: boolean + hasTriggerNode?: boolean + variant?: 'popover' | 'panel' } const AllStartBlocks = ({ @@ -51,6 +67,9 @@ const AllStartBlocks = ({ availableBlocksTypes, tags = [], allowUserInputSelection = false, + hasUserInputNode = false, + hasTriggerNode = false, + variant = 'popover', }: AllStartBlocksProps) => { const { t } = useTranslation() const [hasStartBlocksContent, setHasStartBlocksContent] = useState(false) @@ -100,9 +119,8 @@ const AllStartBlocks = ({ const shouldShowFeatured = enableTriggerPlugin && enable_marketplace && !hasFilter - const hasTriggerOptions = entryNodeTypes.some(type => type !== BlockEnumValue.Start) - const shouldShowTriggerListTitle = hasTriggerOptions && (hasStartBlocksContent || hasPluginContent) - const shouldShowMarketplaceFooter = enable_marketplace && !hasFilter + const shouldShowMarketplaceFooter = enable_marketplace + const isPanelVariant = variant === 'panel' const handleStartBlocksContentChange = useCallback((hasContent: boolean) => { setHasStartBlocksContent(hasContent) @@ -115,6 +133,11 @@ const AllStartBlocks = ({ const hasMarketplaceContent = enableTriggerPlugin && enable_marketplace && marketplacePlugins.length > 0 const hasAnyContent = hasStartBlocksContent || hasPluginContent || shouldShowFeatured || hasMarketplaceContent const shouldShowEmptyState = hasFilter && !hasAnyContent + const shouldShowInstalledTriggersDivider = isPanelVariant && hasStartBlocksContent && enableTriggerPlugin && hasPluginContent + const shouldShowMarketplaceSectionDivider = enableTriggerPlugin + && enable_marketplace + && (hasStartBlocksContent || hasPluginContent) + && (shouldShowFeatured || hasMarketplaceContent) useEffect(() => { if (!enableTriggerPlugin && hasPluginContent) @@ -134,16 +157,58 @@ const AllStartBlocks = ({ }, [enableTriggerPlugin, enable_marketplace, hasFilter, fetchPlugins, searchText, tags]) return ( -
-
+
+
pluginRef.current?.handleScroll()} >
- {shouldShowFeatured && ( - <> + {hasUserInputNode && ( +
+
+ + + +
+ {t('nodes.startPlaceholder.userInputConflictTip', { ns: 'workflow' })} +
+
+ )} + +
+ + + {shouldShowInstalledTriggersDivider && ( + + )} + + {enableTriggerPlugin && ( + + )} + + {shouldShowMarketplaceSectionDivider && ( + + )} + + {shouldShowFeatured && ( -
- -
- - )} - {shouldShowTriggerListTitle && ( -
- {t('tabs.allTriggers', { ns: 'workflow' })} -
- )} - - - {enableTriggerPlugin && ( - - )} - {enableTriggerPlugin && enable_marketplace && ( - } - list={marketplacePlugins} - searchText={trimmedSearchText} - category={PluginCategoryEnum.trigger} - tags={tags} - hideFindMoreFooter - /> - )} + )} + {enableTriggerPlugin && enable_marketplace && ( + } + list={marketplacePlugins} + searchText={trimmedSearchText} + category={PluginCategoryEnum.trigger} + tags={tags} + hideFindMoreFooter + /> + )} +
{shouldShowEmptyState && (
- {t('tabs.noPluginsFound', { ns: 'workflow' })} + {t('nodes.startPlaceholder.noTriggersFound', { ns: 'workflow' })}
- {shouldShowMarketplaceFooter && !shouldShowEmptyState && ( - // Footer - Same as Tools tab marketplace footer + {shouldShowMarketplaceFooter && ( - {t('findMoreInMarketplace', { ns: 'plugin' })} - + {isPanelVariant + ? ( + <> + + + + {t('nodes.startPlaceholder.browseMoreOnMarketplace', { ns: 'workflow' })} + + + ) + : ( + <> + {t('findMoreInMarketplace', { ns: 'plugin' })} + + + )} )}
diff --git a/web/app/components/workflow/block-selector/block-selector-row.tsx b/web/app/components/workflow/block-selector/block-selector-row.tsx new file mode 100644 index 00000000000..a9f1cf43b0d --- /dev/null +++ b/web/app/components/workflow/block-selector/block-selector-row.tsx @@ -0,0 +1,81 @@ +import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from 'react' +import { cn } from '@langgenius/dify-ui/cn' + +type SharedProps = { + children: ReactNode + className?: string + disabled?: boolean + hoverable?: boolean +} + +type ButtonRowProps = SharedProps + & Omit, 'className' | 'disabled'> + & { + as?: 'button' + nativeDisabled?: boolean + } + +type DivRowProps = SharedProps + & Omit, 'className'> + & { + as: 'div' + } + +type BlockSelectorRowProps = ButtonRowProps | DivRowProps + +const rowClassName = (className?: string, disabled = false, hoverable = true) => cn( + 'flex h-8 w-full items-center rounded-lg pr-2 pl-3', + !disabled && hoverable && 'hover:bg-state-base-hover', + disabled && 'cursor-not-allowed', + className, +) + +const buttonClassName = (className?: string, disabled = false, hoverable = true) => cn( + rowClassName(className, disabled, hoverable), + !disabled && 'cursor-pointer', + 'border-0 bg-transparent text-left focus-visible:ring-1 focus-visible:ring-components-input-border-hover focus-visible:outline-hidden', +) + +export function BlockSelectorRow(props: BlockSelectorRowProps) { + if (props.as === 'div') { + const { + as: _as, + children, + className, + disabled = false, + hoverable, + ...rest + } = props + + return ( +
+ {children} +
+ ) + } + + const { + as: _as, + children, + className, + disabled = false, + hoverable, + nativeDisabled, + type = 'button', + ...rest + } = props + + return ( + + ) +} diff --git a/web/app/components/workflow/block-selector/featured-triggers.tsx b/web/app/components/workflow/block-selector/featured-triggers.tsx index 13efb38c8f2..8b058feddb1 100644 --- a/web/app/components/workflow/block-selector/featured-triggers.tsx +++ b/web/app/components/workflow/block-selector/featured-triggers.tsx @@ -19,6 +19,7 @@ import { formatNumber } from '@/utils/format' import { getMarketplaceUrl } from '@/utils/var' import BlockIcon from '../block-icon' import { BlockEnum } from '../types' +import { BlockSelectorRow } from './block-selector-row' import { TriggerPluginActionPreviewCard } from './trigger-plugin/action-item' import TriggerPluginItem from './trigger-plugin/item' @@ -114,10 +115,10 @@ const FeaturedTriggers = ({ const showEmptyState = !isLoading && totalVisible === 0 return ( -
+
))} - {hasRes && ( -
-
- - - {t('searchInMarketplace', { ns: 'plugin' })} - -
-
- )}
) diff --git a/web/app/components/workflow/block-selector/start-blocks.tsx b/web/app/components/workflow/block-selector/start-blocks.tsx index 71323feeffa..dbd0129fd18 100644 --- a/web/app/components/workflow/block-selector/start-blocks.tsx +++ b/web/app/components/workflow/block-selector/start-blocks.tsx @@ -1,11 +1,13 @@ import type { BlockEnum, CommonNodeType } from '../types' import type { TriggerDefaultValue } from './types' +import { cn } from '@langgenius/dify-ui/cn' import { createPreviewCardHandle, PreviewCard, PreviewCardContent, PreviewCardTrigger, } from '@langgenius/dify-ui/preview-card' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { memo, useCallback, @@ -16,6 +18,7 @@ import { useTranslation } from 'react-i18next' import useNodes from '@/app/components/workflow/store/workflow/use-nodes' import BlockIcon from '../block-icon' import { BlockEnum as BlockEnumValues } from '../types' +import { BlockSelectorRow } from './block-selector-row' // import { useNodeMetaData } from '../hooks' import { START_BLOCKS } from './constants' @@ -25,6 +28,10 @@ type StartBlocksProps = { availableBlocksTypes?: BlockEnum[] onContentStateChange?: (hasContent: boolean) => void hideUserInput?: boolean + showMostCommonBadge?: boolean + showUserInputAdded?: boolean + showUserInputDisabled?: boolean + disabled?: boolean } type StartBlockPreviewPayload = { block: typeof START_BLOCKS[number] @@ -36,6 +43,10 @@ const StartBlocks = ({ availableBlocksTypes = [], onContentStateChange, hideUserInput = false, // Allow parent to explicitly hide Start node option (e.g. when one already exists). + showMostCommonBadge = false, + showUserInputAdded = false, + showUserInputDisabled = false, + disabled = false, }: StartBlocksProps) => { const { t } = useTranslation() const nodes = useNodes() @@ -54,8 +65,9 @@ const StartBlocks = ({ } return START_BLOCKS.filter((block) => { - // Hide User Input (Start) if it already exists in workflow or if hideUserInput is true - if (block.type === BlockEnumValues.Start && (hasStartNode || hideUserInput)) + // Hide User Input (Start) if it already exists in workflow or if hideUserInput is true. + // In read-only conflict modes, keep it visible so the row can show Added or disabled tooltip state. + if (block.type === BlockEnumValues.Start && (hasStartNode || hideUserInput) && !showUserInputAdded && !showUserInputDisabled) return false // Filter by search text @@ -66,7 +78,7 @@ const StartBlocks = ({ // availableBlocksTypes now contains properly filtered entry node types from parent return availableBlocksTypes.includes(block.type) }) - }, [searchText, availableBlocksTypes, nodes, t, hideUserInput]) + }, [searchText, availableBlocksTypes, nodes, t, hideUserInput, showUserInputAdded, showUserInputDisabled]) const isEmpty = filteredBlocks.length === 0 @@ -78,43 +90,84 @@ const StartBlocks = ({ // reachable from the inspector + canvas once the row is clicked to insert // the start node, so hover/focus-only activation is a11y-safe. See // packages/dify-ui/AGENTS.md → Overlay Primitive Selection. - const renderBlock = useCallback((block: typeof START_BLOCKS[number]) => ( - onSelect(block.type)} - > + const renderBlock = useCallback((block: typeof START_BLOCKS[number]) => { + const isUserInput = block.type === BlockEnumValues.Start + const isUserInputDisabled = isUserInput && showUserInputDisabled + const isRowDisabled = disabled || (isUserInput && showUserInputAdded) || isUserInputDisabled + const label = t(`blocks.${block.type}`, { ns: 'workflow' }) + const disabledReason = t('nodes.startPlaceholder.userInputConflictTip', { ns: 'workflow' }) + const row = ( + { + if (isRowDisabled) + return + onSelect(block.type) + }} + > +
- {t(`blocks.${block.type}`, { ns: 'workflow' })} - {block.type === BlockEnumValues.Start && ( + {label} + {isUserInput && showUserInputAdded && ( + + {t('operation.added', { ns: 'common' })} + + )} + {isUserInput && showMostCommonBadge && !showUserInputAdded && ( + + {t('blocks.mostCommon', { ns: 'workflow' })} + + )} + {isUserInput && !showMostCommonBadge && !showUserInputAdded && !showUserInputDisabled && ( {t('blocks.originalStartNode', { ns: 'workflow' })} )}
- )} - /> - ), [onSelect, previewCardHandle, t]) +
+ ) + + if (isUserInputDisabled) { + return ( + + + +

+ {disabledReason} +

+
+
+ ) + } + + return ( + + ) + }, [disabled, onSelect, previewCardHandle, showMostCommonBadge, showUserInputAdded, showUserInputDisabled, t]) if (isEmpty) return null return (
-
+
{filteredBlocks.map((block, index) => (
{renderBlock(block)} - {block.type === BlockEnumValues.Start && index < filteredBlocks.length - 1 && ( + {block.type === BlockEnumValues.Start && !showMostCommonBadge && index < filteredBlocks.length - 1 && (
@@ -147,9 +200,17 @@ function StartBlockPreviewCard({ return null const { block } = payload + const description = block.type === BlockEnumValues.Start + ? t('nodes.start.userInputTipDescription', { ns: 'workflow' }) + : t(`blocksAbout.${block.type}`, { ns: 'workflow' }) + const showDifyTeamAuthor = [ + BlockEnumValues.Start, + BlockEnumValues.TriggerWebhook, + BlockEnumValues.TriggerSchedule, + ].includes(block.type) return ( - +
- {block.type === BlockEnumValues.TriggerWebhook - ? t('customWebhook', { ns: 'workflow' }) - : t(`blocks.${block.type}`, { ns: 'workflow' })} + {t(`blocks.${block.type}`, { ns: 'workflow' })}
- {t(`blocksAbout.${block.type}`, { ns: 'workflow' })} + {description}
- {(block.type === BlockEnumValues.TriggerWebhook || block.type === BlockEnumValues.TriggerSchedule) && ( + {showDifyTeamAuthor && (
{t('author', { ns: 'tools' })} {' '} diff --git a/web/app/components/workflow/block-selector/tabs.tsx b/web/app/components/workflow/block-selector/tabs.tsx index 1c960436fef..2145ce47f88 100644 --- a/web/app/components/workflow/block-selector/tabs.tsx +++ b/web/app/components/workflow/block-selector/tabs.tsx @@ -1,4 +1,4 @@ -import type { Dispatch, FC, SetStateAction } from 'react' +import type { Dispatch, FC, ReactNode, SetStateAction } from 'react' import type { BlockEnum, NodeDefault, @@ -10,6 +10,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/too import { useSuspenseQuery } from '@tanstack/react-query' import { memo, useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' +import { useDocLink } from '@/context/i18n' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { useFeaturedToolsRecommendations } from '@/service/use-plugins' import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllBuiltInTools } from '@/service/use-tools' @@ -35,13 +36,16 @@ type TabsProps = { key: TabsEnum name: string disabled?: boolean - disabledTip?: string + disabledTip?: ReactNode + disabledTipLinkKey?: 'startNodesDocs' }> filterElem: React.ReactNode noBlocks?: boolean noTools?: boolean forceShowStartContent?: boolean // Force show Start content even when noBlocks=true allowStartNodeSelection?: boolean // Allow user input option even when trigger node already exists (e.g. change-node flow or when no Start node yet). + hasUserInputNode?: boolean + hasTriggerNode?: boolean snippetsElem?: React.ReactNode } @@ -107,11 +111,15 @@ const TabHeaderItem = ({ activeTab, onActiveTabChange, disabledTip, + disabledTipLinkHref, + disabledTipLinkLabel, }: { tab: TabsProps['tabs'][number] activeTab: TabsEnum onActiveTabChange: (activeTab: TabsEnum) => void - disabledTip: string + disabledTip: ReactNode + disabledTipLinkHref?: string + disabledTipLinkLabel?: string }) => { const className = cn( 'relative mr-0.5 flex h-8 items-center rounded-t-lg px-3 system-sm-medium', @@ -144,8 +152,21 @@ const TabHeaderItem = ({ )} /> - - {disabledTip} + +
+

{disabledTip}

+ {disabledTipLinkHref && disabledTipLinkLabel && ( + e.stopPropagation()} + > + {disabledTipLinkLabel} + + )} +
) @@ -179,9 +200,12 @@ const Tabs: FC = ({ noTools, forceShowStartContent = false, allowStartNodeSelection = false, + hasUserInputNode = false, + hasTriggerNode = false, snippetsElem, }) => { const { t } = useTranslation() + const docLink = useDocLink() const { data: buildInTools } = useAllBuiltInTools() const { data: customTools } = useAllCustomTools() const { data: workflowTools } = useAllWorkflowTools() @@ -234,6 +258,8 @@ const Tabs: FC = ({ activeTab={activeTab} onActiveTabChange={onActiveTabChange} disabledTip={tab.disabledTip || disabledTip} + disabledTipLinkHref={tab.disabledTipLinkKey === 'startNodesDocs' ? docLink('/use-dify/nodes/trigger/overview') : undefined} + disabledTipLinkLabel={tab.disabledTipLinkKey === 'startNodesDocs' ? t('tabs.startDisabledTipLearnMore', { ns: 'workflow' }) : undefined} /> )) } @@ -246,6 +272,8 @@ const Tabs: FC = ({
{ })) }) + it('should select trigger plugin action items from the keyboard', async () => { + const user = userEvent.setup() + const onSelect = vi.fn() + const provider = createTriggerProvider() + const event = createEvent('on_created', 'On Created') + + render( + , + ) + + const action = screen.getByRole('button', { name: 'On Created' }) + await user.tab() + expect(action).toHaveFocus() + + await user.keyboard('{Enter}') + + expect(onSelect).toHaveBeenCalledWith(BlockEnum.TriggerPlugin, expect.objectContaining({ + event_name: 'on_created', + event_label: 'On Created', + })) + }) + it('should expand providers and select workflow trigger providers directly', async () => { const user = userEvent.setup() const onSelect = vi.fn() @@ -135,6 +162,9 @@ describe('trigger plugin selector components', () => { ) await user.click(screen.getByText('Trigger Provider')) + + expect(screen.getByLabelText('workflow.tabs.allTriggers')).toHaveClass('max-h-[240px]', 'overscroll-contain') + await user.click(screen.getByText('Second Event')) expect(onSelect).toHaveBeenCalledWith(BlockEnum.TriggerPlugin, expect.objectContaining({ @@ -163,6 +193,34 @@ describe('trigger plugin selector components', () => { })) }) + it('should expand trigger providers from the keyboard', async () => { + const user = userEvent.setup() + const onSelect = vi.fn() + + render( + , + ) + + const provider = screen.getByRole('button', { name: /Trigger Provider/ }) + await user.tab() + expect(provider).toHaveFocus() + + await user.keyboard(' ') + + expect(screen.getByRole('region', { name: 'workflow.tabs.allTriggers' })).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Second Event' })).toBeInTheDocument() + }) + it('should filter trigger plugins and report whether content exists', async () => { const onContentStateChange = vi.fn() mockUseAllTriggerPlugins.mockReturnValue({ diff --git a/web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx b/web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx index b4a243b8079..b5e283f4f8a 100644 --- a/web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx +++ b/web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx @@ -40,9 +40,14 @@ const TriggerPluginActionItem: FC = ({ const language = useGetLanguage() const row = ( -
{ if (disabled) return @@ -76,7 +81,7 @@ const TriggerPluginActionItem: FC = ({ {isAdded && (
{t('addToolModal.added', { ns: 'tools' })}
)} -
+ ) return ( diff --git a/web/app/components/workflow/block-selector/trigger-plugin/item.tsx b/web/app/components/workflow/block-selector/trigger-plugin/item.tsx index 5932bbb94ad..75e747d4180 100644 --- a/web/app/components/workflow/block-selector/trigger-plugin/item.tsx +++ b/web/app/components/workflow/block-selector/trigger-plugin/item.tsx @@ -3,6 +3,13 @@ import type { FC } from 'react' import type { TriggerPluginActionPreviewCardHandle } from './action-item' import type { TriggerDefaultValue, TriggerWithProvider } from '@/app/components/workflow/block-selector/types' import { cn } from '@langgenius/dify-ui/cn' +import { + ScrollAreaContent, + ScrollAreaRoot, + ScrollAreaScrollbar, + ScrollAreaThumb, + ScrollAreaViewport, +} from '@langgenius/dify-ui/scroll-area' import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react' import * as React from 'react' import { useMemo, useRef } from 'react' @@ -14,6 +21,7 @@ import { useGetLanguage } from '@/context/i18n' import useTheme from '@/hooks/use-theme' import { Theme } from '@/types/app' import { basePath } from '@/utils/var' +import { BlockSelectorRow } from '../block-selector-row' import TriggerPluginActionItem from './action-item' const normalizeProviderIcon = (icon?: TriggerWithProvider['icon']) => { @@ -30,6 +38,7 @@ type Props = Readonly<{ hasSearchText: boolean previewCardHandle: TriggerPluginActionPreviewCardHandle onSelect: (type: BlockEnum, trigger?: TriggerDefaultValue) => void + disabled?: boolean }> const TriggerPluginItem: FC = ({ @@ -38,6 +47,7 @@ const TriggerPluginItem: FC = ({ hasSearchText, previewCardHandle, onSelect, + disabled = false, }) => { const { t } = useTranslation() const language = useGetLanguage() @@ -93,9 +103,13 @@ const TriggerPluginItem: FC = ({ ref={ref} >
-
{ + if (disabled) + return if (hasAction) { setIsFold(!isFold) return @@ -125,13 +139,14 @@ const TriggerPluginItem: FC = ({ }) }} > -
+
-
+
{notShowProvider ? actions[0]?.label[language] : payload.label[language]} {groupName}
@@ -142,20 +157,33 @@ const TriggerPluginItem: FC = ({ )}
-
+ {!notShowProvider && hasAction && !isFold && ( - actions.map(action => ( - - )) + + + + {actions.map(action => ( + + ))} + + + + + + )}
diff --git a/web/app/components/workflow/block-selector/trigger-plugin/list.tsx b/web/app/components/workflow/block-selector/trigger-plugin/list.tsx index 2d2752c4f67..d73ac3985dd 100644 --- a/web/app/components/workflow/block-selector/trigger-plugin/list.tsx +++ b/web/app/components/workflow/block-selector/trigger-plugin/list.tsx @@ -14,12 +14,14 @@ type TriggerPluginListProps = { searchText: string onContentStateChange?: (hasContent: boolean) => void tags?: string[] + disabled?: boolean } const TriggerPluginList = ({ onSelect, searchText, onContentStateChange, + disabled = false, }: TriggerPluginListProps) => { const { data: triggerPluginsData } = useAllTriggerPlugins() const language = useGetLanguage() @@ -99,6 +101,7 @@ const TriggerPluginList = ({ key={plugin.id} payload={plugin} onSelect={onSelect} + disabled={disabled} hasSearchText={!!searchText} previewCardHandle={previewCardHandle} /> diff --git a/web/app/components/workflow/hooks/__tests__/use-available-blocks.spec.ts b/web/app/components/workflow/hooks/__tests__/use-available-blocks.spec.ts index 0989b08cbdb..955c514c81b 100644 --- a/web/app/components/workflow/hooks/__tests__/use-available-blocks.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-available-blocks.spec.ts @@ -11,6 +11,7 @@ vi.mock('@/context/i18n', () => ({ useGetLanguage: () => 'en' })) const mockNodeTypes = [ BlockEnum.Start, + BlockEnum.StartPlaceholder, BlockEnum.End, BlockEnum.LLM, BlockEnum.Code, @@ -56,6 +57,11 @@ describe('useAvailableBlocks', () => { expect(result.current.availablePrevBlocks).toEqual([]) }) + it('should return empty array for StartPlaceholder node', () => { + const { result } = renderWorkflowHook(() => useAvailableBlocks(BlockEnum.StartPlaceholder), { hooksStoreProps }) + expect(result.current.availablePrevBlocks).toEqual([]) + }) + it('should return empty array for trigger nodes', () => { for (const trigger of [BlockEnum.TriggerPlugin, BlockEnum.TriggerWebhook, BlockEnum.TriggerSchedule]) { const { result } = renderWorkflowHook(() => useAvailableBlocks(trigger), { hooksStoreProps }) @@ -97,9 +103,15 @@ describe('useAvailableBlocks', () => { expect(result.current.availableNextBlocks).toEqual([]) }) + it('should return empty array for StartPlaceholder node', () => { + const { result } = renderWorkflowHook(() => useAvailableBlocks(BlockEnum.StartPlaceholder), { hooksStoreProps }) + expect(result.current.availableNextBlocks).toEqual([]) + }) + it('should return all available nodes for regular block types', () => { const { result } = renderWorkflowHook(() => useAvailableBlocks(BlockEnum.LLM), { hooksStoreProps }) expect(result.current.availableNextBlocks.length).toBeGreaterThan(0) + expect(result.current.availableNextBlocks).not.toContain(BlockEnum.StartPlaceholder) }) }) @@ -144,6 +156,14 @@ describe('useAvailableBlocks', () => { expect(blocks.availablePrevBlocks).toEqual([]) }) + it('should return no blocks for StartPlaceholder node', () => { + const { result } = renderWorkflowHook(() => useAvailableBlocks(BlockEnum.LLM), { hooksStoreProps }) + const blocks = result.current.getAvailableBlocks(BlockEnum.StartPlaceholder) + + expect(blocks.availablePrevBlocks).toEqual([]) + expect(blocks.availableNextBlocks).toEqual([]) + }) + it('should return empty nextBlocks for LoopEnd/KnowledgeBase and available nodes for End', () => { const { result } = renderWorkflowHook(() => useAvailableBlocks(BlockEnum.LLM), { hooksStoreProps }) diff --git a/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts b/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts index ab9eb993d1f..ac7e11e113d 100644 --- a/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts @@ -256,6 +256,31 @@ describe('useChecklist', () => { expect(startRequired!.canNavigate).toBe(false) }) + it('should not report the global missing start node item when a start placeholder is present', () => { + mockNodesMap[BlockEnum.StartPlaceholder] = { + checkValid: () => ({ errorMessage: 'workflow.nodes.startPlaceholder.validationRequired' }), + metaData: { isStart: false, isRequired: false }, + } + const placeholderNode = createNode({ + id: 'start-placeholder', + data: { type: BlockEnum.StartPlaceholder, title: 'Workflow start' }, + }) + + const { result } = renderWorkflowHook( + () => useChecklist([placeholderNode], []), + ) + + expect(result.current.find((item: ChecklistItem) => item.id === 'start-node-required')).toBeUndefined() + expect(result.current).toEqual([ + expect.objectContaining({ + id: 'start-placeholder', + type: BlockEnum.StartPlaceholder, + unConnected: false, + errorMessages: ['workflow.nodes.startPlaceholder.validationRequired'], + }), + ]) + }) + it('should detect plugin not installed', () => { const startNode = createNode({ id: 'start', data: { type: BlockEnum.Start, title: 'Start' } }) const toolNode = createNode({ diff --git a/web/app/components/workflow/hooks/use-available-blocks.ts b/web/app/components/workflow/hooks/use-available-blocks.ts index 888e1264744..c42eee5345f 100644 --- a/web/app/components/workflow/hooks/use-available-blocks.ts +++ b/web/app/components/workflow/hooks/use-available-blocks.ts @@ -6,6 +6,9 @@ import { BlockEnum } from '../types' import { useNodesMetaData } from './use-nodes-meta-data' const availableBlocksFilter = (nodeType: BlockEnum, inContainer?: boolean) => { + if (nodeType === BlockEnum.StartPlaceholder) + return false + if (inContainer && (nodeType === BlockEnum.Iteration || nodeType === BlockEnum.Loop || nodeType === BlockEnum.End || nodeType === BlockEnum.DataSource || nodeType === BlockEnum.KnowledgeBase || nodeType === BlockEnum.HumanInput)) return false @@ -21,7 +24,7 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, inContainer?: boolean) } = useNodesMetaData() const availableNodesType = useMemo(() => availableNodes.map(node => node.metaData.type), [availableNodes]) const availablePrevBlocks = useMemo(() => { - if (!nodeType || nodeType === BlockEnum.Start || nodeType === BlockEnum.DataSource + if (!nodeType || nodeType === BlockEnum.Start || nodeType === BlockEnum.StartPlaceholder || nodeType === BlockEnum.DataSource || nodeType === BlockEnum.TriggerPlugin || nodeType === BlockEnum.TriggerWebhook || nodeType === BlockEnum.TriggerSchedule) { return [] @@ -30,7 +33,7 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, inContainer?: boolean) return availableNodesType }, [availableNodesType, nodeType]) const availableNextBlocks = useMemo(() => { - if (!nodeType || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase) + if (!nodeType || nodeType === BlockEnum.StartPlaceholder || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase) return [] return availableNodesType @@ -38,11 +41,11 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, inContainer?: boolean) const getAvailableBlocks = useCallback((nodeType?: BlockEnum, inContainer?: boolean) => { let availablePrevBlocks = availableNodesType - if (!nodeType || nodeType === BlockEnum.Start || nodeType === BlockEnum.DataSource) + if (!nodeType || nodeType === BlockEnum.Start || nodeType === BlockEnum.StartPlaceholder || nodeType === BlockEnum.DataSource) availablePrevBlocks = [] let availableNextBlocks = availableNodesType - if (!nodeType || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase) + if (!nodeType || nodeType === BlockEnum.StartPlaceholder || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase) availableNextBlocks = [] return { diff --git a/web/app/components/workflow/hooks/use-checklist.ts b/web/app/components/workflow/hooks/use-checklist.ts index 2f74c66d5f4..2eb73ae2a2c 100644 --- a/web/app/components/workflow/hooks/use-checklist.ts +++ b/web/app/components/workflow/hooks/use-checklist.ts @@ -310,7 +310,8 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType? } const isStartNodeMeta = nodesExtraData?.[node!.data.type as BlockEnum]?.metaData.isStart ?? false - const canSkipConnectionCheck = shouldCheckStartNode ? isStartNodeMeta : true + const isStartPlaceholderNode = node!.data.type === BlockEnum.StartPlaceholder + const canSkipConnectionCheck = shouldCheckStartNode ? isStartNodeMeta || isStartPlaceholderNode : true const isUnconnected = !validNodes.some(n => n.id === node!.id) const shouldShowError = errorMessages.length > 0 || (isUnconnected && !canSkipConnectionCheck) @@ -337,7 +338,8 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType? // Check for start nodes (including triggers) if (shouldCheckStartNode) { const startNodesFiltered = nodes.filter(node => START_NODE_TYPES.includes(node.data.type as BlockEnum)) - if (startNodesFiltered.length === 0) { + const hasStartPlaceholderNode = nodes.some(node => node.data.type === BlockEnum.StartPlaceholder) + if (startNodesFiltered.length === 0 && !hasStartPlaceholderNode) { list.push({ id: 'start-node-required', type: BlockEnum.Start, diff --git a/web/app/components/workflow/hooks/use-inspect-vars-crud.ts b/web/app/components/workflow/hooks/use-inspect-vars-crud.ts index 1bca16aea6a..b978455a075 100644 --- a/web/app/components/workflow/hooks/use-inspect-vars-crud.ts +++ b/web/app/components/workflow/hooks/use-inspect-vars-crud.ts @@ -12,7 +12,8 @@ const varsAppendStartNodeKeys = ['query', 'files'] const useInspectVarsCrud = () => { const partOfNodesWithInspectVars = useStore(s => s.nodesWithInspectVars) const configsMap = useHooksStore(s => s.configsMap) - const shouldSkipSharedVariableQueries = configsMap?.flowType === FlowType.ragPipeline || configsMap?.flowType === FlowType.snippet + const shouldSkipSharedVariableQueries = configsMap?.flowType === FlowType.ragPipeline + || configsMap?.flowType === FlowType.snippet const variableFlowId = shouldSkipSharedVariableQueries ? '' : configsMap?.flowId const { data: conversationVars } = useConversationVarValues(configsMap?.flowType, variableFlowId) const { data: allSystemVars } = useSysVarValues(configsMap?.flowType, variableFlowId) diff --git a/web/app/components/workflow/nodes/__tests__/index.spec.tsx b/web/app/components/workflow/nodes/__tests__/index.spec.tsx index 41eb853a99e..a65b3278382 100644 --- a/web/app/components/workflow/nodes/__tests__/index.spec.tsx +++ b/web/app/components/workflow/nodes/__tests__/index.spec.tsx @@ -8,9 +8,11 @@ import CustomNode, { Panel } from '../index' vi.mock('../components', () => ({ NodeComponentMap: { [BlockEnum.Start]: () =>
start-node-component
, + [BlockEnum.StartPlaceholder]: () =>
start-placeholder-node-component
, }, PanelComponentMap: { [BlockEnum.Start]: () =>
start-panel-component
, + [BlockEnum.StartPlaceholder]: () =>
start-placeholder-panel-component
, }, })) @@ -32,9 +34,10 @@ vi.mock('../_base/node', () => ({ ), })) -vi.mock('../_base/components/workflow-panel', () => ({ - __esModule: true, - default: ({ +vi.mock('../_base/components/workflow-panel', async () => { + const React = await vi.importActual('react') + + const MockWorkflowPanel = ({ id, data, children, @@ -42,13 +45,23 @@ vi.mock('../_base/components/workflow-panel', () => ({ id: string data: { type: BlockEnum } children: ReactElement - }) => ( -
-
{`base-panel:${id}:${data.type}`}
- {children} -
- ), -})) + }) => { + const [initialType] = React.useState(data.type) + + return ( +
+
{`base-panel:${id}:${data.type}`}
+
{`base-panel-initial:${initialType}`}
+ {children} +
+ ) + } + + return { + __esModule: true, + default: MockWorkflowPanel, + } +}) const createNodeData = (): WorkflowNode['data'] => ({ title: 'Start', @@ -56,6 +69,12 @@ const createNodeData = (): WorkflowNode['data'] => ({ type: BlockEnum.Start, }) +const createStartPlaceholderData = (): WorkflowNode['data'] => ({ + title: 'Pick a start node', + desc: '', + type: BlockEnum.StartPlaceholder, +}) + const baseNodeProps = { type: CUSTOM_NODE, selected: false, @@ -93,6 +112,29 @@ describe('workflow nodes index', () => { expect(screen.getByText('start-panel-component')).toBeInTheDocument() }) + it('should remount the base panel when a node keeps its id but changes type', () => { + const { rerender } = render( + , + ) + + expect(screen.getByText('base-panel-initial:start-placeholder')).toBeInTheDocument() + + rerender( + , + ) + + expect(screen.getByText('base-panel:node-1:start')).toBeInTheDocument() + expect(screen.getByText('base-panel-initial:start')).toBeInTheDocument() + }) + it('should return null for non-custom panel types', () => { const { container } = render( { expect(document.querySelector('.i-ri-pause-circle-fill')).toBeInTheDocument() }) - it('should render success icon when inspect vars exist without running status and hide description for loop nodes', () => { + it('should render success icon when inspect vars exist without running status and hide description for non-description nodes', () => { const t = ((key: string) => key) as unknown as TFunction const { rerender } = render( { rerender() expect(screen.queryByText('hidden')).not.toBeInTheDocument() + + rerender() + expect(screen.queryByText('old placeholder description')).not.toBeInTheDocument() }) }) diff --git a/web/app/components/workflow/nodes/_base/__tests__/node.helpers.spec.ts b/web/app/components/workflow/nodes/_base/__tests__/node.helpers.spec.ts index 78e1f938c55..01275cbcd33 100644 --- a/web/app/components/workflow/nodes/_base/__tests__/node.helpers.spec.ts +++ b/web/app/components/workflow/nodes/_base/__tests__/node.helpers.spec.ts @@ -24,6 +24,7 @@ describe('node helpers', () => { it('should identify entry and container nodes', () => { expect(isEntryWorkflowNode(BlockEnum.Start)).toBe(true) + expect(isEntryWorkflowNode(BlockEnum.StartPlaceholder)).toBe(true) expect(isEntryWorkflowNode(BlockEnum.TriggerWebhook)).toBe(true) expect(isEntryWorkflowNode(BlockEnum.Tool)).toBe(false) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx index c73361e35c3..c6da272c937 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -91,6 +91,11 @@ import { } from './helpers' import LastRun from './last-run' import useLastRun from './last-run/use-last-run' +import { + StartPlaceholderPanelBody, + StartPlaceholderPanelDescription, + StartPlaceholderPanelTitle, +} from './start-placeholder-panel' import { TriggerSubscription } from './trigger-subscription' import { TabType } from './types' @@ -480,6 +485,19 @@ const BasePanel: FC = ({ const singleRunActionLabel = isSingleRunning ? t('debug.variableInspect.trigger.stop', { ns: 'workflow' }) : runThisStepLabel + const isStartPlaceholderPanel = data.type === BlockEnum.StartPlaceholder + const panelChildren = cloneElement(children as any, { + id, + data, + panelProps: { + getInputVars, + toVarInputs, + runInputData, + setRunInputData, + runResult, + runInputDataRef, + }, + }) const panelTabs = ( @@ -519,16 +537,24 @@ const BasePanel: FC = ({ >
- - + {!isStartPlaceholderPanel && ( + + )} + {isStartPlaceholderPanel + ? ( + + ) + : ( + + )} {viewingUsers.length > 0 && (
= ({
-
- -
- { - needsToolAuth && ( - -
- {panelTabs} - + ) + : ( +
+ +
+ )} + {!isStartPlaceholderPanel && ( + <> + { + needsToolAuth && ( + -
-
- ) - } - { - !!currentDataSource && ( - -
- {panelTabs} - +
+ {panelTabs} + +
+ + ) + } + { + !!currentDataSource && ( + -
-
- ) - } - { - currentTriggerPlugin && ( - - {panelTabs} - - ) - } - { - !needsToolAuth && !currentDataSource && !currentTriggerPlugin && ( -
- {panelTabs} -
- ) - } - + isAuthorized={currentDataSource.is_authorized} + > +
+ {panelTabs} + +
+ + ) + } + { + currentTriggerPlugin && ( + + {panelTabs} + + ) + } + { + !needsToolAuth && !currentDataSource && !currentTriggerPlugin && ( +
+ {panelTabs} +
+ ) + } + + + )}
- -
- {cloneElement(children as any, { - id, - data, - panelProps: { - getInputVars, - toVarInputs, - runInputData, - setRunInputData, - runResult, - runInputDataRef, - }, - })} -
- - { - hasRetryNode(data.type) && ( - - ) - } - { - hasErrorHandleNode(data.type) && ( - - ) - } - { - !!availableNextBlocks.length && ( -
-
- {t('panel.nextStep', { ns: 'workflow' }).toLocaleUpperCase()} -
-
- {t('panel.addNextStep', { ns: 'workflow' })} -
- -
- ) - } - {readmeEntranceComponent} -
- - - + {isStartPlaceholderPanel && ( + + {panelChildren} + + )} + + {!isStartPlaceholderPanel && ( + +
+ {panelChildren} +
+ + { + hasRetryNode(data.type) && ( + + ) + } + { + hasErrorHandleNode(data.type) && ( + + ) + } + { + !!availableNextBlocks.length && ( +
+
+ {t('panel.nextStep', { ns: 'workflow' }).toLocaleUpperCase()} +
+
+ {t('panel.addNextStep', { ns: 'workflow' })} +
+ +
+ ) + } + {readmeEntranceComponent} +
+ )} + + {!isStartPlaceholderPanel && ( + + + + )}
diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts index b0bbc98d7e4..a538bcf29e3 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts @@ -54,6 +54,7 @@ const singleRunFormParamsHooks: Record = { [BlockEnum.DocExtractor]: useDocExtractorSingleRunFormParams, [BlockEnum.Loop]: useLoopSingleRunFormParams, [BlockEnum.Start]: useStartSingleRunFormParams, + [BlockEnum.StartPlaceholder]: undefined, [BlockEnum.IfElse]: useIfElseSingleRunFormParams, [BlockEnum.VariableAggregator]: useVariableAggregatorSingleRunFormParams, [BlockEnum.Assigner]: useVariableAssignerSingleRunFormParams, @@ -93,6 +94,7 @@ const getDataForCheckMoreHooks: Record = { [BlockEnum.DocExtractor]: undefined, [BlockEnum.Loop]: undefined, [BlockEnum.Start]: undefined, + [BlockEnum.StartPlaceholder]: undefined, [BlockEnum.IfElse]: undefined, [BlockEnum.VariableAggregator]: undefined, [BlockEnum.End]: undefined, diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/start-placeholder-panel.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/start-placeholder-panel.tsx new file mode 100644 index 00000000000..fa8e6f41bc3 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/start-placeholder-panel.tsx @@ -0,0 +1,34 @@ +import type { ReactNode } from 'react' +import { useTranslation } from 'react-i18next' + +export function StartPlaceholderPanelTitle() { + const { t } = useTranslation() + + return ( +
+ {t('nodes.startPlaceholder.panelTitle', { ns: 'workflow' })} +
+ ) +} + +export function StartPlaceholderPanelDescription() { + const { t } = useTranslation() + + return ( +
+ {t('nodes.startPlaceholder.panelDescription', { ns: 'workflow' })} +
+ ) +} + +export function StartPlaceholderPanelBody({ + children, +}: { + children: ReactNode +}) { + return ( +
+ {children} +
+ ) +} diff --git a/web/app/components/workflow/nodes/_base/node-sections.tsx b/web/app/components/workflow/nodes/_base/node-sections.tsx index 8b60c103c7f..3916c054d03 100644 --- a/web/app/components/workflow/nodes/_base/node-sections.tsx +++ b/web/app/components/workflow/nodes/_base/node-sections.tsx @@ -83,7 +83,7 @@ export const NodeBody = ({ } export const NodeDescription = ({ data }: { data: NodeProps['data'] }) => { - if (!data.desc || data.type === BlockEnum.Iteration || data.type === BlockEnum.Loop) + if (!data.desc || data.type === BlockEnum.Iteration || data.type === BlockEnum.Loop || data.type === BlockEnum.StartPlaceholder) return null return ( diff --git a/web/app/components/workflow/nodes/_base/node.helpers.tsx b/web/app/components/workflow/nodes/_base/node.helpers.tsx index 2019517133d..2614d4b058f 100644 --- a/web/app/components/workflow/nodes/_base/node.helpers.tsx +++ b/web/app/components/workflow/nodes/_base/node.helpers.tsx @@ -24,7 +24,7 @@ export const getLoopIndexTextKey = (runningStatus: NodeRunningStatus | undefined } export const isEntryWorkflowNode = (type: NodeProps['data']['type']) => { - return isTriggerNode(type) || type === BlockEnum.Start + return isTriggerNode(type) || type === BlockEnum.Start || type === BlockEnum.StartPlaceholder } export const isContainerNode = (type: NodeProps['data']['type']) => { diff --git a/web/app/components/workflow/nodes/_base/node.tsx b/web/app/components/workflow/nodes/_base/node.tsx index 13ec43f3a75..680a8894d0e 100644 --- a/web/app/components/workflow/nodes/_base/node.tsx +++ b/web/app/components/workflow/nodes/_base/node.tsx @@ -229,7 +229,7 @@ const BaseNode: FC = ({ ) } { - !data._isCandidate && ( + data.type !== BlockEnum.StartPlaceholder && !data._isCandidate && ( = ({ ) } { - data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && data.type !== BlockEnum.HumanInput && !data._isCandidate && ( + data.type !== BlockEnum.StartPlaceholder && data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && data.type !== BlockEnum.HumanInput && !data._isCandidate && ( = ({
) - const isStartNode = data.type === BlockEnum.Start + const isStartNode = data.type === BlockEnum.Start || data.type === BlockEnum.StartPlaceholder const isEntryNode = isEntryWorkflowNode(data.type) return isEntryNode diff --git a/web/app/components/workflow/nodes/components.ts b/web/app/components/workflow/nodes/components.ts index ec1e4422bf9..7c196e3f2f5 100644 --- a/web/app/components/workflow/nodes/components.ts +++ b/web/app/components/workflow/nodes/components.ts @@ -36,6 +36,8 @@ import ParameterExtractorNode from './parameter-extractor/node' import ParameterExtractorPanel from './parameter-extractor/panel' import QuestionClassifierNode from './question-classifier/node' import QuestionClassifierPanel from './question-classifier/panel' +import StartPlaceholderNode from './start-placeholder/node' +import StartPlaceholderPanel from './start-placeholder/panel' import StartNode from './start/node' import StartPanel from './start/panel' import TemplateTransformNode from './template-transform/node' @@ -53,6 +55,7 @@ import VariableAssignerPanel from './variable-assigner/panel' export const NodeComponentMap: Record> = { [BlockEnum.Start]: StartNode, + [BlockEnum.StartPlaceholder]: StartPlaceholderNode, [BlockEnum.End]: EndNode, [BlockEnum.Answer]: AnswerNode, [BlockEnum.LLM]: LLMNode, @@ -82,6 +85,7 @@ export const NodeComponentMap: Record> = { export const PanelComponentMap: Record> = { [BlockEnum.Start]: StartPanel, + [BlockEnum.StartPlaceholder]: StartPlaceholderPanel, [BlockEnum.End]: EndPanel, [BlockEnum.Answer]: AnswerPanel, [BlockEnum.LLM]: LLMPanel, diff --git a/web/app/components/workflow/nodes/index.tsx b/web/app/components/workflow/nodes/index.tsx index 7575a156cf9..51893953d41 100644 --- a/web/app/components/workflow/nodes/index.tsx +++ b/web/app/components/workflow/nodes/index.tsx @@ -46,7 +46,7 @@ export const Panel = memo((props: PanelProps) => { if (nodeClass === CUSTOM_NODE) { return ( diff --git a/web/app/components/workflow/nodes/start-placeholder/__tests__/node.spec.tsx b/web/app/components/workflow/nodes/start-placeholder/__tests__/node.spec.tsx new file mode 100644 index 00000000000..6d3dcfcce41 --- /dev/null +++ b/web/app/components/workflow/nodes/start-placeholder/__tests__/node.spec.tsx @@ -0,0 +1,31 @@ +import { render, screen } from '@testing-library/react' +import { BlockEnum } from '@/app/components/workflow/types' +import Node from '../node' + +describe('StartPlaceholderNode', () => { + it('should show the right-panel hint while selected and the click hint after the panel closes', () => { + const { rerender } = render( + , + ) + + expect(screen.getByText('workflow.nodes.startPlaceholder.nodeDescription')).toBeInTheDocument() + + rerender( + , + ) + + expect(screen.getByText('workflow.nodes.startPlaceholder.nodeCollapsedDescription')).toBeInTheDocument() + }) +}) diff --git a/web/app/components/workflow/nodes/start-placeholder/__tests__/panel.spec.tsx b/web/app/components/workflow/nodes/start-placeholder/__tests__/panel.spec.tsx new file mode 100644 index 00000000000..5e98de3b043 --- /dev/null +++ b/web/app/components/workflow/nodes/start-placeholder/__tests__/panel.spec.tsx @@ -0,0 +1,139 @@ +import type { Node } from '@/app/components/workflow/types' +import { fireEvent, render, screen } from '@testing-library/react' +import { BlockEnum } from '@/app/components/workflow/types' +import Panel from '../panel' + +const mocks = vi.hoisted(() => ({ + autoGenerateWebhookUrl: vi.fn(), + handleSyncWorkflowDraft: vi.fn(), + setHasSelectedStartNode: vi.fn(), + setNodes: vi.fn(), + setShouldAutoOpenStartNodeSelector: vi.fn(), +})) + +let currentNodes: Node[] = [] + +vi.mock('reactflow', () => ({ + useStoreApi: () => ({ + getState: () => ({ + getNodes: () => currentNodes, + setNodes: mocks.setNodes, + }), + }), +})) + +vi.mock('@/app/components/plugins/marketplace/search-box', () => ({ + default: () => , +})) + +vi.mock('@/app/components/workflow/block-selector/all-start-blocks', () => ({ + default: ({ onSelect }: { onSelect: (type: BlockEnum) => void }) => ( + + ), +})) + +vi.mock('@/app/components/workflow/hooks', () => ({ + useAutoGenerateWebhookUrl: () => mocks.autoGenerateWebhookUrl, +})) + +vi.mock('@/app/components/workflow/hooks-store', () => ({ + useHooksStore: (selector: (state: unknown) => unknown) => selector({ + availableNodesMetaData: { + nodesMap: { + [BlockEnum.Start]: { + defaultValue: { + title: 'User Input', + desc: '', + variables: [], + }, + }, + }, + }, + }), +})) + +vi.mock('@/app/components/workflow/hooks/use-nodes-sync-draft', () => ({ + useNodesSyncDraft: () => ({ + handleSyncWorkflowDraft: mocks.handleSyncWorkflowDraft, + }), +})) + +vi.mock('@/app/components/workflow/store', () => ({ + useStore: (selector: (state: unknown) => unknown) => selector({ + setHasSelectedStartNode: mocks.setHasSelectedStartNode, + setShouldAutoOpenStartNodeSelector: mocks.setShouldAutoOpenStartNodeSelector, + }), +})) + +const createPlaceholderNode = (): Node => ({ + id: 'placeholder-1', + type: 'custom', + position: { x: 0, y: 0 }, + data: { + type: BlockEnum.StartPlaceholder, + title: 'Pick a start node', + desc: '', + selected: true, + }, +}) + +describe('StartPlaceholderPanel', () => { + beforeEach(() => { + vi.clearAllMocks() + currentNodes = [ + createPlaceholderNode(), + { + id: 'other-node', + type: 'custom', + position: { x: 100, y: 0 }, + data: { + type: BlockEnum.LLM, + title: 'LLM', + desc: '', + selected: true, + }, + }, + ] + mocks.setNodes.mockImplementation((nodes: Node[]) => { + currentNodes = nodes + }) + }) + + describe('Start node selection', () => { + it('should replace the placeholder with user input and auto-open the next node selector', () => { + render( + , + ) + + fireEvent.click(screen.getByRole('button', { name: 'Select User Input' })) + + expect(mocks.setNodes).toHaveBeenCalledTimes(1) + expect(currentNodes[0]).toMatchObject({ + id: 'placeholder-1', + data: { + type: BlockEnum.Start, + title: 'User Input', + selected: true, + variables: [], + }, + }) + expect(currentNodes[1]?.data.selected).toBe(false) + expect(mocks.setHasSelectedStartNode).toHaveBeenCalledWith(true) + expect(mocks.setShouldAutoOpenStartNodeSelector).toHaveBeenCalledWith(true) + expect(mocks.handleSyncWorkflowDraft).toHaveBeenCalledWith(true, false, expect.objectContaining({ + onSuccess: expect.any(Function), + })) + + const callback = mocks.handleSyncWorkflowDraft.mock.calls[0]?.[2] as { onSuccess: () => void } + callback.onSuccess() + + expect(mocks.autoGenerateWebhookUrl).toHaveBeenCalledWith('placeholder-1') + }) + }) +}) diff --git a/web/app/components/workflow/nodes/start-placeholder/default.ts b/web/app/components/workflow/nodes/start-placeholder/default.ts new file mode 100644 index 00000000000..9a4eb2aad90 --- /dev/null +++ b/web/app/components/workflow/nodes/start-placeholder/default.ts @@ -0,0 +1,29 @@ +import type { NodeDefault } from '../../types' +import type { StartPlaceholderNodeType } from './types' +import { BlockEnum } from '@/app/components/workflow/types' +import { genNodeMetaData } from '@/app/components/workflow/utils' + +const metaData = genNodeMetaData({ + sort: 0.05, + type: BlockEnum.StartPlaceholder, + isRequired: false, + isSingleton: true, + isTypeFixed: true, + helpLinkUri: 'user-input', +}) + +const nodeDefault: NodeDefault = { + metaData, + defaultValue: { + title: 'Workflow start', + desc: '', + }, + checkValid(_payload, t) { + return { + isValid: false, + errorMessage: t('nodes.startPlaceholder.validationRequired', { ns: 'workflow' }), + } + }, +} + +export default nodeDefault diff --git a/web/app/components/workflow/nodes/start-placeholder/node.tsx b/web/app/components/workflow/nodes/start-placeholder/node.tsx new file mode 100644 index 00000000000..498fd89cabf --- /dev/null +++ b/web/app/components/workflow/nodes/start-placeholder/node.tsx @@ -0,0 +1,25 @@ +import type { FC } from 'react' +import type { NodeProps } from '@/app/components/workflow/types' +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +const i18nPrefix = 'nodes.startPlaceholder' + +const Node: FC = ({ + data, +}) => { + const { t } = useTranslation() + const descriptionKey = data.selected ? 'nodeDescription' : 'nodeCollapsedDescription' + + return ( +
+
+
+ {t(`${i18nPrefix}.${descriptionKey}`, { ns: 'workflow' })} +
+
+
+ ) +} + +export default React.memo(Node) diff --git a/web/app/components/workflow/nodes/start-placeholder/panel.tsx b/web/app/components/workflow/nodes/start-placeholder/panel.tsx new file mode 100644 index 00000000000..86b13a568ea --- /dev/null +++ b/web/app/components/workflow/nodes/start-placeholder/panel.tsx @@ -0,0 +1,167 @@ +import type { FC } from 'react' +import type { StartPlaceholderNodeType } from './types' +import type { + PluginDefaultValue, + TriggerDefaultValue, +} from '@/app/components/workflow/block-selector/types' +import type { NodePanelProps } from '@/app/components/workflow/types' +import * as React from 'react' +import { + useCallback, + useState, +} from 'react' +import { useTranslation } from 'react-i18next' +import { useStoreApi } from 'reactflow' +import SearchBox from '@/app/components/plugins/marketplace/search-box' +import AllStartBlocks from '@/app/components/workflow/block-selector/all-start-blocks' +import { useAutoGenerateWebhookUrl } from '@/app/components/workflow/hooks' +import { useHooksStore } from '@/app/components/workflow/hooks-store' +import { useNodesSyncDraft } from '@/app/components/workflow/hooks/use-nodes-sync-draft' +import { useStore as useWorkflowStore } from '@/app/components/workflow/store' +import { BlockEnum } from '@/app/components/workflow/types' + +const getTriggerPluginNodeData = ( + triggerConfig: TriggerDefaultValue, + fallbackTitle?: string, + fallbackDesc?: string, +) => { + return { + plugin_id: triggerConfig.plugin_id, + provider_id: triggerConfig.provider_name, + provider_type: triggerConfig.provider_type, + provider_name: triggerConfig.provider_name, + event_name: triggerConfig.event_name, + event_label: triggerConfig.event_label, + event_description: triggerConfig.event_description, + title: triggerConfig.event_label || triggerConfig.title || fallbackTitle, + desc: triggerConfig.event_description || fallbackDesc, + output_schema: { ...triggerConfig.output_schema }, + parameters_schema: triggerConfig.paramSchemas ? [...triggerConfig.paramSchemas] : [], + config: { ...triggerConfig.params }, + subscription_id: triggerConfig.subscription_id, + plugin_unique_identifier: triggerConfig.plugin_unique_identifier, + is_team_authorization: triggerConfig.is_team_authorization, + meta: triggerConfig.meta ? { ...triggerConfig.meta } : undefined, + } +} + +const Panel: FC> = ({ + id, +}) => { + const { t } = useTranslation() + const [searchText, setSearchText] = useState('') + const [tags, setTags] = useState([]) + const availableNodesMetaData = useHooksStore(s => s.availableNodesMetaData) + const setHasSelectedStartNode = useWorkflowStore(s => s.setHasSelectedStartNode) + const setShouldAutoOpenStartNodeSelector = useWorkflowStore(s => s.setShouldAutoOpenStartNodeSelector) + const reactFlowStore = useStoreApi() + const autoGenerateWebhookUrl = useAutoGenerateWebhookUrl() + const { handleSyncWorkflowDraft } = useNodesSyncDraft() + + const handleSelectStartNode = useCallback((nodeType: BlockEnum, toolConfig?: PluginDefaultValue) => { + const nodeDefault = availableNodesMetaData?.nodesMap?.[nodeType] + if (!nodeDefault?.defaultValue) + return + + const baseNodeData = { ...nodeDefault.defaultValue } + const mergedNodeData = (() => { + if (nodeType !== BlockEnum.TriggerPlugin || !toolConfig) { + return { + ...baseNodeData, + ...toolConfig, + } + } + + const triggerNodeData = getTriggerPluginNodeData( + toolConfig as TriggerDefaultValue, + baseNodeData.title, + baseNodeData.desc, + ) + + return { + ...baseNodeData, + ...triggerNodeData, + config: { + ...(baseNodeData as { config?: Record }).config, + ...triggerNodeData.config, + }, + } + })() + + const { getNodes, setNodes } = reactFlowStore.getState() + const nextNodes = getNodes().map((node) => { + if (node.id !== id) { + return { + ...node, + data: { + ...node.data, + selected: false, + }, + } + } + + return { + ...node, + data: { + ...mergedNodeData, + type: nodeType, + selected: true, + }, + } + }) + + setNodes(nextNodes) + setHasSelectedStartNode?.(true) + setShouldAutoOpenStartNodeSelector?.(true) + + handleSyncWorkflowDraft(true, false, { + onSuccess: () => { + autoGenerateWebhookUrl(id) + }, + onError: () => { + console.error('Failed to save start node selection to draft') + }, + }) + }, [ + autoGenerateWebhookUrl, + availableNodesMetaData?.nodesMap, + handleSyncWorkflowDraft, + id, + reactFlowStore, + setHasSelectedStartNode, + setShouldAutoOpenStartNodeSelector, + ]) + + return ( +
+
+ +
+
+ +
+
+ ) +} + +export default React.memo(Panel) diff --git a/web/app/components/workflow/nodes/start-placeholder/types.ts b/web/app/components/workflow/nodes/start-placeholder/types.ts new file mode 100644 index 00000000000..30f354f5b08 --- /dev/null +++ b/web/app/components/workflow/nodes/start-placeholder/types.ts @@ -0,0 +1,3 @@ +import type { CommonNodeType } from '@/app/components/workflow/types' + +export type StartPlaceholderNodeType = CommonNodeType diff --git a/web/app/components/workflow/operator/__tests__/add-block.spec.tsx b/web/app/components/workflow/operator/__tests__/add-block.spec.tsx index 5086b148702..84bbe149e60 100644 --- a/web/app/components/workflow/operator/__tests__/add-block.spec.tsx +++ b/web/app/components/workflow/operator/__tests__/add-block.spec.tsx @@ -3,6 +3,7 @@ import { act, screen, waitFor } from '@testing-library/react' import { FlowType } from '@/types/common' import { createNode } from '../../__tests__/fixtures' import { renderWorkflowFlowComponent } from '../../__tests__/workflow-test-env' +import { TabsEnum } from '../../block-selector/types' import { BlockEnum } from '../../types' import AddBlock from '../add-block' @@ -20,6 +21,7 @@ type BlockSelectorMockProps = { popupClassName: string availableBlocksTypes: BlockEnum[] showStartTab: boolean + defaultActiveTab?: TabsEnum } const { @@ -127,6 +129,7 @@ describe('AddBlock', () => { disabled: false, availableBlocksTypes: mockAvailableNextBlocks, showStartTab: true, + defaultActiveTab: TabsEnum.Start, placement: 'right-start', popupClassName: 'min-w-[256px]!', }) @@ -151,6 +154,20 @@ describe('AddBlock', () => { expect(latestBlockSelectorProps?.showStartTab).toBe(false) }) + + it.each([ + BlockEnum.Start, + BlockEnum.TriggerWebhook, + ])('should keep the normal default tab when a %s node already exists', async (type) => { + renderWithReactFlow([ + createNode({ id: 'entry-node', position: { x: 0, y: 0 }, data: { type } }), + ]) + + await waitFor(() => expect(latestBlockSelectorProps).not.toBeNull()) + + expect(latestBlockSelectorProps?.showStartTab).toBe(true) + expect(latestBlockSelectorProps?.defaultActiveTab).toBeUndefined() + }) }) // User interactions that bridge selector state and workflow state. diff --git a/web/app/components/workflow/operator/add-block.tsx b/web/app/components/workflow/operator/add-block.tsx index 03e2dc9c468..6a20c10c805 100644 --- a/web/app/components/workflow/operator/add-block.tsx +++ b/web/app/components/workflow/operator/add-block.tsx @@ -10,12 +10,17 @@ import { useState, } from 'react' import { useTranslation } from 'react-i18next' -import { useStoreApi } from 'reactflow' +import { + useNodes, + useStoreApi, +} from 'reactflow' import BlockSelector from '@/app/components/workflow/block-selector' import { BlockEnum, + isTriggerNode, } from '@/app/components/workflow/types' import { FlowType } from '@/types/common' +import { TabsEnum } from '../block-selector/types' import { useAvailableBlocks, useIsChatMode, @@ -50,10 +55,18 @@ const AddBlock = ({ const { nodesReadOnly } = useNodesReadOnly() const { handlePaneContextmenuCancel } = usePanelInteractions() const [open, setOpen] = useState(false) + const nodes = useNodes() const { availableNextBlocks } = useAvailableBlocks(BlockEnum.Start, false) const { nodesMap: nodesMetaDataMap } = useNodesMetaData() const flowType = useHooksStore(s => s.configsMap?.flowType) const showStartTab = flowType !== FlowType.ragPipeline && !isChatMode + const hasEntryNode = nodes.some((node) => { + const nodeData = node.data as { type?: BlockEnum } + const nodeType = nodeData.type + return nodeType === BlockEnum.Start || (nodeType ? isTriggerNode(nodeType) : false) + }) + + const defaultActiveTab = showStartTab && !hasEntryNode ? TabsEnum.Start : undefined const handleOpenChange = useCallback((open: boolean) => { setOpen(open) @@ -121,6 +134,7 @@ const AddBlock = ({ popupClassName="min-w-[256px]!" availableBlocksTypes={availableNextBlocks} showStartTab={showStartTab} + defaultActiveTab={defaultActiveTab} /> ) } diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index 77457b379f9..8835d3e45b2 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -27,6 +27,7 @@ import type { export enum BlockEnum { Start = 'start', + StartPlaceholder = 'start-placeholder', End = 'end', Answer = 'answer', LLM = 'llm', diff --git a/web/i18n/ar-TN/workflow.json b/web/i18n/ar-TN/workflow.json index fcfeba0ccd8..6c876e51149 100644 --- a/web/i18n/ar-TN/workflow.json +++ b/web/i18n/ar-TN/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "حلقة", "blocks.loop-end": "خروج من الحلقة", "blocks.loop-start": "بداية الحلقة", + "blocks.mostCommon": "الأكثر شيوعًا", "blocks.originalStartNode": "عقدة البداية الأصلية", "blocks.parameter-extractor": "مستخرج المعلمات", "blocks.question-classifier": "مصنف الأسئلة", "blocks.start": "إدخال المستخدم", + "blocks.start-placeholder": "بداية workflow", "blocks.template-transform": "قالب", "blocks.tool": "أداة", "blocks.trigger-plugin": "مشغل الإضافة", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "استخدم LLM لاستخراج المعلمات الهيكلية من اللغة الطبيعية لاستدعاء الأدوات أو طلبات HTTP.", "blocksAbout.question-classifier": "تحديد شروط تصنيف أسئلة المستخدم، يمكن لـ LLM تحديد كيفية تقدم المحادثة بناءً على وصف التصنيف", "blocksAbout.start": "تحديد المعلمات الأولية لبدء سير العمل", + "blocksAbout.start-placeholder": "اختر كيف يبدأ هذا workflow", "blocksAbout.template-transform": "تحويل البيانات إلى سلسلة باستخدام بنية قالب Jinja", "blocksAbout.tool": "استخدم الأدوات الخارجية لتوسيع قدرات سير العمل", "blocksAbout.trigger-plugin": "مشغل تكامل تابع لجهة خارجية يبدأ سير العمل من أحداث النظام الأساسي الخارجي", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "نوع الرسالة", "nodes.start.outputVars.query": "إدخال المستخدم", "nodes.start.required": "مطلوب", + "nodes.start.userInputTipDescription": "حدّد المدخلات التي سيتم جمعها من المستخدمين النهائيين عند بدء workflow عند الطلب.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "تصفّح المزيد في Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "ابحث عن المزيد من الأدوات في Marketplace", + "nodes.startPlaceholder.noTriggersFound": "لم يتم العثور على أي مشغلات", + "nodes.startPlaceholder.nodeCollapsedDescription": "انقر لتكوين عقدة البداية", + "nodes.startPlaceholder.nodeDescription": "اختر عقدة بداية من اللوحة اليمنى", + "nodes.startPlaceholder.nodeTitle": "بداية workflow", + "nodes.startPlaceholder.panelDescription": "تحدد عقدة البداية ما الذي يشغّل workflow لديك", + "nodes.startPlaceholder.panelTitle": "اختر عقدة بداية", + "nodes.startPlaceholder.userInputConflictTip": "لا يمكن دمج إدخال المستخدم مع مشغلات أخرى", + "nodes.startPlaceholder.validationRequired": "اختر عقدة بداية أولًا.", "nodes.templateTransform.code": "الكود", "nodes.templateTransform.codeSupportTip": "يدعم Jinja2 فقط", "nodes.templateTransform.inputVars": "متغيرات الإدخال", @@ -1209,9 +1223,11 @@ "tabs.sources": "المصادر", "tabs.start": "البداية", "tabs.startDisabledTip": "تتعارض عقدة المشغل وعقدة إدخال المستخدم.", + "tabs.startDisabledTipLearnMore": "تعرّف على المزيد حول عقد البداية", "tabs.startNotSupportedTip": "علامة التبويب \"ابدأ\" غير مدعومة في المقتطفات.", "tabs.tools": "الأدوات", "tabs.transform": "تحويل", + "tabs.unconfiguredStartDisabledTip": "تمت إضافة عقدة بداية غير مكوّنة إلى اللوحة. أكمل الإعداد قبل المتابعة.", "tabs.usePlugin": "حدد الأداة", "tabs.utilities": "الأدوات المساعدة", "tabs.workflowTool": "سير العمل", diff --git a/web/i18n/de-DE/workflow.json b/web/i18n/de-DE/workflow.json index 7d3bdf3fd22..4cf68da10e3 100644 --- a/web/i18n/de-DE/workflow.json +++ b/web/i18n/de-DE/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Schleife", "blocks.loop-end": "Schleife beenden", "blocks.loop-start": "Schleifenbeginn", + "blocks.mostCommon": "Am häufigsten", "blocks.originalStartNode": "ursprünglicher Startknoten", "blocks.parameter-extractor": "Parameter-Extraktor", "blocks.question-classifier": "Fragenklassifizierer", "blocks.start": "Start", + "blocks.start-placeholder": "Workflow-Start", "blocks.template-transform": "Vorlage", "blocks.tool": "Werkzeug", "blocks.trigger-plugin": "Plugin-Auslöser", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Verwenden Sie LLM, um strukturierte Parameter aus natürlicher Sprache für Werkzeugaufrufe oder HTTP-Anfragen zu extrahieren.", "blocksAbout.question-classifier": "Definieren Sie die Klassifizierungsbedingungen von Benutzerfragen, LLM kann basierend auf der Klassifikationsbeschreibung festlegen, wie die Konversation fortschreitet", "blocksAbout.start": "Definieren Sie die Anfangsparameter zum Starten eines Workflows", + "blocksAbout.start-placeholder": "Wählen Sie aus, wie dieser Workflow startet", "blocksAbout.template-transform": "Daten in Zeichenfolgen mit Jinja-Vorlagensyntax umwandeln", "blocksAbout.tool": "Verwenden Sie externe Tools, um die Workflow-Funktionen zu erweitern", "blocksAbout.trigger-plugin": "Auslöser für die Integration von Drittanbietern, der Workflows anhand von Ereignissen externer Plattformen startet", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "Nachrichtentyp", "nodes.start.outputVars.query": "Benutzereingabe", "nodes.start.required": "erforderlich", + "nodes.start.userInputTipDescription": "Definieren Sie Eingaben, die von Endbenutzern erfasst werden, wenn Ihr Workflow bei Bedarf gestartet wird.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Mehr im Marketplace durchsuchen", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Weitere Tools im Marketplace finden", + "nodes.startPlaceholder.noTriggersFound": "Keine Trigger gefunden", + "nodes.startPlaceholder.nodeCollapsedDescription": "Klicken, um den Startknoten zu konfigurieren", + "nodes.startPlaceholder.nodeDescription": "Wählen Sie im rechten Bereich einen Startknoten aus", + "nodes.startPlaceholder.nodeTitle": "Workflow-Start", + "nodes.startPlaceholder.panelDescription": "Der Startknoten legt fest, wodurch Ihr Workflow ausgeführt wird", + "nodes.startPlaceholder.panelTitle": "Startknoten auswählen", + "nodes.startPlaceholder.userInputConflictTip": "Benutzereingabe kann nicht mit anderen Triggern kombiniert werden", + "nodes.startPlaceholder.validationRequired": "Wählen Sie zuerst einen Startknoten aus.", "nodes.templateTransform.code": "Code", "nodes.templateTransform.codeSupportTip": "Unterstützt nur Jinja2", "nodes.templateTransform.inputVars": "Eingabevariablen", @@ -1209,9 +1223,11 @@ "tabs.sources": "Quellen", "tabs.start": "Start", "tabs.startDisabledTip": "Trigger-Knoten und Benutzereingabeknoten schließen sich gegenseitig aus.", + "tabs.startDisabledTipLearnMore": "Mehr über Startknoten erfahren", "tabs.startNotSupportedTip": "Die Registerkarte „Start“ wird in Snippets nicht unterstützt.", "tabs.tools": "Werkzeuge", "tabs.transform": "Transformieren", + "tabs.unconfiguredStartDisabledTip": "Ein nicht konfigurierter Startknoten wurde zur Arbeitsfläche hinzugefügt. Schließen Sie die Einrichtung ab, bevor Sie fortfahren.", "tabs.usePlugin": "Werkzeug auswählen", "tabs.utilities": "Dienstprogramme", "tabs.workflowTool": "Arbeitsablauf", diff --git a/web/i18n/en-US/workflow.json b/web/i18n/en-US/workflow.json index 52b79654fdc..de2f0d0b805 100644 --- a/web/i18n/en-US/workflow.json +++ b/web/i18n/en-US/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Loop", "blocks.loop-end": "Exit Loop", "blocks.loop-start": "Loop Start", + "blocks.mostCommon": "Most common", "blocks.originalStartNode": "original start node", "blocks.parameter-extractor": "Parameter Extractor", "blocks.question-classifier": "Question Classifier", "blocks.start": "User Input", + "blocks.start-placeholder": "Workflow start", "blocks.template-transform": "Template", "blocks.tool": "Tool", "blocks.trigger-plugin": "Plugin Trigger", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Use LLM to extract structured parameters from natural language for tool invocations or HTTP requests.", "blocksAbout.question-classifier": "Define the classification conditions of user questions, LLM can define how the conversation progresses based on the classification description", "blocksAbout.start": "Define the initial parameters for launching a workflow", + "blocksAbout.start-placeholder": "Choose how this workflow starts", "blocksAbout.template-transform": "Convert data to string using Jinja template syntax", "blocksAbout.tool": "Use external tools to extend workflow capabilities", "blocksAbout.trigger-plugin": "Third-party integration trigger that starts workflows from external platform events", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "message type", "nodes.start.outputVars.query": "User input", "nodes.start.required": "required", + "nodes.start.userInputTipDescription": "Define inputs to collect from end users when your workflow starts on demand.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Browse more in Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Find more tools in Marketplace", + "nodes.startPlaceholder.noTriggersFound": "No triggers were found", + "nodes.startPlaceholder.nodeCollapsedDescription": "Click to configure the start node", + "nodes.startPlaceholder.nodeDescription": "Pick a start node from the right panel", + "nodes.startPlaceholder.nodeTitle": "Workflow start", + "nodes.startPlaceholder.panelDescription": "The start node defines what triggers your workflow to run", + "nodes.startPlaceholder.panelTitle": "Pick a start node", + "nodes.startPlaceholder.userInputConflictTip": "User Input cannot be combined with other triggers", + "nodes.startPlaceholder.validationRequired": "Choose a start node first.", "nodes.templateTransform.code": "Code", "nodes.templateTransform.codeSupportTip": "Only supports Jinja2", "nodes.templateTransform.inputVars": "Input Variables", @@ -1209,9 +1223,11 @@ "tabs.sources": "Sources", "tabs.start": "Start", "tabs.startDisabledTip": "Trigger node and user input node are mutually exclusive.", + "tabs.startDisabledTipLearnMore": "Learn more about start nodes", "tabs.startNotSupportedTip": "The Start tab is not supported in snippets.", "tabs.tools": "Tools", "tabs.transform": "Transform", + "tabs.unconfiguredStartDisabledTip": "An unconfigured start node has been added to canvas. Please complete the setup before continuing.", "tabs.usePlugin": "Select tool", "tabs.utilities": "Utilities", "tabs.workflowTool": "Workflow", diff --git a/web/i18n/es-ES/workflow.json b/web/i18n/es-ES/workflow.json index d205d96e617..c929051f612 100644 --- a/web/i18n/es-ES/workflow.json +++ b/web/i18n/es-ES/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Bucle", "blocks.loop-end": "Salir del bucle", "blocks.loop-start": "Inicio del bucle", + "blocks.mostCommon": "Más común", "blocks.originalStartNode": "nodo inicial original", "blocks.parameter-extractor": "Extractor de parámetros", "blocks.question-classifier": "Clasificador de preguntas", "blocks.start": "Inicio", + "blocks.start-placeholder": "Inicio del workflow", "blocks.template-transform": "Plantilla", "blocks.tool": "Herramienta", "blocks.trigger-plugin": "Disparador de complemento", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Utiliza LLM para extraer parámetros estructurados del lenguaje natural para invocaciones de herramientas o solicitudes HTTP.", "blocksAbout.question-classifier": "Define las condiciones de clasificación de las preguntas de los usuarios, LLM puede definir cómo progresa la conversación en función de la descripción de clasificación", "blocksAbout.start": "Define los parámetros iniciales para iniciar un flujo de trabajo", + "blocksAbout.start-placeholder": "Elige cómo empieza este workflow", "blocksAbout.template-transform": "Convierte datos en una cadena utilizando la sintaxis de plantillas Jinja", "blocksAbout.tool": "Utiliza herramientas externas para ampliar las capacidades del flujo de trabajo", "blocksAbout.trigger-plugin": "Disparador de integración de terceros que inicia flujos de trabajo a partir de eventos de plataformas externas", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "tipo de mensaje", "nodes.start.outputVars.query": "Entrada del usuario", "nodes.start.required": "requerido", + "nodes.start.userInputTipDescription": "Define las entradas que se recopilarán de los usuarios finales cuando tu workflow se inicie bajo demanda.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Explorar más en Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Encontrar más herramientas en Marketplace", + "nodes.startPlaceholder.noTriggersFound": "No se encontraron disparadores", + "nodes.startPlaceholder.nodeCollapsedDescription": "Haz clic para configurar el nodo de inicio", + "nodes.startPlaceholder.nodeDescription": "Elige un nodo de inicio en el panel derecho", + "nodes.startPlaceholder.nodeTitle": "Inicio del workflow", + "nodes.startPlaceholder.panelDescription": "El nodo de inicio define qué activa la ejecución de tu workflow", + "nodes.startPlaceholder.panelTitle": "Elige un nodo de inicio", + "nodes.startPlaceholder.userInputConflictTip": "La entrada de usuario no se puede combinar con otros disparadores", + "nodes.startPlaceholder.validationRequired": "Elige primero un nodo de inicio.", "nodes.templateTransform.code": "Código", "nodes.templateTransform.codeSupportTip": "Solo admite Jinja2", "nodes.templateTransform.inputVars": "Variables de entrada", @@ -1209,9 +1223,11 @@ "tabs.sources": "Fuentes", "tabs.start": "Iniciar", "tabs.startDisabledTip": "El nodo activador y el nodo de entrada del usuario son mutuamente excluyentes.", + "tabs.startDisabledTipLearnMore": "Más información sobre los nodos de inicio", "tabs.startNotSupportedTip": "La pestaña Inicio no se admite en fragmentos.", "tabs.tools": "Herramientas", "tabs.transform": "Transformar", + "tabs.unconfiguredStartDisabledTip": "Se ha añadido al lienzo un nodo de inicio sin configurar. Completa la configuración antes de continuar.", "tabs.usePlugin": "Seleccionar herramienta", "tabs.utilities": "Utilidades", "tabs.workflowTool": "Flujo de trabajo", diff --git a/web/i18n/fa-IR/workflow.json b/web/i18n/fa-IR/workflow.json index 9fbe2d7b3cc..5e9767e1cf0 100644 --- a/web/i18n/fa-IR/workflow.json +++ b/web/i18n/fa-IR/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "حلقه", "blocks.loop-end": "خروج از حلقه", "blocks.loop-start": "شروع حلقه", + "blocks.mostCommon": "رایج‌ترین", "blocks.originalStartNode": "گره شروع اصلی", "blocks.parameter-extractor": "استخراج‌کننده پارامتر", "blocks.question-classifier": "دسته‌بندی‌کننده سؤال", "blocks.start": "شروع", + "blocks.start-placeholder": "شروع workflow", "blocks.template-transform": "مبدل الگو", "blocks.tool": "ابزار", "blocks.trigger-plugin": "راه‌انداز پلاگین", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "استخراج پارامترهای ساختاریافته از زبان طبیعی توسط مدل زبانی بزرگ برای فراخوانی ابزارها یا درخواست‌های HTTP", "blocksAbout.question-classifier": "تعریف شرایط دسته‌بندی سؤالات کاربر؛ مدل زبانی بزرگ بر اساس توضیحات دسته‌بندی، مسیر مکالمه را تعیین می‌کند", "blocksAbout.start": "تعریف پارامترهای اولیه برای آغاز گردش کار", + "blocksAbout.start-placeholder": "نحوه شروع این workflow را انتخاب کنید", "blocksAbout.template-transform": "تبدیل داده‌ها به رشته با نحو الگوی Jinja", "blocksAbout.tool": "استفاده از ابزارهای خارجی برای گسترش قابلیت‌های گردش کار", "blocksAbout.trigger-plugin": "یکپارچه‌سازی با سرویس‌های ثالث برای آغاز گردش کار از رویدادهای پلتفرم خارجی", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "نوع پیام", "nodes.start.outputVars.query": "ورودی کاربر", "nodes.start.required": "الزامی", + "nodes.start.userInputTipDescription": "ورودی‌هایی را تعریف کنید که هنگام شروع workflow به‌صورت درخواستی از کاربران نهایی جمع‌آوری می‌شوند.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "موارد بیشتری را در Marketplace مرور کنید", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "ابزارهای بیشتری را در Marketplace پیدا کنید", + "nodes.startPlaceholder.noTriggersFound": "هیچ تریگری یافت نشد", + "nodes.startPlaceholder.nodeCollapsedDescription": "برای پیکربندی گره شروع کلیک کنید", + "nodes.startPlaceholder.nodeDescription": "یک گره شروع را از پنل سمت راست انتخاب کنید", + "nodes.startPlaceholder.nodeTitle": "شروع workflow", + "nodes.startPlaceholder.panelDescription": "گره شروع مشخص می‌کند چه چیزی اجرای workflow شما را فعال می‌کند", + "nodes.startPlaceholder.panelTitle": "یک گره شروع انتخاب کنید", + "nodes.startPlaceholder.userInputConflictTip": "ورودی کاربر نمی‌تواند با تریگرهای دیگر ترکیب شود", + "nodes.startPlaceholder.validationRequired": "ابتدا یک گره شروع انتخاب کنید.", "nodes.templateTransform.code": "کد", "nodes.templateTransform.codeSupportTip": "فقط از Jinja2 پشتیبانی می‌شود", "nodes.templateTransform.inputVars": "متغیرهای ورودی", @@ -1209,9 +1223,11 @@ "tabs.sources": "منابع", "tabs.start": "شروع", "tabs.startDisabledTip": "گره تریگر و گره ورودی کاربر نمی‌توانند همزمان فعال باشند.", + "tabs.startDisabledTipLearnMore": "درباره گره‌های شروع بیشتر بدانید", "tabs.startNotSupportedTip": "تب Start در قطعه‌ها پشتیبانی نمی‌شود.", "tabs.tools": "ابزارها", "tabs.transform": "تبدیل", + "tabs.unconfiguredStartDisabledTip": "یک گره شروع پیکربندی‌نشده به بوم اضافه شده است. پیش از ادامه، تنظیمات را کامل کنید.", "tabs.usePlugin": "انتخاب ابزار", "tabs.utilities": "ابزارهای کاربردی", "tabs.workflowTool": "گردش کار", diff --git a/web/i18n/fr-FR/workflow.json b/web/i18n/fr-FR/workflow.json index 8cb68762575..fa7d3a9302a 100644 --- a/web/i18n/fr-FR/workflow.json +++ b/web/i18n/fr-FR/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Boucle", "blocks.loop-end": "Sortir de la boucle", "blocks.loop-start": "Début de boucle", + "blocks.mostCommon": "Le plus courant", "blocks.originalStartNode": "nœud de départ original", "blocks.parameter-extractor": "Extracteur de paramètres", "blocks.question-classifier": "Classificateur de questions", "blocks.start": "Début", + "blocks.start-placeholder": "Début du workflow", "blocks.template-transform": "Modèle", "blocks.tool": "Outil", "blocks.trigger-plugin": "Déclencheur de plugin", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Utiliser LLM pour extraire des paramètres structurés du langage naturel pour les invocations d'outils ou les requêtes HTTP.", "blocksAbout.question-classifier": "Définir les conditions de classification des questions des utilisateurs, LLM peut définir comment la conversation progresse en fonction de la description de la classification", "blocksAbout.start": "Définir les paramètres initiaux pour lancer un flux de travail", + "blocksAbout.start-placeholder": "Choisissez comment ce workflow démarre", "blocksAbout.template-transform": "Convertir les données en chaîne en utilisant la syntaxe du template Jinja", "blocksAbout.tool": "Utilisez des outils externes pour étendre les capacités du flux de travail", "blocksAbout.trigger-plugin": "Déclencheur d’intégration tierce qui démarre des flux de travail à partir d’événements d’une plateforme externe", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "type de message", "nodes.start.outputVars.query": "Saisie utilisateur", "nodes.start.required": "requis", + "nodes.start.userInputTipDescription": "Définissez les entrées à collecter auprès des utilisateurs finaux lorsque votre workflow démarre à la demande.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Parcourir plus sur le Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Trouver plus d’outils sur le Marketplace", + "nodes.startPlaceholder.noTriggersFound": "Aucun déclencheur trouvé", + "nodes.startPlaceholder.nodeCollapsedDescription": "Cliquez pour configurer le nœud de départ", + "nodes.startPlaceholder.nodeDescription": "Choisissez un nœud de départ dans le panneau de droite", + "nodes.startPlaceholder.nodeTitle": "Début du workflow", + "nodes.startPlaceholder.panelDescription": "Le nœud de départ définit ce qui déclenche l’exécution de votre workflow", + "nodes.startPlaceholder.panelTitle": "Choisissez un nœud de départ", + "nodes.startPlaceholder.userInputConflictTip": "L’entrée utilisateur ne peut pas être combinée avec d’autres déclencheurs", + "nodes.startPlaceholder.validationRequired": "Choisissez d’abord un nœud de départ.", "nodes.templateTransform.code": "Code", "nodes.templateTransform.codeSupportTip": "Prend en charge uniquement Jinja2", "nodes.templateTransform.inputVars": "Variables de saisie", @@ -1209,9 +1223,11 @@ "tabs.sources": "Sources", "tabs.start": "Démarrer", "tabs.startDisabledTip": "Le nœud de déclenchement et le nœud d'entrée utilisateur sont mutuellement exclusifs.", + "tabs.startDisabledTipLearnMore": "En savoir plus sur les nœuds de départ", "tabs.startNotSupportedTip": "L'onglet Démarrer n'est pas pris en charge dans les extraits de code.", "tabs.tools": "Outils", "tabs.transform": "Transformer", + "tabs.unconfiguredStartDisabledTip": "Un nœud de départ non configuré a été ajouté au canevas. Terminez la configuration avant de continuer.", "tabs.usePlugin": "Sélectionner l'outil", "tabs.utilities": "Utilitaires", "tabs.workflowTool": "Flux de travail", diff --git a/web/i18n/hi-IN/workflow.json b/web/i18n/hi-IN/workflow.json index 09565b9a974..d08b569b6ae 100644 --- a/web/i18n/hi-IN/workflow.json +++ b/web/i18n/hi-IN/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "लूप", "blocks.loop-end": "लूप से बाहर निकलें", "blocks.loop-start": "लूप प्रारंभ", + "blocks.mostCommon": "सबसे आम", "blocks.originalStartNode": "मूल प्रारंभ नोड", "blocks.parameter-extractor": "पैरामीटर निष्कर्षक", "blocks.question-classifier": "प्रश्न वर्गीकरण", "blocks.start": "प्रारंभ", + "blocks.start-placeholder": "workflow प्रारंभ", "blocks.template-transform": "टेम्पलेट", "blocks.tool": "उपकरण", "blocks.trigger-plugin": "प्लगइन ट्रिगर", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "टूल आमंत्रणों या HTTP अनुरोधों के लिए प्राकृतिक भाषा से संरचित पैरामीटर निकालने के लिए LLM का उपयोग करें।", "blocksAbout.question-classifier": "उपयोगकर्ता प्रश्नों की वर्गीकरण शर्तों को परिभाषित करें, LLM वर्गीकरण विवरण के आधार पर संवाद कैसे आगे बढ़ता है, इसे परिभाषित कर सकता है", "blocksAbout.start": "वर्कफ़्लो लॉन्च करने के लिए प्रारंभिक पैरामीटर को परिभाषित करें", + "blocksAbout.start-placeholder": "चुनें कि यह workflow कैसे शुरू होता है", "blocksAbout.template-transform": "Jinja टेम्पलेट सिंटैक्स का उपयोग करके डेटा को स्ट्रिंग में परिवर्तित करें", "blocksAbout.tool": "कार्यप्रवाह क्षमताओं को बढ़ाने के लिए बाहरी उपकरणों का उपयोग करें", "blocksAbout.trigger-plugin": "थर्ड-पार्टी इंटीग्रेशन ट्रिगर जो बाहरी प्लेटफ़ॉर्म घटनाओं से वर्कफ़्लो शुरू करता है", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "संदेश प्रकार", "nodes.start.outputVars.query": "यूजर इनपुट", "nodes.start.required": "आवश्यक", + "nodes.start.userInputTipDescription": "जब आपका workflow मांग पर शुरू होता है, तब अंतिम उपयोगकर्ताओं से एकत्र किए जाने वाले इनपुट परिभाषित करें।", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Marketplace में और ब्राउज़ करें", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Marketplace में और टूल खोजें", + "nodes.startPlaceholder.noTriggersFound": "कोई ट्रिगर नहीं मिला", + "nodes.startPlaceholder.nodeCollapsedDescription": "प्रारंभ नोड कॉन्फ़िगर करने के लिए क्लिक करें", + "nodes.startPlaceholder.nodeDescription": "दाएँ पैनल से एक प्रारंभ नोड चुनें", + "nodes.startPlaceholder.nodeTitle": "workflow प्रारंभ", + "nodes.startPlaceholder.panelDescription": "प्रारंभ नोड यह परिभाषित करता है कि आपका workflow किससे चलेगा", + "nodes.startPlaceholder.panelTitle": "एक प्रारंभ नोड चुनें", + "nodes.startPlaceholder.userInputConflictTip": "उपयोगकर्ता इनपुट को अन्य ट्रिगर के साथ संयोजित नहीं किया जा सकता", + "nodes.startPlaceholder.validationRequired": "पहले एक प्रारंभ नोड चुनें।", "nodes.templateTransform.code": "कोड", "nodes.templateTransform.codeSupportTip": "केवल Jinja2 का समर्थन करता है", "nodes.templateTransform.inputVars": "इनपुट वेरिएबल्स", @@ -1209,9 +1223,11 @@ "tabs.sources": "स्रोत", "tabs.start": "शुरू करें", "tabs.startDisabledTip": "ट्रिगर नोड और उपयोगकर्ता इनपुट नोड परस्पर विशेष हैं।", + "tabs.startDisabledTipLearnMore": "प्रारंभ नोड्स के बारे में और जानें", "tabs.startNotSupportedTip": "स्निपेट्स में स्टार्ट टैब समर्थित नहीं है।", "tabs.tools": "टूल्स", "tabs.transform": "परिवर्तन", + "tabs.unconfiguredStartDisabledTip": "कैनवास में एक असंरचित प्रारंभ नोड जोड़ा गया है। जारी रखने से पहले सेटअप पूरा करें।", "tabs.usePlugin": "उपकरण चुनें", "tabs.utilities": "उपयोगिताएं", "tabs.workflowTool": "कार्यप्रवाह", diff --git a/web/i18n/id-ID/workflow.json b/web/i18n/id-ID/workflow.json index 99cd1968112..bf7e1fd4c8c 100644 --- a/web/i18n/id-ID/workflow.json +++ b/web/i18n/id-ID/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Perulangan", "blocks.loop-end": "Keluar Loop", "blocks.loop-start": "Mulai Loop", + "blocks.mostCommon": "Paling umum", "blocks.originalStartNode": "node awal asli", "blocks.parameter-extractor": "Ekstraktor Parameter", "blocks.question-classifier": "Pengklasifikasi Pertanyaan", "blocks.start": "Mulai", + "blocks.start-placeholder": "Awal workflow", "blocks.template-transform": "Templat", "blocks.tool": "Alat", "blocks.trigger-plugin": "Pemicu Plugin", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Gunakan LLM untuk mengekstrak parameter terstruktur dari bahasa alami untuk pemanggilan alat atau permintaan HTTP.", "blocksAbout.question-classifier": "Tentukan kondisi klasifikasi pertanyaan pengguna, LLM dapat menentukan bagaimana percakapan berlangsung berdasarkan deskripsi klasifikasi", "blocksAbout.start": "Menentukan parameter awal untuk meluncurkan alur kerja", + "blocksAbout.start-placeholder": "Pilih bagaimana workflow ini dimulai", "blocksAbout.template-transform": "Mengonversi data menjadi string menggunakan sintaks templat Jinja", "blocksAbout.tool": "Gunakan alat eksternal untuk memperluas kemampuan alur kerja", "blocksAbout.trigger-plugin": "Pemicu integrasi pihak ketiga yang memulai alur kerja dari kejadian platform eksternal", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "Jenis pesan", "nodes.start.outputVars.query": "Masukan pengguna", "nodes.start.required": "Diperlukan", + "nodes.start.userInputTipDescription": "Tentukan input yang akan dikumpulkan dari pengguna akhir saat workflow Anda dimulai sesuai permintaan.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Jelajahi lebih banyak di Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Temukan lebih banyak alat di Marketplace", + "nodes.startPlaceholder.noTriggersFound": "Tidak ada pemicu yang ditemukan", + "nodes.startPlaceholder.nodeCollapsedDescription": "Klik untuk mengonfigurasi node awal", + "nodes.startPlaceholder.nodeDescription": "Pilih node awal dari panel kanan", + "nodes.startPlaceholder.nodeTitle": "Awal workflow", + "nodes.startPlaceholder.panelDescription": "Node awal menentukan apa yang memicu workflow Anda berjalan", + "nodes.startPlaceholder.panelTitle": "Pilih node awal", + "nodes.startPlaceholder.userInputConflictTip": "Input pengguna tidak dapat digabungkan dengan pemicu lain", + "nodes.startPlaceholder.validationRequired": "Pilih node awal terlebih dahulu.", "nodes.templateTransform.code": "Kode", "nodes.templateTransform.codeSupportTip": "Hanya mendukung Jinja2", "nodes.templateTransform.inputVars": "Variabel Masukan", @@ -1209,9 +1223,11 @@ "tabs.sources": "Sumber", "tabs.start": "Mulai", "tabs.startDisabledTip": "Node pemicu dan node input pengguna saling eksklusif.", + "tabs.startDisabledTipLearnMore": "Pelajari lebih lanjut tentang node awal", "tabs.startNotSupportedTip": "Tab Mulai tidak didukung dalam cuplikan.", "tabs.tools": "Perkakas", "tabs.transform": "Mengubah", + "tabs.unconfiguredStartDisabledTip": "Node awal yang belum dikonfigurasi telah ditambahkan ke kanvas. Selesaikan penyiapan sebelum melanjutkan.", "tabs.usePlugin": "Pilih alat", "tabs.utilities": "Utilitas", "tabs.workflowTool": "Alur Kerja", diff --git a/web/i18n/it-IT/workflow.json b/web/i18n/it-IT/workflow.json index 853ceb3dfa5..f8b36fdefc3 100644 --- a/web/i18n/it-IT/workflow.json +++ b/web/i18n/it-IT/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Anello", "blocks.loop-end": "Uscire dal ciclo", "blocks.loop-start": "Inizio ciclo", + "blocks.mostCommon": "Più comune", "blocks.originalStartNode": "nodo iniziale originale", "blocks.parameter-extractor": "Estrattore Parametri", "blocks.question-classifier": "Classificatore Domande", "blocks.start": "Inizio", + "blocks.start-placeholder": "Avvio del workflow", "blocks.template-transform": "Template", "blocks.tool": "Strumento", "blocks.trigger-plugin": "Attivatore del plugin", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Usa LLM per estrarre parametri strutturati dal linguaggio naturale per invocazioni di strumenti o richieste HTTP.", "blocksAbout.question-classifier": "Definisci le condizioni di classificazione delle domande dell'utente, LLM può definire come prosegue la conversazione in base alla descrizione della classificazione", "blocksAbout.start": "Definisci i parametri iniziali per l'avvio di un flusso di lavoro", + "blocksAbout.start-placeholder": "Scegli come inizia questo workflow", "blocksAbout.template-transform": "Converti i dati in stringa usando la sintassi del template Jinja", "blocksAbout.tool": "Usa strumenti esterni per estendere le capacità del flusso di lavoro", "blocksAbout.trigger-plugin": "Trigger di integrazione di terze parti che avvia flussi di lavoro da eventi di piattaforme esterne", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "tipo di messaggio", "nodes.start.outputVars.query": "Input Utente", "nodes.start.required": "richiesto", + "nodes.start.userInputTipDescription": "Definisci gli input da raccogliere dagli utenti finali quando il workflow viene avviato su richiesta.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Sfoglia altro nel Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Trova altri strumenti nel Marketplace", + "nodes.startPlaceholder.noTriggersFound": "Nessun trigger trovato", + "nodes.startPlaceholder.nodeCollapsedDescription": "Fai clic per configurare il nodo iniziale", + "nodes.startPlaceholder.nodeDescription": "Scegli un nodo iniziale dal pannello a destra", + "nodes.startPlaceholder.nodeTitle": "Avvio del workflow", + "nodes.startPlaceholder.panelDescription": "Il nodo iniziale definisce cosa attiva l’esecuzione del workflow", + "nodes.startPlaceholder.panelTitle": "Scegli un nodo iniziale", + "nodes.startPlaceholder.userInputConflictTip": "L’input utente non può essere combinato con altri trigger", + "nodes.startPlaceholder.validationRequired": "Scegli prima un nodo iniziale.", "nodes.templateTransform.code": "Codice", "nodes.templateTransform.codeSupportTip": "Supporta solo Jinja2", "nodes.templateTransform.inputVars": "Variabili di Input", @@ -1209,9 +1223,11 @@ "tabs.sources": "Fonti", "tabs.start": "Inizia", "tabs.startDisabledTip": "Il nodo di attivazione e il nodo di input utente sono mutualmente esclusivi.", + "tabs.startDisabledTipLearnMore": "Scopri di più sui nodi iniziali", "tabs.startNotSupportedTip": "La scheda Start non è supportata negli snippet.", "tabs.tools": "Strumenti", "tabs.transform": "Trasforma", + "tabs.unconfiguredStartDisabledTip": "Un nodo iniziale non configurato è stato aggiunto alla tela. Completa la configurazione prima di continuare.", "tabs.usePlugin": "Strumento di selezione", "tabs.utilities": "Utility", "tabs.workflowTool": "Flusso di lavoro", diff --git a/web/i18n/ja-JP/workflow.json b/web/i18n/ja-JP/workflow.json index 938d1b1567e..ae7b5f1dc31 100644 --- a/web/i18n/ja-JP/workflow.json +++ b/web/i18n/ja-JP/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "ループ", "blocks.loop-end": "ループ完了", "blocks.loop-start": "ループ開始", + "blocks.mostCommon": "最も一般的", "blocks.originalStartNode": "元の開始ノード", "blocks.parameter-extractor": "パラメータ抽出", "blocks.question-classifier": "質問分類器", "blocks.start": "ユーザー入力", + "blocks.start-placeholder": "ワークフロー開始", "blocks.template-transform": "テンプレート", "blocks.tool": "ツール", "blocks.trigger-plugin": "プラグイントリガー", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "自然言語から構造化パラメータを抽出し、後続処理で利用します。", "blocksAbout.question-classifier": "質問の分類条件を定義し、LLM が分類に基づいて対話フローを制御します。", "blocksAbout.start": "ワークフロー開始時の初期パラメータを定義します。", + "blocksAbout.start-placeholder": "このワークフローの開始方法を選択します", "blocksAbout.template-transform": "Jinja テンプレート構文でデータを文字列に変換します。", "blocksAbout.tool": "外部ツールを使用してワークフローの機能を拡張する", "blocksAbout.trigger-plugin": "サードパーティ統合トリガー、外部プラットフォームのイベントによってワークフローを開始します", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "メッセージ種別", "nodes.start.outputVars.query": "ユーザー入力", "nodes.start.required": "必須", + "nodes.start.userInputTipDescription": "ワークフローがオンデマンドで開始されるときにエンドユーザーから収集する入力を定義します。", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Marketplace でもっと探す", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Marketplace でさらにツールを探す", + "nodes.startPlaceholder.noTriggersFound": "トリガーが見つかりませんでした", + "nodes.startPlaceholder.nodeCollapsedDescription": "クリックして開始ノードを設定", + "nodes.startPlaceholder.nodeDescription": "右側のパネルから開始ノードを選択", + "nodes.startPlaceholder.nodeTitle": "ワークフロー開始", + "nodes.startPlaceholder.panelDescription": "開始ノードはワークフローを実行するトリガーを定義します", + "nodes.startPlaceholder.panelTitle": "開始ノードを選択", + "nodes.startPlaceholder.userInputConflictTip": "ユーザー入力は他のトリガーと組み合わせることはできません", + "nodes.startPlaceholder.validationRequired": "最初に開始ノードを選択してください。", "nodes.templateTransform.code": "コード", "nodes.templateTransform.codeSupportTip": "Jinja2 のみをサポートしています", "nodes.templateTransform.inputVars": "入力変数", @@ -1209,9 +1223,11 @@ "tabs.sources": "ソース", "tabs.start": "始める", "tabs.startDisabledTip": "トリガーノードとユーザー入力ノードは互いに排他です。", + "tabs.startDisabledTipLearnMore": "開始ノードの詳細を見る", "tabs.startNotSupportedTip": "[スタート] タブはスニペットではサポートされていません。", "tabs.tools": "ツール", "tabs.transform": "変換", + "tabs.unconfiguredStartDisabledTip": "未設定の開始ノードがキャンバスに追加されています。続行する前に設定を完了してください。", "tabs.usePlugin": "ツールを選択", "tabs.utilities": "ツール", "tabs.workflowTool": "ワークフロー", diff --git a/web/i18n/ko-KR/workflow.json b/web/i18n/ko-KR/workflow.json index 44373a0ee4c..d2547712244 100644 --- a/web/i18n/ko-KR/workflow.json +++ b/web/i18n/ko-KR/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "루프", "blocks.loop-end": "루프 종료", "blocks.loop-start": "루프 시작", + "blocks.mostCommon": "가장 일반적", "blocks.originalStartNode": "원래 시작 노드", "blocks.parameter-extractor": "매개변수 추출기", "blocks.question-classifier": "질문 분류기", "blocks.start": "시작", + "blocks.start-placeholder": "워크플로 시작", "blocks.template-transform": "템플릿", "blocks.tool": "도구", "blocks.trigger-plugin": "플러그인 트리거", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "도구 호출 또는 HTTP 요청을 위해 자연어에서 구조화된 매개변수를 추출하기 위해 LLM 을 사용합니다.", "blocksAbout.question-classifier": "사용자 질문의 분류 조건을 정의합니다. LLM 은 분류 설명을 기반으로 대화의 진행 방식을 정의할 수 있습니다", "blocksAbout.start": "워크플로우를 시작하기 위한 초기 매개변수를 정의합니다", + "blocksAbout.start-placeholder": "이 워크플로가 시작되는 방식을 선택하세요", "blocksAbout.template-transform": "Jinja 템플릿 구문을 사용하여 데이터를 문자열로 변환합니다", "blocksAbout.tool": "외부 도구를 사용하여 워크플로우 기능을 확장하세요", "blocksAbout.trigger-plugin": "외부 플랫폼 이벤트로 워크플로를 시작하는 타사 통합 트리거", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "메시지 유형", "nodes.start.outputVars.query": "사용자 입력", "nodes.start.required": "필수", + "nodes.start.userInputTipDescription": "워크플로가 필요할 때 시작될 때 최종 사용자에게서 수집할 입력을 정의합니다.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Marketplace 에서 더 찾아보기", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Marketplace 에서 더 많은 도구 찾기", + "nodes.startPlaceholder.noTriggersFound": "트리거를 찾을 수 없습니다", + "nodes.startPlaceholder.nodeCollapsedDescription": "시작 노드를 구성하려면 클릭하세요", + "nodes.startPlaceholder.nodeDescription": "오른쪽 패널에서 시작 노드를 선택하세요", + "nodes.startPlaceholder.nodeTitle": "워크플로 시작", + "nodes.startPlaceholder.panelDescription": "시작 노드는 워크플로 실행을 트리거하는 항목을 정의합니다", + "nodes.startPlaceholder.panelTitle": "시작 노드 선택", + "nodes.startPlaceholder.userInputConflictTip": "사용자 입력은 다른 트리거와 함께 사용할 수 없습니다", + "nodes.startPlaceholder.validationRequired": "먼저 시작 노드를 선택하세요.", "nodes.templateTransform.code": "코드", "nodes.templateTransform.codeSupportTip": "Jinja2 만 지원합니다", "nodes.templateTransform.inputVars": "입력 변수", @@ -1209,9 +1223,11 @@ "tabs.sources": "소스", "tabs.start": "시작", "tabs.startDisabledTip": "트리거 노드와 사용자 입력 노드는 상호 배타적입니다.", + "tabs.startDisabledTipLearnMore": "시작 노드 자세히 알아보기", "tabs.startNotSupportedTip": "시작 탭은 조각에서 지원되지 않습니다.", "tabs.tools": "도구", "tabs.transform": "변환", + "tabs.unconfiguredStartDisabledTip": "구성되지 않은 시작 노드가 캔버스에 추가되었습니다. 계속하기 전에 설정을 완료하세요.", "tabs.usePlugin": "도구 선택", "tabs.utilities": "유틸리티", "tabs.workflowTool": "워크플로우", diff --git a/web/i18n/nl-NL/workflow.json b/web/i18n/nl-NL/workflow.json index 2dcf6ade439..f6760bf68bc 100644 --- a/web/i18n/nl-NL/workflow.json +++ b/web/i18n/nl-NL/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Loop", "blocks.loop-end": "Exit Loop", "blocks.loop-start": "Loop Start", + "blocks.mostCommon": "Meest gebruikt", "blocks.originalStartNode": "original start node", "blocks.parameter-extractor": "Parameter Extractor", "blocks.question-classifier": "Question Classifier", "blocks.start": "User Input", + "blocks.start-placeholder": "Workflow-start", "blocks.template-transform": "Template", "blocks.tool": "Tool", "blocks.trigger-plugin": "Plugin Trigger", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Use LLM to extract structured parameters from natural language for tool invocations or HTTP requests.", "blocksAbout.question-classifier": "Define the classification conditions of user questions, LLM can define how the conversation progresses based on the classification description", "blocksAbout.start": "Define the initial parameters for launching a workflow", + "blocksAbout.start-placeholder": "Kies hoe deze workflow start", "blocksAbout.template-transform": "Convert data to string using Jinja template syntax", "blocksAbout.tool": "Use external tools to extend workflow capabilities", "blocksAbout.trigger-plugin": "Third-party integration trigger that starts workflows from external platform events", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "message type", "nodes.start.outputVars.query": "User input", "nodes.start.required": "required", + "nodes.start.userInputTipDescription": "Definieer invoer die bij eindgebruikers wordt verzameld wanneer je workflow op aanvraag start.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Meer bekijken in Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Meer tools vinden in Marketplace", + "nodes.startPlaceholder.noTriggersFound": "Geen triggers gevonden", + "nodes.startPlaceholder.nodeCollapsedDescription": "Klik om de startnode te configureren", + "nodes.startPlaceholder.nodeDescription": "Kies een startnode in het rechterpaneel", + "nodes.startPlaceholder.nodeTitle": "Workflow-start", + "nodes.startPlaceholder.panelDescription": "De startnode bepaalt wat je workflow activeert", + "nodes.startPlaceholder.panelTitle": "Kies een startnode", + "nodes.startPlaceholder.userInputConflictTip": "Gebruikersinvoer kan niet worden gecombineerd met andere triggers", + "nodes.startPlaceholder.validationRequired": "Kies eerst een startnode.", "nodes.templateTransform.code": "Code", "nodes.templateTransform.codeSupportTip": "Only supports Jinja2", "nodes.templateTransform.inputVars": "Input Variables", @@ -1209,9 +1223,11 @@ "tabs.sources": "Sources", "tabs.start": "Start", "tabs.startDisabledTip": "Trigger node and user input node are mutually exclusive.", + "tabs.startDisabledTipLearnMore": "Meer informatie over startnodes", "tabs.startNotSupportedTip": "Het tabblad Start wordt niet ondersteund in fragmenten.", "tabs.tools": "Tools", "tabs.transform": "Transform", + "tabs.unconfiguredStartDisabledTip": "Er is een niet-geconfigureerde startnode aan het canvas toegevoegd. Voltooi de configuratie voordat je doorgaat.", "tabs.usePlugin": "Select tool", "tabs.utilities": "Utilities", "tabs.workflowTool": "Workflow", diff --git a/web/i18n/pl-PL/workflow.json b/web/i18n/pl-PL/workflow.json index 36aee9c524a..46395005f99 100644 --- a/web/i18n/pl-PL/workflow.json +++ b/web/i18n/pl-PL/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Pętla", "blocks.loop-end": "Wyjście z pętli", "blocks.loop-start": "Początek pętli", + "blocks.mostCommon": "Najczęstsze", "blocks.originalStartNode": "oryginalny węzeł początkowy", "blocks.parameter-extractor": "Ekstraktor parametrów", "blocks.question-classifier": "Klasyfikator pytań", "blocks.start": "Start", + "blocks.start-placeholder": "Start workflow", "blocks.template-transform": "Szablon", "blocks.tool": "Narzędzie", "blocks.trigger-plugin": "Wyzwalacz wtyczki", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Użyj LLM do wyodrębnienia strukturalnych parametrów z języka naturalnego do wywołań narzędzi lub żądań HTTP.", "blocksAbout.question-classifier": "Zdefiniuj warunki klasyfikacji pytań użytkowników, LLM może definiować, jak rozmowa postępuje na podstawie opisu klasyfikacji", "blocksAbout.start": "Zdefiniuj początkowe parametry uruchamiania przepływu pracy", + "blocksAbout.start-placeholder": "Wybierz, jak rozpoczyna się ten workflow", "blocksAbout.template-transform": "Konwertuj dane na ciąg znaków przy użyciu składni szablonu Jinja", "blocksAbout.tool": "Używaj zewnętrznych narzędzi, aby rozszerzyć możliwości przepływu pracy", "blocksAbout.trigger-plugin": "Wyzwalacz integracji zewnętrznej, który uruchamia przepływy pracy na podstawie zdarzeń z platformy zewnętrznej", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "typ wiadomości", "nodes.start.outputVars.query": "Wprowadzenie użytkownika", "nodes.start.required": "wymagane", + "nodes.start.userInputTipDescription": "Określ dane wejściowe zbierane od użytkowników końcowych, gdy workflow uruchamia się na żądanie.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Przeglądaj więcej w Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Znajdź więcej narzędzi w Marketplace", + "nodes.startPlaceholder.noTriggersFound": "Nie znaleziono wyzwalaczy", + "nodes.startPlaceholder.nodeCollapsedDescription": "Kliknij, aby skonfigurować węzeł startowy", + "nodes.startPlaceholder.nodeDescription": "Wybierz węzeł startowy z prawego panelu", + "nodes.startPlaceholder.nodeTitle": "Start workflow", + "nodes.startPlaceholder.panelDescription": "Węzeł startowy określa, co uruchamia workflow", + "nodes.startPlaceholder.panelTitle": "Wybierz węzeł startowy", + "nodes.startPlaceholder.userInputConflictTip": "Danych wejściowych użytkownika nie można łączyć z innymi wyzwalaczami", + "nodes.startPlaceholder.validationRequired": "Najpierw wybierz węzeł startowy.", "nodes.templateTransform.code": "Kod", "nodes.templateTransform.codeSupportTip": "Obsługuje tylko Jinja2", "nodes.templateTransform.inputVars": "Zmienne wejściowe", @@ -1209,9 +1223,11 @@ "tabs.sources": "Źródeł", "tabs.start": "Start", "tabs.startDisabledTip": "Węzeł wyzwalacza i węzeł wprowadzania danych przez użytkownika wzajemnie się wykluczają.", + "tabs.startDisabledTipLearnMore": "Dowiedz się więcej o węzłach startowych", "tabs.startNotSupportedTip": "Karta Start nie jest obsługiwana we fragmentach.", "tabs.tools": "Narzędzia", "tabs.transform": "Transformacja", + "tabs.unconfiguredStartDisabledTip": "Do obszaru roboczego dodano nieskonfigurowany węzeł startowy. Przed kontynuowaniem dokończ konfigurację.", "tabs.usePlugin": "Wybierz narzędzie", "tabs.utilities": "Narzędzia pomocnicze", "tabs.workflowTool": "Przepływ pracy", diff --git a/web/i18n/pt-BR/workflow.json b/web/i18n/pt-BR/workflow.json index 02de8500543..db4d85ce17a 100644 --- a/web/i18n/pt-BR/workflow.json +++ b/web/i18n/pt-BR/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Laço", "blocks.loop-end": "Sair do Loop", "blocks.loop-start": "Início do Loop", + "blocks.mostCommon": "Mais comum", "blocks.originalStartNode": "nó inicial original", "blocks.parameter-extractor": "Extrator de parâmetros", "blocks.question-classifier": "Classificador de perguntas", "blocks.start": "Iniciar", + "blocks.start-placeholder": "Início do workflow", "blocks.template-transform": "Modelo", "blocks.tool": "Ferramenta", "blocks.trigger-plugin": "Acionador de Plugin", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Use LLM para extrair parâmetros estruturados da linguagem natural para invocações de ferramentas ou requisições HTTP.", "blocksAbout.question-classifier": "Definir as condições de classificação das perguntas dos usuários, LLM pode definir como a conversa progride com base na descrição da classificação", "blocksAbout.start": "Definir os parâmetros iniciais para iniciar um fluxo de trabalho", + "blocksAbout.start-placeholder": "Escolha como este workflow começa", "blocksAbout.template-transform": "Converter dados em string usando a sintaxe de template Jinja", "blocksAbout.tool": "Use ferramentas externas para ampliar as capacidades do fluxo de trabalho", "blocksAbout.trigger-plugin": "Gatilho de integração de terceiros que inicia fluxos de trabalho a partir de eventos de plataformas externas", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "tipo de mensagem", "nodes.start.outputVars.query": "Entrada do usuário", "nodes.start.required": "requerido", + "nodes.start.userInputTipDescription": "Defina entradas para coletar dos usuários finais quando seu workflow iniciar sob demanda.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Procurar mais no Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Encontrar mais ferramentas no Marketplace", + "nodes.startPlaceholder.noTriggersFound": "Nenhum gatilho foi encontrado", + "nodes.startPlaceholder.nodeCollapsedDescription": "Clique para configurar o nó inicial", + "nodes.startPlaceholder.nodeDescription": "Escolha um nó inicial no painel à direita", + "nodes.startPlaceholder.nodeTitle": "Início do workflow", + "nodes.startPlaceholder.panelDescription": "O nó inicial define o que aciona a execução do seu workflow", + "nodes.startPlaceholder.panelTitle": "Escolha um nó inicial", + "nodes.startPlaceholder.userInputConflictTip": "Entrada do usuário não pode ser combinada com outros gatilhos", + "nodes.startPlaceholder.validationRequired": "Escolha primeiro um nó inicial.", "nodes.templateTransform.code": "Código", "nodes.templateTransform.codeSupportTip": "Suporta apenas Jinja2", "nodes.templateTransform.inputVars": "Variáveis de entrada", @@ -1209,9 +1223,11 @@ "tabs.sources": "Fontes", "tabs.start": "Começar", "tabs.startDisabledTip": "O nó de gatilho e o nó de entrada do usuário são mutuamente exclusivos.", + "tabs.startDisabledTipLearnMore": "Saiba mais sobre nós iniciais", "tabs.startNotSupportedTip": "A guia Iniciar não é compatível com snippets.", "tabs.tools": "Ferramentas", "tabs.transform": "Transformar", + "tabs.unconfiguredStartDisabledTip": "Um nó inicial não configurado foi adicionado à tela. Conclua a configuração antes de continuar.", "tabs.usePlugin": "Selecionar ferramenta", "tabs.utilities": "Utilitários", "tabs.workflowTool": "Fluxo de trabalho", diff --git a/web/i18n/ro-RO/workflow.json b/web/i18n/ro-RO/workflow.json index e5f4464ca62..5d255f9852b 100644 --- a/web/i18n/ro-RO/workflow.json +++ b/web/i18n/ro-RO/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Loop", "blocks.loop-end": "Ieșire din buclă", "blocks.loop-start": "Întreținere buclă", + "blocks.mostCommon": "Cel mai frecvent", "blocks.originalStartNode": "nod de start original", "blocks.parameter-extractor": "Extractor de parametri", "blocks.question-classifier": "Clasificator de întrebări", "blocks.start": "Începe", + "blocks.start-placeholder": "Pornire workflow", "blocks.template-transform": "Șablon", "blocks.tool": "Unealtă", "blocks.trigger-plugin": "Declanșator plugin", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Utilizați LLM pentru a extrage parametrii structurați din limbajul natural pentru invocările de instrumente sau cererile HTTP.", "blocksAbout.question-classifier": "Definiți condițiile de clasificare a întrebărilor utilizatorului, LLM poate defini cum progresează conversația pe baza descrierii clasificării", "blocksAbout.start": "Definiți parametrii inițiali pentru lansarea unui flux de lucru", + "blocksAbout.start-placeholder": "Alegeți cum începe acest workflow", "blocksAbout.template-transform": "Convertiți datele în șiruri de caractere folosind sintaxa șablonului Jinja", "blocksAbout.tool": "Utilizați instrumente externe pentru a extinde capacitățile fluxului de lucru", "blocksAbout.trigger-plugin": "Declanșator de integrare terță parte care pornește fluxuri de lucru din evenimente ale platformelor externe", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "tip mesaj", "nodes.start.outputVars.query": "Intrare utilizator", "nodes.start.required": "necesar", + "nodes.start.userInputTipDescription": "Definiți intrările de colectat de la utilizatorii finali când workflow-ul pornește la cerere.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Răsfoiți mai multe în Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Găsiți mai multe instrumente în Marketplace", + "nodes.startPlaceholder.noTriggersFound": "Nu au fost găsite declanșatoare", + "nodes.startPlaceholder.nodeCollapsedDescription": "Faceți clic pentru a configura nodul de pornire", + "nodes.startPlaceholder.nodeDescription": "Alegeți un nod de pornire din panoul din dreapta", + "nodes.startPlaceholder.nodeTitle": "Pornire workflow", + "nodes.startPlaceholder.panelDescription": "Nodul de pornire definește ce declanșează rularea workflow-ului", + "nodes.startPlaceholder.panelTitle": "Alegeți un nod de pornire", + "nodes.startPlaceholder.userInputConflictTip": "Intrarea utilizatorului nu poate fi combinată cu alte declanșatoare", + "nodes.startPlaceholder.validationRequired": "Alegeți mai întâi un nod de pornire.", "nodes.templateTransform.code": "Cod", "nodes.templateTransform.codeSupportTip": "Suportă doar Jinja2", "nodes.templateTransform.inputVars": "Variabile de intrare", @@ -1209,9 +1223,11 @@ "tabs.sources": "Surse", "tabs.start": "Începe", "tabs.startDisabledTip": "Nodul de declanșare și nodul de intrare a utilizatorului se exclud reciproc.", + "tabs.startDisabledTipLearnMore": "Aflați mai multe despre nodurile de pornire", "tabs.startNotSupportedTip": "Fila Start nu este acceptată în fragmente.", "tabs.tools": "Instrumente", "tabs.transform": "Transformare", + "tabs.unconfiguredStartDisabledTip": "Un nod de pornire neconfigurat a fost adăugat pe canvas. Finalizați configurarea înainte de a continua.", "tabs.usePlugin": "Selectează instrumentul", "tabs.utilities": "Utilități", "tabs.workflowTool": "Flux de lucru", diff --git a/web/i18n/ru-RU/workflow.json b/web/i18n/ru-RU/workflow.json index e5ca0c9dc59..5d485efc384 100644 --- a/web/i18n/ru-RU/workflow.json +++ b/web/i18n/ru-RU/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Цикл", "blocks.loop-end": "Конец цикла", "blocks.loop-start": "Начало цикла", + "blocks.mostCommon": "Самый распространенный", "blocks.originalStartNode": "исходный начальный узел", "blocks.parameter-extractor": "Экстрактор параметров", "blocks.question-classifier": "Классификатор вопросов", "blocks.start": "Начало", + "blocks.start-placeholder": "Запуск workflow", "blocks.template-transform": "Шаблон", "blocks.tool": "Инструмент", "blocks.trigger-plugin": "Триггер плагина", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Используйте LLM для извлечения структурированных параметров из естественного языка для вызова инструментов или HTTP-запросов.", "blocksAbout.question-classifier": "Определите условия классификации вопросов пользователей, LLM может определить, как будет развиваться разговор на основе описания классификации", "blocksAbout.start": "Определите начальные параметры для запуска рабочего процесса", + "blocksAbout.start-placeholder": "Выберите, как запускается этот workflow", "blocksAbout.template-transform": "Преобразование данных в строку с использованием синтаксиса шаблонов Jinja", "blocksAbout.tool": "Используйте внешние инструменты для расширения возможностей рабочего процесса", "blocksAbout.trigger-plugin": "Триггер интеграции с третьими сторонами, который запускает рабочие процессы на основе событий внешней платформы", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "тип сообщения", "nodes.start.outputVars.query": "Ввод пользователя", "nodes.start.required": "обязательно", + "nodes.start.userInputTipDescription": "Задайте входные данные, которые нужно собрать у конечных пользователей при запуске workflow по запросу.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Найти больше в Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Найти больше инструментов в Marketplace", + "nodes.startPlaceholder.noTriggersFound": "Триггеры не найдены", + "nodes.startPlaceholder.nodeCollapsedDescription": "Нажмите, чтобы настроить начальный узел", + "nodes.startPlaceholder.nodeDescription": "Выберите начальный узел на правой панели", + "nodes.startPlaceholder.nodeTitle": "Запуск workflow", + "nodes.startPlaceholder.panelDescription": "Начальный узел определяет, что запускает выполнение вашего workflow", + "nodes.startPlaceholder.panelTitle": "Выберите начальный узел", + "nodes.startPlaceholder.userInputConflictTip": "Пользовательский ввод нельзя сочетать с другими триггерами", + "nodes.startPlaceholder.validationRequired": "Сначала выберите начальный узел.", "nodes.templateTransform.code": "Код", "nodes.templateTransform.codeSupportTip": "Поддерживает только Jinja2", "nodes.templateTransform.inputVars": "Входные переменные", @@ -1209,9 +1223,11 @@ "tabs.sources": "Источников", "tabs.start": "Начать", "tabs.startDisabledTip": "Узел триггера и узел ввода пользователя исключают друг друга.", + "tabs.startDisabledTipLearnMore": "Подробнее о начальных узлах", "tabs.startNotSupportedTip": "Вкладка «Пуск» не поддерживается во фрагментах.", "tabs.tools": "Инструменты", "tabs.transform": "Преобразование", + "tabs.unconfiguredStartDisabledTip": "На холст добавлен ненастроенный начальный узел. Завершите настройку, прежде чем продолжить.", "tabs.usePlugin": "Выбрать инструмент", "tabs.utilities": "Утилиты", "tabs.workflowTool": "Рабочий процесс", diff --git a/web/i18n/sl-SI/workflow.json b/web/i18n/sl-SI/workflow.json index e1d3cd028f1..fc17ac6bde2 100644 --- a/web/i18n/sl-SI/workflow.json +++ b/web/i18n/sl-SI/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Zanka", "blocks.loop-end": "Izhod iz zanke", "blocks.loop-start": "Začetek zanke", + "blocks.mostCommon": "Najpogostejše", "blocks.originalStartNode": "izvorna začetna točka", "blocks.parameter-extractor": "Ekstraktor parametrov", "blocks.question-classifier": "Razvrščevalec vprašanj", "blocks.start": "Začni", + "blocks.start-placeholder": "Začetek workflowa", "blocks.template-transform": "Predloga", "blocks.tool": "Orodje", "blocks.trigger-plugin": "Sprožilec vtičnika", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Uporabite LLM za pridobivanje strukturiranih parametrov iz naravnega jezika za klice orodij ali HTTP zahtev.", "blocksAbout.question-classifier": "Določite pogoje klasifikacije uporabniških vprašanj, LLM lahko določi, kako se pogovor razvija na podlagi opisa klasifikacije.", "blocksAbout.start": "Določite začetne parametre za zagon delovnega toka", + "blocksAbout.start-placeholder": "Izberite, kako se ta workflow začne", "blocksAbout.template-transform": "Pretvori podatke v niz z uporabo Jinja predloge", "blocksAbout.tool": "Uporabite zunanja orodja za razširitev zmogljivosti delovnega toka", "blocksAbout.trigger-plugin": "Sprožilec integracije tretje osebe, ki začne delovne tokove iz dogodkov na zunanji platformi", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "vrsta sporočila", "nodes.start.outputVars.query": "Uporabniški vnos", "nodes.start.required": "zahtevano", + "nodes.start.userInputTipDescription": "Določite vnose, ki jih želite zbrati od končnih uporabnikov, ko se workflow zažene na zahtevo.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Prebrskajte več v Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Poiščite več orodij v Marketplace", + "nodes.startPlaceholder.noTriggersFound": "Ni najdenih sprožilcev", + "nodes.startPlaceholder.nodeCollapsedDescription": "Kliknite za konfiguracijo začetnega vozlišča", + "nodes.startPlaceholder.nodeDescription": "Izberite začetno vozlišče na desni plošči", + "nodes.startPlaceholder.nodeTitle": "Začetek workflowa", + "nodes.startPlaceholder.panelDescription": "Začetno vozlišče določa, kaj sproži zagon vašega workflowa", + "nodes.startPlaceholder.panelTitle": "Izberite začetno vozlišče", + "nodes.startPlaceholder.userInputConflictTip": "Uporabniškega vnosa ni mogoče kombinirati z drugimi sprožilci", + "nodes.startPlaceholder.validationRequired": "Najprej izberite začetno vozlišče.", "nodes.templateTransform.code": "Koda", "nodes.templateTransform.codeSupportTip": "Podpira samo Jinja2", "nodes.templateTransform.inputVars": "Vhodne spremenljivke", @@ -1209,9 +1223,11 @@ "tabs.sources": "Virov", "tabs.start": "Začni", "tabs.startDisabledTip": "Vozlišče sprožilca in vozlišče vnosa uporabnika se med seboj izključujeta.", + "tabs.startDisabledTipLearnMore": "Več o začetnih vozliščih", "tabs.startNotSupportedTip": "Zavihek Start ni podprt v izrezkih.", "tabs.tools": "Orodja", "tabs.transform": "Pretvori", + "tabs.unconfiguredStartDisabledTip": "Na platno je bilo dodano nekonfigurirano začetno vozlišče. Pred nadaljevanjem dokončajte nastavitev.", "tabs.usePlugin": "Izberi orodje", "tabs.utilities": "Komunalne storitve", "tabs.workflowTool": "Delovni tok", diff --git a/web/i18n/th-TH/workflow.json b/web/i18n/th-TH/workflow.json index 7fa8d0bf684..91d073fe32e 100644 --- a/web/i18n/th-TH/workflow.json +++ b/web/i18n/th-TH/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "ลูป", "blocks.loop-end": "ออกจากลูป", "blocks.loop-start": "เริ่มลูป", + "blocks.mostCommon": "พบบ่อยที่สุด", "blocks.originalStartNode": "โหนดเริ่มต้นเดิม", "blocks.parameter-extractor": "ตัวแยกพารามิเตอร์", "blocks.question-classifier": "ตัวจําแนกคําถาม", "blocks.start": "เริ่ม", + "blocks.start-placeholder": "เริ่มต้นเวิร์กโฟลว์", "blocks.template-transform": "แม่ แบบ", "blocks.tool": "เครื่องมือ", "blocks.trigger-plugin": "ทริกเกอร์ปลั๊กอิน", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "ใช้ LLM เพื่อแยกพารามิเตอร์ที่มีโครงสร้างจากภาษาธรรมชาติสําหรับการเรียกใช้เครื่องมือหรือคําขอ HTTP", "blocksAbout.question-classifier": "กําหนดเงื่อนไขการจําแนกประเภทของคําถามของผู้ใช้ LLM สามารถกําหนดความคืบหน้าของการสนทนาตามคําอธิบายการจําแนกประเภท", "blocksAbout.start": "กําหนดพารามิเตอร์เริ่มต้นสําหรับการเปิดใช้เวิร์กโฟลว์", + "blocksAbout.start-placeholder": "เลือกวิธีเริ่มต้นเวิร์กโฟลว์นี้", "blocksAbout.template-transform": "แปลงข้อมูลเป็นสตริงโดยใช้ไวยากรณ์เทมเพลต Jinja", "blocksAbout.tool": "ใช้เครื่องมือภายนอกเพื่อขยายความสามารถของเวิร์กโฟลว์", "blocksAbout.trigger-plugin": "ทริกเกอร์การรวมจากบุคคลที่สามที่เริ่มการทำงานอัตโนมัติจากเหตุการณ์ของแพลตฟอร์มภายนอก", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "ประเภทข้อความ", "nodes.start.outputVars.query": "การป้อนข้อมูลของผู้ใช้", "nodes.start.required": "ต้องระบุ", + "nodes.start.userInputTipDescription": "กำหนดอินพุตที่จะรวบรวมจากผู้ใช้ปลายทางเมื่อเวิร์กโฟลว์เริ่มทำงานตามคำขอ", + "nodes.startPlaceholder.browseMoreOnMarketplace": "เรียกดูเพิ่มเติมใน Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "ค้นหาเครื่องมือเพิ่มเติมใน Marketplace", + "nodes.startPlaceholder.noTriggersFound": "ไม่พบทริกเกอร์", + "nodes.startPlaceholder.nodeCollapsedDescription": "คลิกเพื่อกำหนดค่าโหนดเริ่มต้น", + "nodes.startPlaceholder.nodeDescription": "เลือกโหนดเริ่มต้นจากแผงด้านขวา", + "nodes.startPlaceholder.nodeTitle": "เริ่มต้นเวิร์กโฟลว์", + "nodes.startPlaceholder.panelDescription": "โหนดเริ่มต้นกำหนดสิ่งที่จะทริกเกอร์ให้เวิร์กโฟลว์ทำงาน", + "nodes.startPlaceholder.panelTitle": "เลือกโหนดเริ่มต้น", + "nodes.startPlaceholder.userInputConflictTip": "อินพุตผู้ใช้ไม่สามารถใช้ร่วมกับทริกเกอร์อื่นได้", + "nodes.startPlaceholder.validationRequired": "โปรดเลือกโหนดเริ่มต้นก่อน", "nodes.templateTransform.code": "รหัส", "nodes.templateTransform.codeSupportTip": "รองรับเฉพาะ Jinja2", "nodes.templateTransform.inputVars": "ตัวแปรอินพุต", @@ -1209,9 +1223,11 @@ "tabs.sources": "แหล่ง", "tabs.start": "เริ่ม", "tabs.startDisabledTip": "โหนดทริกเกอร์และโหนดป้อนข้อมูลของผู้ใช้ไม่สามารถใช้ร่วมกันได้", + "tabs.startDisabledTipLearnMore": "เรียนรู้เพิ่มเติมเกี่ยวกับโหนดเริ่มต้น", "tabs.startNotSupportedTip": "ตัวอย่างข้อมูลไม่รองรับแท็บเริ่มต้น", "tabs.tools": "เครื่อง มือ", "tabs.transform": "แปลง", + "tabs.unconfiguredStartDisabledTip": "มีการเพิ่มโหนดเริ่มต้นที่ยังไม่ได้กำหนดค่าลงในแคนวาส โปรดตั้งค่าให้เสร็จก่อนดำเนินการต่อ", "tabs.usePlugin": "เลือกเครื่องมือ", "tabs.utilities": "สาธารณูปโภค", "tabs.workflowTool": "เวิร์กโฟลว์", diff --git a/web/i18n/tr-TR/workflow.json b/web/i18n/tr-TR/workflow.json index b478e8c9c04..8aa69757957 100644 --- a/web/i18n/tr-TR/workflow.json +++ b/web/i18n/tr-TR/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Döngü", "blocks.loop-end": "Döngüden Çık", "blocks.loop-start": "Döngü Başlangıcı", + "blocks.mostCommon": "En yaygın", "blocks.originalStartNode": "orijinal başlangıç düğümü", "blocks.parameter-extractor": "Parametre Çıkarıcı", "blocks.question-classifier": "Soru Sınıflandırıcı", "blocks.start": "Başlat", + "blocks.start-placeholder": "Workflow başlangıcı", "blocks.template-transform": "Şablon", "blocks.tool": "Araç", "blocks.trigger-plugin": "Eklenti Tetikleyicisi", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Aracı çağırmak veya HTTP istekleri için doğal dilden yapılandırılmış parametreler çıkarmak için LLM kullanın.", "blocksAbout.question-classifier": "Kullanıcı sorularının sınıflandırma koşullarını tanımlayın, LLM sınıflandırma açıklamasına dayalı olarak konuşmanın nasıl ilerleyeceğini tanımlayabilir", "blocksAbout.start": "Bir iş akışını başlatmak için başlangıç parametrelerini tanımlayın", + "blocksAbout.start-placeholder": "Bu workflow’un nasıl başlayacağını seçin", "blocksAbout.template-transform": "Jinja şablon sözdizimini kullanarak verileri stringe dönüştürün", "blocksAbout.tool": "İş akışı yeteneklerini genişletmek için dış araçlar kullanın", "blocksAbout.trigger-plugin": "Üçüncü taraf entegrasyon tetikleyicisi, dış platform olaylarından iş akışlarını başlatır", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "mesaj türü", "nodes.start.outputVars.query": "Kullanıcı girişi", "nodes.start.required": "gerekli", + "nodes.start.userInputTipDescription": "Workflow isteğe bağlı başladığında son kullanıcılardan toplanacak girişleri tanımlayın.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Marketplace’te daha fazlasına göz atın", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Marketplace’te daha fazla araç bulun", + "nodes.startPlaceholder.noTriggersFound": "Tetikleyici bulunamadı", + "nodes.startPlaceholder.nodeCollapsedDescription": "Başlangıç düğümünü yapılandırmak için tıklayın", + "nodes.startPlaceholder.nodeDescription": "Sağ panelden bir başlangıç düğümü seçin", + "nodes.startPlaceholder.nodeTitle": "Workflow başlangıcı", + "nodes.startPlaceholder.panelDescription": "Başlangıç düğümü workflow’unuzu neyin çalıştıracağını tanımlar", + "nodes.startPlaceholder.panelTitle": "Bir başlangıç düğümü seçin", + "nodes.startPlaceholder.userInputConflictTip": "Kullanıcı girişi diğer tetikleyicilerle birleştirilemez", + "nodes.startPlaceholder.validationRequired": "Önce bir başlangıç düğümü seçin.", "nodes.templateTransform.code": "Kod", "nodes.templateTransform.codeSupportTip": "Sadece Jinja2 destekler", "nodes.templateTransform.inputVars": "Giriş Değişkenleri", @@ -1209,9 +1223,11 @@ "tabs.sources": "Kaynak", "tabs.start": "Başlat", "tabs.startDisabledTip": "Tetikleyici düğümü ve kullanıcı girişi düğümü birbirini dışlar.", + "tabs.startDisabledTipLearnMore": "Başlangıç düğümleri hakkında daha fazla bilgi edinin", "tabs.startNotSupportedTip": "Başlangıç sekmesi parçacıklarda desteklenmez.", "tabs.tools": "Araçlar", "tabs.transform": "Dönüştür", + "tabs.unconfiguredStartDisabledTip": "Tuvale yapılandırılmamış bir başlangıç düğümü eklendi. Devam etmeden önce kurulumu tamamlayın.", "tabs.usePlugin": "Araç seç", "tabs.utilities": "Yardımcı Araçlar", "tabs.workflowTool": "İş Akışı", diff --git a/web/i18n/uk-UA/workflow.json b/web/i18n/uk-UA/workflow.json index 948f9e1dab0..1b4b8e7a289 100644 --- a/web/i18n/uk-UA/workflow.json +++ b/web/i18n/uk-UA/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Петля", "blocks.loop-end": "Вихід з циклу", "blocks.loop-start": "Початок циклу", + "blocks.mostCommon": "Найпоширеніше", "blocks.originalStartNode": "оригінальний початковий вузол", "blocks.parameter-extractor": "Екстрактор параметрів", "blocks.question-classifier": "Класифікатор питань", "blocks.start": "Початок", + "blocks.start-placeholder": "Початок workflow", "blocks.template-transform": "Шаблон", "blocks.tool": "Інструмент", "blocks.trigger-plugin": "Тригер плагіна", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Використовуйте LLM для вилучення структурованих параметрів з природної мови для викликів інструментів або HTTP-запитів.", "blocksAbout.question-classifier": "Визначте умови класифікації запитань користувачів, LLM може визначати, як розвивається розмова на основі опису класифікації", "blocksAbout.start": "Визначте початкові параметри для запуску робочого потоку", + "blocksAbout.start-placeholder": "Виберіть, як запускається цей workflow", "blocksAbout.template-transform": "Перетворіть дані на рядок за допомогою синтаксису шаблону Jinja", "blocksAbout.tool": "Використовуйте зовнішні інструменти для розширення можливостей робочого процесу", "blocksAbout.trigger-plugin": "Тригер інтеграції сторонніх розробників, який запускає робочі процеси з подій зовнішньої платформи", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "тип повідомлення", "nodes.start.outputVars.query": "Введення користувача", "nodes.start.required": "обов'язковий", + "nodes.start.userInputTipDescription": "Визначте вхідні дані, які потрібно збирати від кінцевих користувачів, коли workflow запускається на вимогу.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Переглянути більше в Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Знайти більше інструментів у Marketplace", + "nodes.startPlaceholder.noTriggersFound": "Тригери не знайдено", + "nodes.startPlaceholder.nodeCollapsedDescription": "Натисніть, щоб налаштувати початковий вузол", + "nodes.startPlaceholder.nodeDescription": "Виберіть початковий вузол на правій панелі", + "nodes.startPlaceholder.nodeTitle": "Початок workflow", + "nodes.startPlaceholder.panelDescription": "Початковий вузол визначає, що запускає виконання вашого workflow", + "nodes.startPlaceholder.panelTitle": "Виберіть початковий вузол", + "nodes.startPlaceholder.userInputConflictTip": "Користувацьке введення не можна поєднувати з іншими тригерами", + "nodes.startPlaceholder.validationRequired": "Спочатку виберіть початковий вузол.", "nodes.templateTransform.code": "Код", "nodes.templateTransform.codeSupportTip": "Підтримує лише Jinja2", "nodes.templateTransform.inputVars": "Вхідні змінні", @@ -1209,9 +1223,11 @@ "tabs.sources": "Джерел", "tabs.start": "Почати", "tabs.startDisabledTip": "Вузол тригера та вузол введення користувача взаємовиключні.", + "tabs.startDisabledTipLearnMore": "Докладніше про початкові вузли", "tabs.startNotSupportedTip": "Вкладка «Пуск» не підтримується у фрагментах.", "tabs.tools": "Інструменти", "tabs.transform": "Трансформація", + "tabs.unconfiguredStartDisabledTip": "На полотно додано неналаштований початковий вузол. Завершіть налаштування, перш ніж продовжити.", "tabs.usePlugin": "Вибрати інструмент", "tabs.utilities": "Утиліти", "tabs.workflowTool": "Робочий потік", diff --git a/web/i18n/vi-VN/workflow.json b/web/i18n/vi-VN/workflow.json index 139f378b2d8..4187e46b9da 100644 --- a/web/i18n/vi-VN/workflow.json +++ b/web/i18n/vi-VN/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Vòng", "blocks.loop-end": "Thoát vòng lặp", "blocks.loop-start": "Bắt đầu vòng lặp", + "blocks.mostCommon": "Phổ biến nhất", "blocks.originalStartNode": "nút bắt đầu gốc", "blocks.parameter-extractor": "Trình trích xuất tham số", "blocks.question-classifier": "Phân loại câu hỏi", "blocks.start": "Bắt đầu", + "blocks.start-placeholder": "Bắt đầu workflow", "blocks.template-transform": "Mẫu", "blocks.tool": "Công cụ", "blocks.trigger-plugin": "Kích hoạt Plugin", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Sử dụng LLM để trích xuất các tham số có cấu trúc từ ngôn ngữ tự nhiên để gọi công cụ hoặc yêu cầu HTTP.", "blocksAbout.question-classifier": "Định nghĩa các điều kiện phân loại câu hỏi của người dùng, LLM có thể định nghĩa cách cuộc trò chuyện tiến triển dựa trên mô tả phân loại", "blocksAbout.start": "Định nghĩa các tham số ban đầu để khởi chạy quy trình làm việc", + "blocksAbout.start-placeholder": "Chọn cách workflow này bắt đầu", "blocksAbout.template-transform": "Chuyển đổi dữ liệu thành chuỗi bằng cú pháp mẫu Jinja", "blocksAbout.tool": "Sử dụng các công cụ bên ngoài để mở rộng khả năng quy trình làm việc", "blocksAbout.trigger-plugin": "Kích hoạt tích hợp bên thứ ba khởi chạy quy trình từ các sự kiện trên nền tảng bên ngoài", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "loại tin nhắn", "nodes.start.outputVars.query": "Đầu vào của người dùng", "nodes.start.required": "bắt buộc", + "nodes.start.userInputTipDescription": "Xác định các đầu vào cần thu thập từ người dùng cuối khi workflow của bạn bắt đầu theo yêu cầu.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Duyệt thêm trên Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Tìm thêm công cụ trên Marketplace", + "nodes.startPlaceholder.noTriggersFound": "Không tìm thấy trình kích hoạt nào", + "nodes.startPlaceholder.nodeCollapsedDescription": "Nhấp để cấu hình nút bắt đầu", + "nodes.startPlaceholder.nodeDescription": "Chọn một nút bắt đầu từ bảng bên phải", + "nodes.startPlaceholder.nodeTitle": "Bắt đầu workflow", + "nodes.startPlaceholder.panelDescription": "Nút bắt đầu xác định điều gì kích hoạt workflow của bạn chạy", + "nodes.startPlaceholder.panelTitle": "Chọn một nút bắt đầu", + "nodes.startPlaceholder.userInputConflictTip": "Đầu vào người dùng không thể kết hợp với các trình kích hoạt khác", + "nodes.startPlaceholder.validationRequired": "Trước tiên hãy chọn một nút bắt đầu.", "nodes.templateTransform.code": "Mã", "nodes.templateTransform.codeSupportTip": "Chỉ hỗ trợ Jinja2", "nodes.templateTransform.inputVars": "Biến đầu vào", @@ -1209,9 +1223,11 @@ "tabs.sources": "Nguồn", "tabs.start": "Bắt đầu", "tabs.startDisabledTip": "Nút kích hoạt và nút nhập liệu của người dùng là loại trừ lẫn nhau.", + "tabs.startDisabledTipLearnMore": "Tìm hiểu thêm về các nút bắt đầu", "tabs.startNotSupportedTip": "Tab bắt đầu không được hỗ trợ trong đoạn trích.", "tabs.tools": "Công cụ", "tabs.transform": "Chuyển đổi", + "tabs.unconfiguredStartDisabledTip": "Một nút bắt đầu chưa được cấu hình đã được thêm vào canvas. Vui lòng hoàn tất thiết lập trước khi tiếp tục.", "tabs.usePlugin": "Chọn công cụ", "tabs.utilities": "Tiện ích", "tabs.workflowTool": "Quy trình làm việc", diff --git a/web/i18n/zh-Hans/workflow.json b/web/i18n/zh-Hans/workflow.json index 272e852e2e0..77d65b9eb43 100644 --- a/web/i18n/zh-Hans/workflow.json +++ b/web/i18n/zh-Hans/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "循环", "blocks.loop-end": "退出循环", "blocks.loop-start": "循环开始", + "blocks.mostCommon": "最常用", "blocks.originalStartNode": "原始开始节点", "blocks.parameter-extractor": "参数提取器", "blocks.question-classifier": "问题分类器", "blocks.start": "用户输入", + "blocks.start-placeholder": "工作流开始", "blocks.template-transform": "模板转换", "blocks.tool": "工具", "blocks.trigger-plugin": "插件触发器", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "利用 LLM 从自然语言内推理提取出结构化参数,用于后置的工具调用或 HTTP 请求。", "blocksAbout.question-classifier": "定义用户问题的分类条件,LLM 能够根据分类描述定义对话的进展方式", "blocksAbout.start": "定义一个 workflow 流程启动的初始参数", + "blocksAbout.start-placeholder": "选择这个 workflow 的启动方式", "blocksAbout.template-transform": "使用 Jinja 模板语法将数据转换为字符串", "blocksAbout.tool": "使用外部工具扩展工作流功能", "blocksAbout.trigger-plugin": "从外部平台事件启动工作流的第三方集成触发器", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "消息类型", "nodes.start.outputVars.query": "用户输入", "nodes.start.required": "必填", + "nodes.start.userInputTipDescription": "定义当 workflow 按需启动时需要向终端用户收集的输入。", + "nodes.startPlaceholder.browseMoreOnMarketplace": "在 Marketplace 浏览更多", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "在 Marketplace 查找更多工具", + "nodes.startPlaceholder.noTriggersFound": "未找到触发器", + "nodes.startPlaceholder.nodeCollapsedDescription": "点击配置开始节点", + "nodes.startPlaceholder.nodeDescription": "从右侧面板选择开始节点", + "nodes.startPlaceholder.nodeTitle": "工作流开始", + "nodes.startPlaceholder.panelDescription": "开始节点定义 workflow 的触发方式", + "nodes.startPlaceholder.panelTitle": "选择开始节点", + "nodes.startPlaceholder.userInputConflictTip": "用户输入不能和其他触发器组合使用", + "nodes.startPlaceholder.validationRequired": "请先选择开始节点。", "nodes.templateTransform.code": "代码", "nodes.templateTransform.codeSupportTip": "只支持 Jinja2", "nodes.templateTransform.inputVars": "输入变量", @@ -1209,9 +1223,11 @@ "tabs.sources": "数据源", "tabs.start": "开始", "tabs.startDisabledTip": "触发节点与用户输入节点互斥。", + "tabs.startDisabledTipLearnMore": "了解更多开始节点", "tabs.startNotSupportedTip": "Snippet 暂不支持 Start 标签。", "tabs.tools": "工具", "tabs.transform": "转换", + "tabs.unconfiguredStartDisabledTip": "画布上已有未配置的开始节点。请先完成设置后再继续。", "tabs.usePlugin": "选择工具", "tabs.utilities": "工具", "tabs.workflowTool": "工作流", diff --git a/web/i18n/zh-Hant/workflow.json b/web/i18n/zh-Hant/workflow.json index 71b13c8eee3..0f98827c890 100644 --- a/web/i18n/zh-Hant/workflow.json +++ b/web/i18n/zh-Hant/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "循環", "blocks.loop-end": "退出循環", "blocks.loop-start": "循環開始", + "blocks.mostCommon": "最常用", "blocks.originalStartNode": "原始起始節點", "blocks.parameter-extractor": "參數提取器", "blocks.question-classifier": "問題分類器", "blocks.start": "開始", + "blocks.start-placeholder": "工作流程開始", "blocks.template-transform": "模板轉換", "blocks.tool": "工具", "blocks.trigger-plugin": "插件觸發器", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "利用 LLM 從自然語言內推理提取出結構化參數,用於後置的工具調用或 HTTP 請求。", "blocksAbout.question-classifier": "定義用戶問題的分類條件,LLM 能夠根據分類描述定義對話的進展方式", "blocksAbout.start": "定義一個 workflow 流程啟動的參數", + "blocksAbout.start-placeholder": "選擇此工作流程的啟動方式", "blocksAbout.template-transform": "使用 Jinja 模板語法將資料轉換為字符串", "blocksAbout.tool": "使用外部工具來擴展工作流程功能", "blocksAbout.trigger-plugin": "第三方整合觸發器,從外部平台事件啟動工作流程", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "消息類型", "nodes.start.outputVars.query": "用戶輸入", "nodes.start.required": "必填", + "nodes.start.userInputTipDescription": "定義工作流程按需啟動時要向終端使用者收集的輸入。", + "nodes.startPlaceholder.browseMoreOnMarketplace": "在 Marketplace 瀏覽更多", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "在 Marketplace 中尋找更多工具", + "nodes.startPlaceholder.noTriggersFound": "找不到觸發器", + "nodes.startPlaceholder.nodeCollapsedDescription": "點擊以設定開始節點", + "nodes.startPlaceholder.nodeDescription": "從右側面板選擇開始節點", + "nodes.startPlaceholder.nodeTitle": "工作流程開始", + "nodes.startPlaceholder.panelDescription": "開始節點定義工作流程的觸發方式", + "nodes.startPlaceholder.panelTitle": "選擇開始節點", + "nodes.startPlaceholder.userInputConflictTip": "使用者輸入不能與其他觸發器組合使用", + "nodes.startPlaceholder.validationRequired": "請先選擇開始節點。", "nodes.templateTransform.code": "模板程式碼", "nodes.templateTransform.codeSupportTip": "只支持 Jinja2", "nodes.templateTransform.inputVars": "輸入變數", @@ -1209,9 +1223,11 @@ "tabs.sources": "來源", "tabs.start": "開始", "tabs.startDisabledTip": "觸發節點與使用者輸入節點是互斥的。", + "tabs.startDisabledTipLearnMore": "了解更多開始節點", "tabs.startNotSupportedTip": "代码段中不支持“开始”选项卡。", "tabs.tools": "工具", "tabs.transform": "轉換", + "tabs.unconfiguredStartDisabledTip": "畫布上已有未設定的開始節點。請先完成設定後再繼續。", "tabs.usePlugin": "選取工具", "tabs.utilities": "工具", "tabs.workflowTool": "工作流", From c5ab38b2ad5a06dddeb788623f9fe5076af05d81 Mon Sep 17 00:00:00 2001 From: KVOJJJin Date: Fri, 12 Jun 2026 14:19:23 +0800 Subject: [PATCH 041/122] chore(web): support separate public API target for dev proxy (#37363) --- web/.env.example | 2 ++ web/dev-proxy.config.ts | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/web/.env.example b/web/.env.example index 77beee14174..762db54dafa 100644 --- a/web/.env.example +++ b/web/.env.example @@ -23,6 +23,8 @@ NEXT_PUBLIC_SOCKET_URL=ws://localhost:5001 # Dev proxy routes are configured in web/dev-proxy.config.ts. # pnpm -C web run dev:proxy loads web/.env.local before evaluating that config file. DEV_PROXY_TARGET=https://cloud.dify.ai +# Defaults to DEV_PROXY_TARGET when omitted. Set this when Web App public APIs use a different origin. +DEV_PROXY_PUBLIC_TARGET=https://udify.app DEV_PROXY_HOST=127.0.0.1 DEV_PROXY_PORT=5001 diff --git a/web/dev-proxy.config.ts b/web/dev-proxy.config.ts index c3d1528fb0e..bbd43f82303 100644 --- a/web/dev-proxy.config.ts +++ b/web/dev-proxy.config.ts @@ -3,6 +3,7 @@ import type { CookieRewriteOptions, DevProxyConfig } from '@langgenius/dev-proxy const DIFY_CLOUD_TARGET = 'https://cloud.dify.ai' const DEV_PROXY_TARGET = process.env.DEV_PROXY_TARGET || DIFY_CLOUD_TARGET const DEV_PROXY_ENTERPRISE_TARGET = process.env.DEV_PROXY_ENTERPRISE_TARGET || DEV_PROXY_TARGET +const DEV_PROXY_PUBLIC_TARGET = process.env.DEV_PROXY_PUBLIC_TARGET || DEV_PROXY_TARGET const DEV_PROXY_HOST = process.env.DEV_PROXY_HOST || '127.0.0.1' const DEV_PROXY_PORT = Number(process.env.DEV_PROXY_PORT || 5001) @@ -46,10 +47,16 @@ export default { { paths: [ '/console/api', - '/api', ], target: DEV_PROXY_TARGET, cookieRewrite: difyCookieRewrite, }, + { + paths: [ + '/api', + ], + target: DEV_PROXY_PUBLIC_TARGET, + cookieRewrite: difyCookieRewrite, + }, ], } satisfies DevProxyConfig From 07eb4903b8f0cffedf83426d13276559790e6890 Mon Sep 17 00:00:00 2001 From: L1nSn0w Date: Fri, 12 Jun 2026 14:35:15 +0800 Subject: [PATCH 042/122] feat: 429 rate-limit handling on the unified ErrorBody contract (openapi + difyctl) (#37313) Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com> --- api/configs/feature/__init__.py | 5 +- api/controllers/openapi/app_run.py | 15 +- api/libs/rate_limit.py | 26 +- .../openapi/test_app_run_rate_limit.py | 29 +++ .../openapi/test_error_contract.py | 2 + .../unit_tests/libs/test_external_api.py | 18 ++ .../unit_tests/libs/test_rate_limit_bearer.py | 21 +- cli/src/api/app-run.ts | 2 + .../app/_strategies/streaming-structured.ts | 2 +- .../run/app/_strategies/streaming-text.ts | 2 +- cli/src/commands/run/app/index.ts | 2 + cli/src/commands/run/app/run.ts | 1 + cli/src/errors/codes.test.ts | 2 + cli/src/errors/codes.ts | 4 + cli/src/http/client.test.ts | 242 +++++++++++++++++- cli/src/http/client.ts | 46 +++- cli/src/http/error-mapper.test.ts | 16 ++ cli/src/http/error-mapper.ts | 10 + cli/src/http/rate-limit.test.ts | 91 +++++++ cli/src/http/rate-limit.ts | 90 +++++++ cli/src/http/retry.test.ts | 18 +- cli/src/http/retry.ts | 10 +- cli/src/http/types.ts | 5 + cli/test/fixtures/dify-mock/server.ts | 3 +- 24 files changed, 638 insertions(+), 24 deletions(-) create mode 100644 api/tests/unit_tests/controllers/openapi/test_app_run_rate_limit.py create mode 100644 cli/src/http/rate-limit.test.ts create mode 100644 cli/src/http/rate-limit.ts diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index 109ce749a64..dc8c840da9c 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -943,9 +943,10 @@ class AuthConfig(BaseSettings): default=True, ) - OPENAPI_RATE_LIMIT_PER_TOKEN: PositiveInt = Field( + OPENAPI_RATE_LIMIT_PER_TOKEN: NonNegativeInt = Field( description="Per-token rate limit on /openapi/v1/* (requests per minute). " - "Bucket keyed on sha256(token), shared across api replicas via Redis.", + "Bucket keyed on sha256(token), shared across api replicas via Redis. " + "Set to 0 to disable the per-token limit entirely.", default=60, ) diff --git a/api/controllers/openapi/app_run.py b/api/controllers/openapi/app_run.py index d801f5183f1..3101b2de421 100644 --- a/api/controllers/openapi/app_run.py +++ b/api/controllers/openapi/app_run.py @@ -8,7 +8,14 @@ from contextlib import contextmanager from typing import Any from flask_restx import Resource -from werkzeug.exceptions import BadRequest, HTTPException, InternalServerError, NotFound, UnprocessableEntity +from werkzeug.exceptions import ( + BadRequest, + HTTPException, + InternalServerError, + NotFound, + TooManyRequests, + UnprocessableEntity, +) import services from controllers.openapi import openapi_ns @@ -29,6 +36,7 @@ from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpErr from core.app.apps.base_app_queue_manager import AppQueueManager from core.app.entities.app_invoke_entities import InvokeFrom from core.errors.error import ( + AppInvokeQuotaExceededError, ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError, @@ -71,6 +79,11 @@ def _translate_service_errors() -> Iterator[None]: raise ProviderQuotaExceededError() except ModelCurrentlyNotSupportError: raise ProviderModelCurrentlyNotSupportError() + except AppInvokeQuotaExceededError: + # App concurrency limit. Without this it falls through to the bare `except Exception` + # below and surfaces as a 500. Render as the canonical 429 (code "too_many_requests"); + # the source message is dropped since it carries internal detail (client_id / limits). + raise TooManyRequests() except InvokeRateLimitError as ex: raise InvokeRateLimitHttpError(ex.description) except InvokeError as e: diff --git a/api/libs/rate_limit.py b/api/libs/rate_limit.py index 68147f21cfe..13b27eae9f7 100644 --- a/api/libs/rate_limit.py +++ b/api/libs/rate_limit.py @@ -15,7 +15,7 @@ from enum import StrEnum from functools import wraps from typing import ParamSpec, TypeVar -from flask import jsonify, make_response, request, session +from flask import request, session from werkzeug.exceptions import TooManyRequests from configs import dify_config @@ -132,20 +132,32 @@ def enforce(spec: RateLimit, *, key: str) -> None: limiter.increment_rate_limit(key) +class _BearerRateLimited(TooManyRequests): + """Per-token 429. Carries Retry-After as a plain ``headers`` attribute because the openapi + error formatter reads ``getattr(e, "headers")`` (not werkzeug's ``get_headers()``). No + pre-built response, so the formatter still renders the canonical ErrorBody ("too_many_requests"). + """ + + headers: dict[str, str] + + def __init__(self, retry_after_seconds: int) -> None: + super().__init__() + self.headers = {"Retry-After": str(retry_after_seconds)} + + def enforce_bearer_rate_limit(token_hash: str) -> None: """Per-token rate limit on /openapi/v1/* bearer-authed routes. Bucket key = ``token:`` so the same token shares one bucket across api replicas (Redis-backed sliding window). """ + # 0 (or less) disables the per-token limit. Short-circuit here: a limiter built with + # max_attempts=0 would otherwise treat every request as already over the limit. + if LIMIT_BEARER_PER_TOKEN.limit <= 0: + return limiter = _build_limiter(LIMIT_BEARER_PER_TOKEN) key = f"token:{token_hash}" if limiter.is_rate_limited(key): retry_after = limiter.seconds_until_available(key) - response = make_response( - jsonify({"error": "rate_limited", "retry_after_ms": retry_after * 1000}), - 429, - ) - response.headers["Retry-After"] = str(retry_after) - raise TooManyRequests(response=response) + raise _BearerRateLimited(retry_after) limiter.increment_rate_limit(key) diff --git a/api/tests/unit_tests/controllers/openapi/test_app_run_rate_limit.py b/api/tests/unit_tests/controllers/openapi/test_app_run_rate_limit.py new file mode 100644 index 00000000000..d9c468d0a4e --- /dev/null +++ b/api/tests/unit_tests/controllers/openapi/test_app_run_rate_limit.py @@ -0,0 +1,29 @@ +"""The openapi run boundary maps internal rate-limit exceptions to canonical 429s. + +Both render through the ErrorBody formatter: TooManyRequests -> code "too_many_requests" +(retryable throttle), InvokeRateLimitError -> code "rate_limit_error" (quota). +""" + +import pytest +from werkzeug.exceptions import TooManyRequests + +from controllers.openapi.app_run import _translate_service_errors +from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError +from core.errors.error import AppInvokeQuotaExceededError +from services.errors.llm import InvokeRateLimitError + + +def test_translate_maps_app_concurrency_to_too_many_requests(): + # Regression guard: this used to fall through to a 500 (it was not caught here). + with pytest.raises(TooManyRequests) as exc: + with _translate_service_errors(): + raise AppInvokeQuotaExceededError("internal: client_id=abc max=10") + assert exc.value.code == 429 + + +def test_translate_maps_workflow_quota_to_rate_limit_error(): + with pytest.raises(InvokeRateLimitHttpError) as exc: + with _translate_service_errors(): + raise InvokeRateLimitError("workflow quota exhausted") + assert exc.value.error_code == "rate_limit_error" + assert exc.value.code == 429 diff --git a/api/tests/unit_tests/controllers/openapi/test_error_contract.py b/api/tests/unit_tests/controllers/openapi/test_error_contract.py index 12293de321d..45a577443b7 100644 --- a/api/tests/unit_tests/controllers/openapi/test_error_contract.py +++ b/api/tests/unit_tests/controllers/openapi/test_error_contract.py @@ -10,6 +10,7 @@ from werkzeug.exceptions import ( Forbidden, InternalServerError, NotFound, + TooManyRequests, Unauthorized, UnprocessableEntity, ) @@ -309,6 +310,7 @@ ERROR_MATRIX = [ (ProviderModelCurrentlyNotSupportError(), 400, "model_currently_not_support"), (CompletionRequestError(), 400, "completion_request_error"), (InvokeRateLimitHttpError(), 429, "rate_limit_error"), + (TooManyRequests("x"), 429, "too_many_requests"), # difyctl's classifyRateLimit keys retryability on this code (FileTooLargeError(), 413, "file_too_large"), (UnsupportedFileTypeError(), 415, "unsupported_file_type"), (NoFileUploadedError(), 400, "no_file_uploaded"), diff --git a/api/tests/unit_tests/libs/test_external_api.py b/api/tests/unit_tests/libs/test_external_api.py index 5135970bcc5..7ebdf5f60eb 100644 --- a/api/tests/unit_tests/libs/test_external_api.py +++ b/api/tests/unit_tests/libs/test_external_api.py @@ -6,6 +6,7 @@ from constants import COOKIE_NAME_ACCESS_TOKEN, COOKIE_NAME_CSRF_TOKEN, COOKIE_N from core.errors.error import AppInvokeQuotaExceededError from libs.exception import BaseHTTPException from libs.external_api import ExternalApi +from libs.rate_limit import _BearerRateLimited def _create_api_app(): @@ -58,6 +59,13 @@ def _create_api_app(): e.description = {"field": "is required"} raise e + # The per-token rate limit raises this; ExternalApi must carry its Retry-After header to the + # wire (it reads getattr(e, "headers"), not werkzeug's get_headers()). + @api.route("/rate-limited") + class RateLimited(Resource): + def get(self): + raise _BearerRateLimited(23) + app.register_blueprint(bp, url_prefix="/api") return app @@ -115,6 +123,16 @@ def test_external_api_param_mapping_and_quota(): assert res.status_code in (400, 429) +def test_external_api_carries_exception_headers_to_429_response(): + # Locks the coupling enforce_bearer_rate_limit relies on: handle_error reads getattr(e, + # "headers") and puts it on the response, so Retry-After reaches the wire. + app = _create_api_app() + res = app.test_client().get("/api/rate-limited") + assert res.status_code == 429 + assert res.headers["Retry-After"] == "23" + assert (res.get_json() or {})["status"] == 429 + + def test_unauthorized_and_force_logout_clears_cookies(): """Test that UnauthorizedAndForceLogout error clears auth cookies""" diff --git a/api/tests/unit_tests/libs/test_rate_limit_bearer.py b/api/tests/unit_tests/libs/test_rate_limit_bearer.py index b204575ccb8..62363f5f600 100644 --- a/api/tests/unit_tests/libs/test_rate_limit_bearer.py +++ b/api/tests/unit_tests/libs/test_rate_limit_bearer.py @@ -11,6 +11,8 @@ from werkzeug.exceptions import TooManyRequests from libs.helper import RateLimiter from libs.rate_limit import ( LIMIT_BEARER_PER_TOKEN, + RateLimit, + RateLimitScope, enforce_bearer_rate_limit, ) @@ -67,8 +69,17 @@ def test_enforce_bearer_rate_limit_raises_429_with_retry_after(mock_build): mock_build.return_value = limiter with pytest.raises(TooManyRequests) as exc: enforce_bearer_rate_limit("hash-1") - headers = dict(exc.value.get_response().headers) - assert headers.get("Retry-After") == "23" - body = exc.value.get_response().get_json() or {} - assert body.get("error") == "rate_limited" - assert body.get("retry_after_ms") == 23000 + # Header-only TooManyRequests: the canonical ErrorBody (code "too_many_requests") is built + # later by the openapi formatter; here we only assert the advisory header rides along. + assert dict(exc.value.headers).get("Retry-After") == "23" + + +@patch("libs.rate_limit._build_limiter") +def test_enforce_bearer_rate_limit_disabled_when_limit_is_zero(mock_build, monkeypatch): + # 0 disables the limit — short-circuit before building/consulting a limiter. + monkeypatch.setattr( + "libs.rate_limit.LIMIT_BEARER_PER_TOKEN", + RateLimit(limit=0, window=timedelta(minutes=1), scopes=(RateLimitScope.TOKEN_ID,)), + ) + enforce_bearer_rate_limit("hash-1") + mock_build.assert_not_called() diff --git a/cli/src/api/app-run.ts b/cli/src/api/app-run.ts index 1cb64057f8e..cbd36de2049 100644 --- a/cli/src/api/app-run.ts +++ b/cli/src/api/app-run.ts @@ -34,6 +34,7 @@ export function buildRunBody(args: RunBodyArgs): Record { export type StreamOptions = { signal?: AbortSignal includeStateSnapshot?: boolean + retryOnRateLimit?: boolean } export class AppRunClient { @@ -59,6 +60,7 @@ export class AppRunClient { headers: { Accept: 'text/event-stream' }, signal: opts.signal, throwOnError: true, + retryOnRateLimit: opts.retryOnRateLimit, }) if (res.body === null) throw new Error('streaming response body missing') diff --git a/cli/src/commands/run/app/_strategies/streaming-structured.ts b/cli/src/commands/run/app/_strategies/streaming-structured.ts index 182aa89d080..c6f02292528 100644 --- a/cli/src/commands/run/app/_strategies/streaming-structured.ts +++ b/cli/src/commands/run/app/_strategies/streaming-structured.ts @@ -56,7 +56,7 @@ export class StreamingStructuredStrategy implements RunStrategy { let resp: Record try { - const events = await ctx.runClient.runStream(opts.appId, body, { signal: ctrl.signal }) + const events = await ctx.runClient.runStream(opts.appId, body, { signal: ctrl.signal, retryOnRateLimit: opts.retryOnRateLimit }) const wrappedEvents = captureTaskId(events, (id) => { taskId = id }) diff --git a/cli/src/commands/run/app/_strategies/streaming-text.ts b/cli/src/commands/run/app/_strategies/streaming-text.ts index 872acc785bc..66b2c2a88ec 100644 --- a/cli/src/commands/run/app/_strategies/streaming-text.ts +++ b/cli/src/commands/run/app/_strategies/streaming-text.ts @@ -28,7 +28,7 @@ export class StreamingTextStrategy implements RunStrategy { handle('SIGINT', cleanup) try { - const events = await ctx.runClient.runStream(opts.appId, body, { signal: ctrl.signal }) + const events = await ctx.runClient.runStream(opts.appId, body, { signal: ctrl.signal, retryOnRateLimit: opts.retryOnRateLimit }) const sp = streamPrinterFor(mode, ctx.think, deps.io.isErrTTY) const dec = new TextDecoder() for await (const ev of events) { diff --git a/cli/src/commands/run/app/index.ts b/cli/src/commands/run/app/index.ts index 16bd30c069f..44ea93c542b 100644 --- a/cli/src/commands/run/app/index.ts +++ b/cli/src/commands/run/app/index.ts @@ -35,6 +35,7 @@ export default class RunApp extends DifyCommand { 'workspace': Flags.string({ description: 'Workspace id (overrides DIFY_WORKSPACE_ID and stored default)' }), 'stream': Flags.boolean({ description: 'Print output live as tokens/events arrive (default: collect and print at end)', default: false }), 'think': Flags.boolean({ description: 'Show model thinking/reasoning when available. Strips ... blocks silently by default; with --think, thinking is printed to stderr.', default: false }), + 'retry-on-limit': Flags.boolean({ description: 'On a 429 rate limit, wait and retry this POST (bounded) instead of failing immediately. Off by default since running an app is not idempotent.', default: false }), 'http-retry': httpRetryFlag, 'output': Flags.outputFormat({ options: [OutputFormat.JSON, OutputFormat.YAML, OutputFormat.TEXT], default: '' }), } @@ -56,6 +57,7 @@ export default class RunApp extends DifyCommand { format, stream: flags.stream, think: flags.think, + retryOnRateLimit: flags['retry-on-limit'], }, { active: ctx.active, http: ctx.http, host: ctx.host, io: ctx.io, cache: ctx.cache }, ) diff --git a/cli/src/commands/run/app/run.ts b/cli/src/commands/run/app/run.ts index ada582b48b3..8eb767c5dbe 100644 --- a/cli/src/commands/run/app/run.ts +++ b/cli/src/commands/run/app/run.ts @@ -27,6 +27,7 @@ export type RunAppOptions = { readonly format?: string readonly stream?: boolean readonly think?: boolean + readonly retryOnRateLimit?: boolean } export type RunAppDeps = { diff --git a/cli/src/errors/codes.test.ts b/cli/src/errors/codes.test.ts index a29697f57a7..eb76b13a22f 100644 --- a/cli/src/errors/codes.test.ts +++ b/cli/src/errors/codes.test.ts @@ -18,6 +18,7 @@ describe('error codes', () => { expect(ExitCode.Usage).toBe(2) expect(ExitCode.Auth).toBe(4) expect(ExitCode.VersionCompat).toBe(6) + expect(ExitCode.RateLimited).toBe(7) }) it('every code maps to an exit', () => { @@ -46,6 +47,7 @@ describe('error codes', () => { [ErrorCode.Server4xxOther, ExitCode.Generic], [ErrorCode.ClientError, ExitCode.Generic], [ErrorCode.Unknown, ExitCode.Generic], + [ErrorCode.RateLimited, ExitCode.RateLimited], ])('exitFor(%s) -> %d', (code, want) => { expect(exitFor(code)).toBe(want) }) diff --git a/cli/src/errors/codes.ts b/cli/src/errors/codes.ts index cc0d09b745e..e2b16cb3619 100644 --- a/cli/src/errors/codes.ts +++ b/cli/src/errors/codes.ts @@ -12,6 +12,7 @@ export const ErrorCode = { ConfigInvalidKey: 'config_invalid_key', ConfigInvalidValue: 'config_invalid_value', NetworkConnection: 'network_connection', + RateLimited: 'rate_limited', Server5xx: 'server_5xx', Server4xxOther: 'server_4xx_other', ClientError: 'client_error', @@ -28,6 +29,8 @@ export const ExitCode = { Usage: 2, Auth: 4, VersionCompat: 6, + // Distinct from Generic so wrappers can tell "rate limited, retry later" from a hard failure. + RateLimited: 7, } as const export type ExitCodeValue = (typeof ExitCode)[keyof typeof ExitCode] @@ -46,6 +49,7 @@ const CODE_TO_EXIT: Readonly> = { config_invalid_key: ExitCode.Usage, config_invalid_value: ExitCode.Usage, network_connection: ExitCode.Generic, + rate_limited: ExitCode.RateLimited, server_5xx: ExitCode.Generic, server_4xx_other: ExitCode.Generic, client_error: ExitCode.Generic, diff --git a/cli/src/http/client.test.ts b/cli/src/http/client.test.ts index fbde1ecdcab..ae4448843a4 100644 --- a/cli/src/http/client.test.ts +++ b/cli/src/http/client.test.ts @@ -192,7 +192,7 @@ describe('http client', () => { expect(caught.code).toBe(ErrorCode.Server4xxOther) }) - it('handles 429 via retry status code list', async () => { + it('surfaces a 429 as a rate-limit error (dedicated exit code), no retry when budget is 0', async () => { mock.setScenario('rate-limited') const client = createHttpClient({ baseURL: base(mock.url), @@ -206,8 +206,246 @@ describe('http client', () => { } catch (err) { caught = err } expect(isHttpClientError(caught)).toBe(true) - if (isHttpClientError(caught)) + if (isHttpClientError(caught)) { expect(caught.httpStatus).toBe(429) + expect(caught.code).toBe(ErrorCode.RateLimited) + expect(caught.exit()).toBe(7) + expect(caught.serverError?.code).toBe('too_many_requests') + } + }) + + it('retries an idempotent GET on a throttle 429, then succeeds', async () => { + let calls = 0 + const stub = await startStub((_req, res) => { + calls++ + if (calls === 1) { + res.writeHead(429, { 'content-type': 'application/json' }) + res.end(JSON.stringify({ code: 'too_many_requests', message: 'slow down', status: 429 })) + return + } + res.writeHead(200, { 'content-type': 'application/json' }) + res.end(JSON.stringify({ workspaces: [] })) + }) + const events: { phase: string, status?: number, delayMs?: number }[] = [] + try { + const client = createHttpClient({ + baseURL: base(stub.url), + bearer: 'dfoa_test', + retryAttempts: 3, + timeoutMs: 5_000, + logger: e => events.push({ phase: e.phase, status: e.status, delayMs: e.delayMs }), + }) + const body = await client.get<{ workspaces: unknown[] }>('workspaces') + expect(body.workspaces).toEqual([]) + } + finally { + await stub.stop() + } + expect(calls).toBe(2) + const retry = events.find(e => e.phase === 'retry') + expect(retry?.status).toBe(429) + expect(retry?.delayMs).toBeGreaterThan(0) + }) + + it('does not retry a quota 429 (rate_limit_error) — surfaces immediately', async () => { + let requests = 0 + const stub = await startStub((_req, res) => { + res.writeHead(429, { 'content-type': 'application/json' }) + res.end(JSON.stringify({ code: 'rate_limit_error', message: 'quota exhausted', status: 429 })) + }) + let caught: unknown + try { + const client = createHttpClient({ + baseURL: base(stub.url), + bearer: 'dfoa_test', + retryAttempts: 3, + timeoutMs: 5_000, + logger: (e) => { + if (e.phase === 'request') + requests++ + }, + }) + try { + await client.get('workspaces') + } + catch (err) { caught = err } + } + finally { + await stub.stop() + } + expect(requests).toBe(1) + expect(isHttpClientError(caught)).toBe(true) + if (isHttpClientError(caught)) { + expect(caught.code).toBe(ErrorCode.RateLimited) + expect(caught.serverError?.code).toBe('rate_limit_error') + } + }) + + it('does not retry a POST 429 by default; retries with retry-on-limit', async () => { + let postDefault = 0 + const stubDefault = await startStub((_req, res) => { + postDefault++ + res.writeHead(429, { 'content-type': 'application/json' }) + res.end(JSON.stringify({ code: 'too_many_requests', message: 'slow down', status: 429 })) + }) + try { + const client = createHttpClient({ baseURL: base(stubDefault.url), bearer: 'dfoa_test', retryAttempts: 3, timeoutMs: 5_000 }) + await expect(client.post('apps/app-1/run', { json: { inputs: {} } })).rejects.toBeDefined() + } + finally { + await stubDefault.stop() + } + expect(postDefault).toBe(1) + + let calls = 0 + const stubOptIn = await startStub((_req, res) => { + calls++ + if (calls === 1) { + res.writeHead(429, { 'content-type': 'application/json' }) + res.end(JSON.stringify({ code: 'too_many_requests', message: 'slow down', status: 429 })) + return + } + res.writeHead(200, { 'content-type': 'application/json' }) + res.end(JSON.stringify({ ok: true })) + }) + try { + const client = createHttpClient({ baseURL: base(stubOptIn.url), bearer: 'dfoa_test', retryAttempts: 3, timeoutMs: 5_000 }) + const body = await client.post<{ ok: boolean }>('apps/app-1/run', { json: { inputs: {} }, retryOnRateLimit: true }) + expect(body.ok).toBe(true) + } + finally { + await stubOptIn.stop() + } + expect(calls).toBe(2) + }) + + it('still never retries a POST 5xx even with retry-on-limit (idempotency guard)', async () => { + let requests = 0 + const stub = await startStub((_req, res) => { + requests++ + res.writeHead(503, { 'content-type': 'application/json' }) + res.end(JSON.stringify({ code: 'internal_server_error', message: 'boom', status: 503 })) + }) + try { + const client = createHttpClient({ baseURL: base(stub.url), bearer: 'dfoa_test', retryAttempts: 3, timeoutMs: 5_000 }) + await expect(client.post('apps/app-1/run', { json: { inputs: {} }, retryOnRateLimit: true })).rejects.toBeDefined() + } + finally { + await stub.stop() + } + expect(requests).toBe(1) + }) + + it('surfaces a RateLimited error after exhausting 429 retries on GET', async () => { + let requests = 0 + let retries = 0 + const stub = await startStub((_req, res) => { + res.writeHead(429, { 'content-type': 'application/json' }) + res.end(JSON.stringify({ code: 'too_many_requests', message: 'slow down', status: 429 })) + }) + let caught: unknown + try { + const client = createHttpClient({ + baseURL: base(stub.url), + bearer: 'dfoa_test', + retryAttempts: 2, + timeoutMs: 5_000, + logger: (e) => { + if (e.phase === 'request') + requests++ + else if (e.phase === 'retry') + retries++ + }, + }) + try { + await client.get('workspaces') + } + catch (err) { caught = err } + } + finally { + await stub.stop() + } + expect(requests).toBe(3) + expect(retries).toBe(2) + expect(isHttpClientError(caught)).toBe(true) + if (isHttpClientError(caught)) + expect(caught.code).toBe(ErrorCode.RateLimited) + }) + + it('surfaces a throttle 429 whose Retry-After exceeds the honored cap (no retry)', async () => { + let requests = 0 + const stub = await startStub((_req, res) => { + // 120s advised wait > MAX_HONORED_WAIT_MS (60s) — surface rather than park for minutes. + res.writeHead(429, { 'content-type': 'application/json', 'retry-after': '120' }) + res.end(JSON.stringify({ code: 'too_many_requests', message: 'slow down', status: 429 })) + }) + let caught: unknown + try { + const client = createHttpClient({ + baseURL: base(stub.url), + bearer: 'dfoa_test', + retryAttempts: 3, + timeoutMs: 5_000, + logger: (e) => { + if (e.phase === 'request') + requests++ + }, + }) + try { + await client.get('workspaces') + } + catch (err) { caught = err } + } + finally { + await stub.stop() + } + expect(requests).toBe(1) + expect(isHttpClientError(caught)).toBe(true) + if (isHttpClientError(caught)) + expect(caught.code).toBe(ErrorCode.RateLimited) + }) + + it('stream GET surfaces a 429 (returns the Response, no throw, no retry)', async () => { + let calls = 0 + const stub = await startStub((_req, res) => { + calls++ + res.writeHead(429, { 'content-type': 'application/json' }) + res.end(JSON.stringify({ code: 'too_many_requests', message: 'slow down', status: 429 })) + }) + try { + const client = createHttpClient({ baseURL: base(stub.url), bearer: 'dfoa_test', timeoutMs: 5_000 }) + const res = await client.stream('workspaces') + expect(res.status).toBe(429) + await res.body?.cancel() + } + finally { + await stub.stop() + } + expect(calls).toBe(1) + }) + + it('stream POST retries a throttle 429 when retry-on-limit is set', async () => { + let calls = 0 + const stub = await startStub((_req, res) => { + calls++ + if (calls === 1) { + res.writeHead(429, { 'content-type': 'application/json' }) + res.end(JSON.stringify({ code: 'too_many_requests', message: 'slow down', status: 429 })) + return + } + res.writeHead(200, { 'content-type': 'text/event-stream' }) + res.end('data: {}\n\n') + }) + try { + const client = createHttpClient({ baseURL: base(stub.url), bearer: 'dfoa_test', timeoutMs: 5_000 }) + const res = await client.stream('apps/app-1/run', { method: 'POST', json: {}, retryOnRateLimit: true }) + expect(res.status).toBe(200) + await res.body?.cancel() + } + finally { + await stub.stop() + } + expect(calls).toBe(2) }) it('does not retry POST on 503', async () => { diff --git a/cli/src/http/client.ts b/cli/src/http/client.ts index f099b012259..940aee9152b 100644 --- a/cli/src/http/client.ts +++ b/cli/src/http/client.ts @@ -15,7 +15,8 @@ import { buildBody } from './body.js' import { classifyResponse } from './error-mapper.js' import { classifyTransport, logRequest, logResponse, setBearer, setUserAgent } from './hooks.js' import { proxyDispatcher } from './proxy.js' -import { backoffDelay, shouldRetry } from './retry.js' +import { classifyRateLimit, MAX_HONORED_WAIT_MS, RATE_LIMIT_MAX_ATTEMPTS, rateLimitDelayMs } from './rate-limit.js' +import { backoffDelay, isIdempotentRetryMethod, shouldRetry } from './retry.js' import { redactBearer } from './sanitize.js' import { appendSearchParams, joinURL } from './url.js' @@ -133,6 +134,7 @@ function buildRequest(state: ClientState, path: string, opts: RequestOptions, th timeoutMs: effectiveTimeoutMs, retryAttempts: effectiveRetryAttempts, throwOnError, + retryOnRateLimit: opts.retryOnRateLimit ?? false, } return { request, resolved, effectiveTimeoutMs, userSignal: opts.signal } } @@ -204,6 +206,37 @@ async function execute( const res = ctx.response if (!res.ok) { + // 429 has its own policy. The server self-describes via the ErrorBody `code`: a + // "too_many_requests" throttle waits-and-retries on idempotent methods (or opted-in POSTs) + // honoring Retry-After; quota / unrecognized 429s surface immediately rather than burning + // retries. Surfacing reuses the shared classifyResponse so the body parses to ErrorBody. + if (res.status === 429) { + const decision = await classifyRateLimit(res.clone()) + const canRetry + = decision.retryable + && attempt < effectiveRetryAttempts + && (decision.retryAfterMs === undefined || decision.retryAfterMs <= MAX_HONORED_WAIT_MS) + && (isIdempotentRetryMethod(method) || (method === 'POST' && resolved.retryOnRateLimit)) + if (canRetry) { + const delay = rateLimitDelayMs(decision, attempt + 1) + state.logger?.({ phase: 'retry', method, url: redactBearer(ctx.request.url), status: 429, attempt: attempt + 1, delayMs: delay }) + await res.body?.cancel().catch(() => {}) + if (delay > 0) + await new Promise(resolve => setTimeout(resolve, delay)) + continue + } + + ctx.error = await classifyResponse(ctx.request, res) + await runHooks(state.hooks.onResponseError, ctx) + if (throwOnError) { + const finalErr = ctx.error + if (finalErr instanceof Error && typeof Error.captureStackTrace === 'function') + Error.captureStackTrace(finalErr, execute) + throw finalErr + } + return res + } + if (attempt < effectiveRetryAttempts && shouldRetry(res, ctx)) { state.logger?.({ phase: 'retry', method, url: redactBearer(ctx.request.url), attempt: attempt + 1 }) // Drain the discarded error body so undici can release the socket back to its @@ -259,10 +292,18 @@ export function createHttpClient(opts: ClientOptions): HttpClient { const streamFetch = (path: string, callOpts?: RequestOptions): Promise => { // SSE bodies must not be aborted by a request-level timeout — `0` is the buildRequest // sentinel for "no timeout" and also overrides the client default. + // + // A stream normally never retries (a mid-stream replay would double-send). When the caller + // opts into 429 retry, allow a bounded budget: the 429 admission rejection arrives as a plain + // body before the stream opens, and execute()'s 429 branch is the only path that fires for a + // POST — shouldRetry still rejects POST for transport / 5xx, so nothing else replays. + const retryAttempts = callOpts?.retryOnRateLimit === true + ? (callOpts.retryAttempts ?? RATE_LIMIT_MAX_ATTEMPTS) + : 0 const finalOpts: RequestOptions = { ...callOpts, method: callOpts?.method ?? 'GET', - retryAttempts: 0, + retryAttempts, timeoutMs: 0, } const built = buildRequest(state, path, finalOpts, false) @@ -284,6 +325,7 @@ export function createHttpClient(opts: ClientOptions): HttpClient { timeoutMs: state.defaultTimeoutMs, retryAttempts: state.defaultRetryAttempts, throwOnError: false, + retryOnRateLimit: false, } const userSignal = init?.signal ?? req.signal return execute(state, req, resolved, state.defaultTimeoutMs, userSignal) diff --git a/cli/src/http/error-mapper.test.ts b/cli/src/http/error-mapper.test.ts index ab0397cce73..0a487723352 100644 --- a/cli/src/http/error-mapper.test.ts +++ b/cli/src/http/error-mapper.test.ts @@ -54,6 +54,22 @@ describe('classifyResponse — canonical ErrorBody', () => { expect(err.code).toBe(ErrorCode.Server4xxOther) expect(err.serverError?.code).toBe('some_future_code') }) + + it('429 classifies as RateLimited (dedicated exit code) and keeps the server code', async () => { + const err = await classified(429, { code: 'too_many_requests', message: 'slow down', status: 429 }) + + expect(err.code).toBe(ErrorCode.RateLimited) + expect(err.exit()).toBe(7) + expect(err.serverError?.code).toBe('too_many_requests') + }) + + it('429 with no parseable ErrorBody falls back to a generic rate-limit message', async () => { + const err = await classified(429, 'not json') + + expect(err.code).toBe(ErrorCode.RateLimited) + expect(err.serverError).toBeUndefined() + expect(err.message).toBe('too many requests') + }) }) describe('classifyResponse — non-conforming bodies (no fallback by design)', () => { diff --git a/cli/src/http/error-mapper.ts b/cli/src/http/error-mapper.ts index f2b3b1bf605..aca1a7e6184 100644 --- a/cli/src/http/error-mapper.ts +++ b/cli/src/http/error-mapper.ts @@ -36,9 +36,19 @@ const SERVER_4XX_CLASS: StatusClass = { includeRaw: true, } +// 429 gets a dedicated CLI code (its own exit code) so wrappers can tell a rate limit from a hard +// failure. The serverError.code ("too_many_requests" / "rate_limit_error") still rides along. +const RATE_LIMITED_CLASS: StatusClass = { + code: ErrorCode.RateLimited, + fallbackMessage: () => 'too many requests', + includeRaw: false, +} + function statusClass(status: number): StatusClass { if (status === 401) return AUTH_EXPIRED_CLASS + if (status === 429) + return RATE_LIMITED_CLASS if (status >= 500) return SERVER_5XX_CLASS return SERVER_4XX_CLASS diff --git a/cli/src/http/rate-limit.test.ts b/cli/src/http/rate-limit.test.ts new file mode 100644 index 00000000000..5b81bb679ae --- /dev/null +++ b/cli/src/http/rate-limit.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it } from 'vitest' +import { + classifyRateLimit, + MAX_HONORED_WAIT_MS, + parseRetryAfterMs, + RATE_LIMIT_MAX_ATTEMPTS, + rateLimitDelayMs, +} from './rate-limit.js' + +function res429(body: unknown, headers?: Record): Response { + return new Response(typeof body === 'string' ? body : JSON.stringify(body), { + status: 429, + headers: { 'content-type': 'application/json', ...headers }, + }) +} + +function headers(init?: Record): Headers { + return new Headers(init) +} + +describe('classifyRateLimit', () => { + it('throttle (code too_many_requests) is retryable and reads the Retry-After header', async () => { + const d = await classifyRateLimit(res429({ code: 'too_many_requests', status: 429 }, { 'retry-after': '2' })) + expect(d).toEqual({ retryable: true, retryAfterMs: 2000 }) + }) + + it('throttle without Retry-After is retryable with no advised wait', async () => { + const d = await classifyRateLimit(res429({ code: 'too_many_requests', status: 429 })) + expect(d).toEqual({ retryable: true, retryAfterMs: undefined }) + }) + + it('quota (code rate_limit_error) is not retryable', async () => { + const d = await classifyRateLimit(res429({ code: 'rate_limit_error', status: 429 }, { 'retry-after': '5' })) + expect(d.retryable).toBe(false) + expect(d.retryAfterMs).toBeUndefined() + }) + + it.each([ + ['unknown code', { code: 'mystery' }], + ['no code', { message: 'nope' }], + ['non-JSON body', 'not json'], + ])('unrecognized 429 (%s) is conservatively non-retryable', async (_label, body) => { + const d = await classifyRateLimit(res429(body)) + expect(d.retryable).toBe(false) + }) + + it('reads the body off a clone (response stays consumable)', async () => { + const r = res429({ code: 'too_many_requests', status: 429 }) + await classifyRateLimit(r) + await expect(r.text()).resolves.toContain('too_many_requests') + }) +}) + +describe('parseRetryAfterMs', () => { + it('reads integer-seconds Retry-After as ms', () => { + expect(parseRetryAfterMs(headers({ 'retry-after': '3' }))).toBe(3000) + }) + + it('reads an HTTP-date relative to the injected now, clamped at 0', () => { + const now = Date.parse('2026-06-11T00:00:00Z') + expect(parseRetryAfterMs(headers({ 'retry-after': 'Thu, 11 Jun 2026 00:00:05 GMT' }), now)).toBe(5000) + expect(parseRetryAfterMs(headers({ 'retry-after': 'Thu, 11 Jun 2026 00:00:00 GMT' }), now + 10_000)).toBe(0) + }) + + it('returns undefined when absent or unparseable', () => { + expect(parseRetryAfterMs(headers())).toBeUndefined() + expect(parseRetryAfterMs(headers({ 'retry-after': 'soon' }))).toBeUndefined() + }) +}) + +describe('rateLimitDelayMs', () => { + it('returns the advised wait as-is (the caller already declined over-cap waits)', () => { + expect(rateLimitDelayMs({ retryAfterMs: 800 }, 1)).toBe(800) + expect(rateLimitDelayMs({ retryAfterMs: 5000 }, 1)).toBe(5000) + }) + + it('falls back to equal-jitter backoff when no wait is advised (rng pinned)', () => { + // attempt 1 => backoffDelay 300; equal jitter => [150, 300]. + expect(rateLimitDelayMs({}, 1, { rng: () => 0 })).toBe(150) + expect(rateLimitDelayMs({}, 1, { rng: () => 1 })).toBe(300) + }) + + it('returns 0 when there is neither an advised wait nor any backoff (attempt 0)', () => { + expect(rateLimitDelayMs({}, 0, { rng: () => 0.5 })).toBe(0) + }) + + it('exposes sane retry constants', () => { + expect(MAX_HONORED_WAIT_MS).toBe(60_000) + expect(RATE_LIMIT_MAX_ATTEMPTS).toBe(3) + }) +}) diff --git a/cli/src/http/rate-limit.ts b/cli/src/http/rate-limit.ts new file mode 100644 index 00000000000..15115180e69 --- /dev/null +++ b/cli/src/http/rate-limit.ts @@ -0,0 +1,90 @@ +import { backoffDelay } from './retry.js' + +// Stateless handling for the server's 429s: react when one arrives, never predict or store limits. +// The server is self-describing via the unified ErrorBody `code`: +// "too_many_requests" → throttle, waiting helps (retryable) +// "rate_limit_error" → quota, waiting within the window does not (not retryable) +// anything else / unparsable → conservative: not retryable. + +export type RateLimitDecision = { + readonly retryable: boolean + // The advised wait, from the Retry-After header (only meaningful for a retryable throttle). + readonly retryAfterMs?: number +} + +// The longest server-advised wait we'll honor by retrying. If Retry-After is larger, the 429 +// branch surfaces immediately instead of parking the process for minutes (better to let the +// caller decide than to sleep through several capped retries that will just 429 again). +export const MAX_HONORED_WAIT_MS = 60_000 + +export const RATE_LIMIT_MAX_ATTEMPTS = 3 + +function bodyCode(raw: string): string | undefined { + try { + const parsed = JSON.parse(raw) as unknown + if (typeof parsed === 'object' && parsed !== null) { + const code = (parsed as Record).code + return typeof code === 'string' ? code : undefined + } + } + catch { + // not JSON + } + return undefined +} + +// Read a 429 response into a retry decision. Reads the ErrorBody `code` for retryability and +// the Retry-After header for the wait; both off a clone so the body stays consumable downstream. +export async function classifyRateLimit(response: Response): Promise { + let raw = '' + try { + raw = await response.clone().text() + } + catch { + // ignore read errors; raw stays '' + } + const retryable = bodyCode(raw) === 'too_many_requests' + return { retryable, retryAfterMs: retryable ? parseRetryAfterMs(response.headers) : undefined } +} + +// Parse the Retry-After header to ms: integer seconds, or an HTTP-date relative to `now` +// (injectable for deterministic tests). The unified ErrorBody carries no wait field of its own. +export function parseRetryAfterMs(headers: Headers, now: number = Date.now()): number | undefined { + const header = headers.get('retry-after') + if (header === null) { + return undefined + } + const trimmed = header.trim() + if (/^\d+$/.test(trimmed)) { + return Number(trimmed) * 1000 + } + const dateMs = Date.parse(trimmed) + if (!Number.isNaN(dateMs)) { + return Math.max(0, dateMs - now) + } + return undefined +} + +// Equal-jitter backoff around the exponential base: half fixed + half random. Avoids both the +// thundering-herd of a fixed delay and the near-zero spikes of full jitter. +function jitter(baseMs: number, rng: () => number): number { + if (baseMs <= 0) { + return 0 + } + const half = baseMs / 2 + return Math.round(half + rng() * half) +} + +// How long to wait before the next 429 retry: a known server wait takes precedence (the caller +// has already declined to retry waits beyond MAX_HONORED_WAIT_MS), otherwise jittered exponential +// backoff for sources that advise none (e.g. app concurrency). +export function rateLimitDelayMs( + decision: Pick, + attempt: number, + opts: { rng?: () => number } = {}, +): number { + if (decision.retryAfterMs !== undefined) { + return decision.retryAfterMs + } + return jitter(backoffDelay(attempt), opts.rng ?? Math.random) +} diff --git a/cli/src/http/retry.test.ts b/cli/src/http/retry.test.ts index 83e4d4c9965..25d646facf5 100644 --- a/cli/src/http/retry.test.ts +++ b/cli/src/http/retry.test.ts @@ -1,6 +1,6 @@ import type { FetchContext, HttpMethod, ResolvedOptions } from './types.js' import { describe, expect, it } from 'vitest' -import { backoffDelay, shouldRetry } from './retry.js' +import { backoffDelay, isIdempotentRetryMethod, shouldRetry } from './retry.js' function ctxFor(method: HttpMethod): FetchContext { const options: ResolvedOptions = { @@ -10,6 +10,7 @@ function ctxFor(method: HttpMethod): FetchContext { timeoutMs: undefined, retryAttempts: 0, throwOnError: true, + retryOnRateLimit: false, } return { request: new Request('https://x/y', { method }), @@ -30,6 +31,11 @@ describe('shouldRetry', () => { expect(shouldRetry(res, ctxFor('GET'))).toBe(false) }) + it('no longer retries 429 here (it has a dedicated branch in execute())', () => { + const res = new Response(null, { status: 429 }) + expect(shouldRetry(res, ctxFor('GET'))).toBe(false) + }) + it('does not retry POST regardless of status', () => { const res = new Response(null, { status: 503 }) expect(shouldRetry(res, ctxFor('POST'))).toBe(false) @@ -50,6 +56,16 @@ describe('shouldRetry', () => { }) }) +describe('isIdempotentRetryMethod', () => { + it('is true for GET/PUT/DELETE and false for POST/PATCH', () => { + expect(isIdempotentRetryMethod('GET')).toBe(true) + expect(isIdempotentRetryMethod('PUT')).toBe(true) + expect(isIdempotentRetryMethod('DELETE')).toBe(true) + expect(isIdempotentRetryMethod('POST')).toBe(false) + expect(isIdempotentRetryMethod('PATCH')).toBe(false) + }) +}) + describe('backoffDelay', () => { it('returns 0 for attempts <= 0', () => { expect(backoffDelay(0)).toBe(0) diff --git a/cli/src/http/retry.ts b/cli/src/http/retry.ts index 456bf321669..6e663ba74ef 100644 --- a/cli/src/http/retry.ts +++ b/cli/src/http/retry.ts @@ -1,11 +1,19 @@ import type { FetchContext } from './types.js' export const RETRY_METHODS = ['GET', 'PUT', 'DELETE'] as const -export const RETRY_STATUS_CODES = [408, 413, 429, 500, 502, 503, 504] as const +// 429 is intentionally absent — it has a dedicated branch in execute(). shouldRetry covers +// transport errors / 408 / 413 / 5xx only. +export const RETRY_STATUS_CODES = [408, 413, 500, 502, 503, 504] as const const RETRY_METHODS_SET: ReadonlySet = new Set(RETRY_METHODS) const RETRY_STATUS_SET: ReadonlySet = new Set(RETRY_STATUS_CODES) +// GET/PUT/DELETE are idempotent — safe to auto-retry. The 429 branch reuses this to decide which +// methods may wait-and-retry a throttle without risking a double-run. +export function isIdempotentRetryMethod(method: string): boolean { + return RETRY_METHODS_SET.has(method) +} + export function shouldRetry(target: Response | unknown, ctx: FetchContext): boolean { if (!RETRY_METHODS_SET.has(ctx.options.method)) return false diff --git a/cli/src/http/types.ts b/cli/src/http/types.ts index e06bbfc9fa6..d209e97460c 100644 --- a/cli/src/http/types.ts +++ b/cli/src/http/types.ts @@ -7,6 +7,8 @@ export type HttpLogEvent = { readonly status?: number readonly attempt?: number readonly durationMs?: number + // Set on a 429 retry decision so --verbose can explain how long we waited. + readonly delayMs?: number } export type HttpLogger = (event: HttpLogEvent) => void @@ -51,6 +53,8 @@ export type RequestOptions = { readonly retryAttempts?: number readonly signal?: AbortSignal readonly throwOnError?: boolean + // Opt a non-idempotent POST into bounded wait-and-retry on a 429 throttle. + readonly retryOnRateLimit?: boolean } export type ResolvedOptions = { @@ -60,6 +64,7 @@ export type ResolvedOptions = { readonly timeoutMs: number | undefined readonly retryAttempts: number readonly throwOnError: boolean + readonly retryOnRateLimit: boolean } export type ClientOptions = { diff --git a/cli/test/fixtures/dify-mock/server.ts b/cli/test/fixtures/dify-mock/server.ts index afc135e5327..96edc96f9ba 100644 --- a/cli/test/fixtures/dify-mock/server.ts +++ b/cli/test/fixtures/dify-mock/server.ts @@ -150,8 +150,9 @@ export function buildApp(getScenario: () => Scenario, state?: MockState): Hono { app.use('*', async (c, next) => { const scenario = getScenario() if (scenario === 'rate-limited') { + // Unified ErrorBody — per-token throttle (retryable); Retry-After advises the wait. return c.json( - { error: { code: 'rate_limited', message: 'too many requests' } }, + { code: 'too_many_requests', message: 'Too many requests for this API token.', status: 429 }, { status: 429, headers: { 'retry-after': '1' } }, ) } From 0e14d07adb187a1758b0ac3e562e911c6db7fc6a Mon Sep 17 00:00:00 2001 From: Charles Yao Date: Fri, 12 Jun 2026 09:30:15 +0200 Subject: [PATCH 043/122] feat(api): forward user_type for MCP identity forwarding (webapp end-users) (#37347) Co-authored-by: Claude Opus 4.8 Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/core/app/apps/base_app_queue_manager.py | 2 +- api/core/app/apps/base_app_runner.py | 4 +-- api/core/app/apps/workflow_app_runner.py | 2 +- api/core/app/entities/app_invoke_entities.py | 8 +++++ api/core/tools/mcp_tool/tool.py | 10 ++++++ api/services/enterprise/enterprise_service.py | 2 ++ .../unit_tests/core/app/test_invoke_from.py | 16 ++++++++++ .../unit_tests/core/tools/test_mcp_tool.py | 32 +++++++++++++++++++ .../enterprise/test_enterprise_service.py | 15 +++++++++ 9 files changed, 86 insertions(+), 5 deletions(-) diff --git a/api/core/app/apps/base_app_queue_manager.py b/api/core/app/apps/base_app_queue_manager.py index b959987078e..9551a5e38c4 100644 --- a/api/core/app/apps/base_app_queue_manager.py +++ b/api/core/app/apps/base_app_queue_manager.py @@ -41,7 +41,7 @@ class AppQueueManager(ABC): self._invoke_from = invoke_from self.invoke_from = invoke_from # Public accessor for invoke_from - user_prefix = "account" if self._invoke_from in {InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER} else "end-user" + user_prefix = "account" if self._invoke_from.runs_as_account() else "end-user" self._task_belong_cache_key = AppQueueManager._generate_task_belong_cache_key(self._task_id) redis_client.setex(self._task_belong_cache_key, 1800, f"{user_prefix}-{self._user_id}") diff --git a/api/core/app/apps/base_app_runner.py b/api/core/app/apps/base_app_runner.py index 15f6359929a..a89a0cf70db 100644 --- a/api/core/app/apps/base_app_runner.py +++ b/api/core/app/apps/base_app_runner.py @@ -431,9 +431,7 @@ class AppRunner: url=f"/files/tools/{tool_file.id}", upload_file_id=tool_file.id, created_by_role=( - CreatorUserRole.ACCOUNT - if queue_manager.invoke_from in {InvokeFrom.DEBUGGER, InvokeFrom.EXPLORE} - else CreatorUserRole.END_USER + CreatorUserRole.ACCOUNT if queue_manager.invoke_from.runs_as_account() else CreatorUserRole.END_USER ), created_by=user_id, ) diff --git a/api/core/app/apps/workflow_app_runner.py b/api/core/app/apps/workflow_app_runner.py index 944860ee39c..69f6c5b69b7 100644 --- a/api/core/app/apps/workflow_app_runner.py +++ b/api/core/app/apps/workflow_app_runner.py @@ -104,7 +104,7 @@ class WorkflowBasedAppRunner: @staticmethod def _resolve_user_from(invoke_from: InvokeFrom) -> UserFrom: - if invoke_from in {InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER}: + if invoke_from.runs_as_account(): return UserFrom.ACCOUNT return UserFrom.END_USER diff --git a/api/core/app/entities/app_invoke_entities.py b/api/core/app/entities/app_invoke_entities.py index 08ecc2097b3..2153289e0e6 100644 --- a/api/core/app/entities/app_invoke_entities.py +++ b/api/core/app/entities/app_invoke_entities.py @@ -47,6 +47,14 @@ class InvokeFrom(StrEnum): } return source_mapping.get(self, "dev") + def runs_as_account(self) -> bool: + """Whether a run from this entry point is attributed to a workspace + Account rather than an end user. Console contexts (debugger/explore) + run as the signed-in Account; webapp/service-api/trigger run as an + EndUser. Single source of truth for the created-by-role / user-type + split shared by the app runners and MCP identity forwarding.""" + return self in (InvokeFrom.DEBUGGER, InvokeFrom.EXPLORE) + class DifyRunContext(BaseModel): tenant_id: str diff --git a/api/core/tools/mcp_tool/tool.py b/api/core/tools/mcp_tool/tool.py index dc34264fb41..7a1553a4b15 100644 --- a/api/core/tools/mcp_tool/tool.py +++ b/api/core/tools/mcp_tool/tool.py @@ -358,7 +358,17 @@ class MCPTool(Tool): tenant_id=self.tenant_id, app_id=app_id, audience=audience, + user_type=self._resolve_user_type(), ) except MCPTokenError as e: raise ToolInvokeError(f"Failed to obtain forwarded identity token: {e}") from e headers[FORWARDED_IDENTITY_HEADER] = token + + def _resolve_user_type(self) -> str: + """Return "account" for console-authenticated callers (debugger/explore), + "end_user" for webapp / service-api / trigger callers — so the enterprise + side routes to the console store vs the published-webapp store.""" + invoke_from = self.runtime.invoke_from + if invoke_from is not None and invoke_from.runs_as_account(): + return "account" + return "end_user" diff --git a/api/services/enterprise/enterprise_service.py b/api/services/enterprise/enterprise_service.py index ccfae848d1a..d7dbd12973d 100644 --- a/api/services/enterprise/enterprise_service.py +++ b/api/services/enterprise/enterprise_service.py @@ -136,6 +136,7 @@ class EnterpriseService: tenant_id: str, app_id: str | None, audience: str, + user_type: str = "account", ) -> tuple[str, int]: """Mint a short-lived SSO id_token (or OAuth2 access_token) representing the calling Dify user, audience-scoped to the given MCP server identifier. @@ -163,6 +164,7 @@ class EnterpriseService: "tenant_id": tenant_id, "app_id": app_id or "", "audience": audience, + "user_type": user_type, }, ) except EnterpriseServiceError as e: diff --git a/api/tests/unit_tests/core/app/test_invoke_from.py b/api/tests/unit_tests/core/app/test_invoke_from.py index e0a8344d2f6..33a4d2edf11 100644 --- a/api/tests/unit_tests/core/app/test_invoke_from.py +++ b/api/tests/unit_tests/core/app/test_invoke_from.py @@ -7,3 +7,19 @@ def test_openapi_variant_present(): def test_openapi_distinct_from_service_api(): assert InvokeFrom.OPENAPI != InvokeFrom.SERVICE_API + + +def test_runs_as_account_only_for_console_contexts(): + # Console contexts (studio debugger / explore) run as the signed-in Account. + assert InvokeFrom.DEBUGGER.runs_as_account() is True + assert InvokeFrom.EXPLORE.runs_as_account() is True + # Everything else is attributed to an end user. + for invoke_from in ( + InvokeFrom.WEB_APP, + InvokeFrom.SERVICE_API, + InvokeFrom.OPENAPI, + InvokeFrom.TRIGGER, + InvokeFrom.PUBLISHED_PIPELINE, + InvokeFrom.VALIDATION, + ): + assert invoke_from.runs_as_account() is False diff --git a/api/tests/unit_tests/core/tools/test_mcp_tool.py b/api/tests/unit_tests/core/tools/test_mcp_tool.py index cdcb628972c..2e2b961bf2a 100644 --- a/api/tests/unit_tests/core/tools/test_mcp_tool.py +++ b/api/tests/unit_tests/core/tools/test_mcp_tool.py @@ -219,6 +219,38 @@ def test_inject_forwarded_identity_translates_token_error_to_invoke_error(): assert "Authorization" not in headers +def test_inject_forwarded_identity_sends_end_user_type_for_webapp(): + """A WEB_APP run forwards user_type=end_user so enterprise routes to the + published-webapp token store.""" + tool = _build_forwarding_tool() + tool.runtime = ToolRuntime(tenant_id="tenant-1", invoke_from=InvokeFrom.WEB_APP) + headers: dict[str, str] = {} + + with patch( + "services.enterprise.enterprise_service.EnterpriseService.issue_mcp_token", + return_value=("forwarded.jwt", 1900000000), + ) as issue: + tool._inject_forwarded_identity( + headers, user_id="eu-1", app_id="app-1", audience="https://mcp.example.com/mcp/" + ) + + assert issue.call_args.kwargs["user_type"] == "end_user" + + +def test_inject_forwarded_identity_sends_account_type_for_debugger(): + """A DEBUGGER/console run forwards user_type=account (the existing behaviour).""" + tool = _build_forwarding_tool() # built with InvokeFrom.DEBUGGER + headers: dict[str, str] = {} + + with patch( + "services.enterprise.enterprise_service.EnterpriseService.issue_mcp_token", + return_value=("forwarded.jwt", 1900000000), + ) as issue: + tool._inject_forwarded_identity(headers, user_id="acc-1", app_id=None, audience="https://mcp.example.com/mcp/") + + assert issue.call_args.kwargs["user_type"] == "account" + + def test_invoke_remote_mcp_tool_fails_closed_when_user_id_missing(): """When forwarding is enabled AND the deployment is enterprise, missing user_id must raise — never silently invoke as the static identity.""" diff --git a/api/tests/unit_tests/services/enterprise/test_enterprise_service.py b/api/tests/unit_tests/services/enterprise/test_enterprise_service.py index e7efe79af00..f64c7233b9d 100644 --- a/api/tests/unit_tests/services/enterprise/test_enterprise_service.py +++ b/api/tests/unit_tests/services/enterprise/test_enterprise_service.py @@ -500,9 +500,24 @@ class TestIssueMCPToken: "tenant_id": "tenant-uuid", "app_id": "app-uuid", "audience": "https://mcp.example.com/mcp/", + "user_type": "account", }, ) + def test_end_user_type_is_forwarded(self): + with patch(f"{MODULE}.EnterpriseRequest") as req: + req.send_request.return_value = {"token": "t", "expires_at": 1900000000} + EnterpriseService.issue_mcp_token( + user_id="end-user-uuid", + tenant_id="tenant-uuid", + app_id="app-uuid", + audience="https://mcp.example.com/mcp/", + user_type="end_user", + ) + body = req.send_request.call_args.kwargs["json"] + assert body["user_type"] == "end_user" + assert body["app_id"] == "app-uuid" + def test_401_maps_to_identity_refresh_error(self): from services.enterprise.base import MCPIdentityRefreshError from services.errors.enterprise import EnterpriseAPIUnauthorizedError From 9c25fa1c9691fd3533328ccac092353ce163668f Mon Sep 17 00:00:00 2001 From: -LAN- Date: Fri, 12 Jun 2026 15:33:29 +0800 Subject: [PATCH 044/122] chore(codeowners): update CLI ownership (#37375) --- .github/CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7942d44541b..6eb60e045a5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -23,8 +23,8 @@ /docs/ @crazywoola # CLI -/cli/ @langgenius/maintainers -/.github/workflows/cli-tests.yml @langgenius/maintainers +/cli/ @GareArc +/.github/workflows/cli-tests.yml @GareArc # Backend (default owner, more specific rules below will override) /api/ @QuantumGhost From 6c0cce4b7faecbfd9ec8c2e4669641b99168ce22 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Fri, 12 Jun 2026 16:52:19 +0900 Subject: [PATCH 045/122] chore: update to openapi v3 by change dep (#37316) Co-authored-by: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Co-authored-by: Stephen Zhou Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/controllers/common/schema.py | 50 +- .../console/app/advanced_prompt_template.py | 3 +- api/controllers/console/billing/compliance.py | 3 +- api/controllers/console/extension.py | 4 +- api/dev/generate_fastopenapi_specs.py | 2 +- api/dev/generate_swagger_markdown_docs.py | 59 +- api/dev/generate_swagger_specs.py | 195 +- api/extensions/ext_fastopenapi.py | 2 +- api/libs/flask_restx_compat.py | 10 +- ...{console-swagger.md => console-openapi.md} | 9498 ++++++++--------- ...{openapi-swagger.md => openapi-openapi.md} | 518 +- ...{service-swagger.md => service-openapi.md} | 1606 ++- .../{web-swagger.md => web-openapi.md} | 635 +- api/pyproject.toml | 1 + .../test_generate_swagger_markdown_docs.py | 168 +- .../commands/test_generate_swagger_specs.py | 64 +- .../controllers/common/test_schema.py | 27 +- .../unit_tests/controllers/test_swagger.py | 72 +- api/uv.lock | 10 +- cli/src/commands/import/app/run.ts | 8 +- .../generated/api/console/account/zod.gen.ts | 2 +- .../api/console/activate/types.gen.ts | 2 +- .../generated/api/console/activate/zod.gen.ts | 2 +- .../generated/api/console/agents/types.gen.ts | 85 +- .../generated/api/console/agents/zod.gen.ts | 94 +- .../console/api-based-extension/types.gen.ts | 4 +- .../console/api-based-extension/zod.gen.ts | 2 +- .../api/console/api-key-auth/types.gen.ts | 4 +- .../api/console/api-key-auth/zod.gen.ts | 2 +- .../generated/api/console/apps/orpc.gen.ts | 95 +- .../generated/api/console/apps/types.gen.ts | 389 +- .../generated/api/console/apps/zod.gen.ts | 560 +- .../api/console/data-source/types.gen.ts | 4 +- .../api/console/data-source/zod.gen.ts | 4 +- .../api/console/datasets/types.gen.ts | 114 +- .../generated/api/console/datasets/zod.gen.ts | 108 +- .../api/console/explore/types.gen.ts | 2 +- .../generated/api/console/explore/zod.gen.ts | 2 +- .../api/console/features/types.gen.ts | 2 +- .../generated/api/console/features/zod.gen.ts | 39 +- .../generated/api/console/info/types.gen.ts | 2 +- .../generated/api/console/info/zod.gen.ts | 2 +- .../api/console/installed-apps/types.gen.ts | 12 +- .../api/console/installed-apps/zod.gen.ts | 12 +- .../generated/api/console/notion/types.gen.ts | 2 +- .../generated/api/console/notion/zod.gen.ts | 2 +- .../generated/api/console/rag/types.gen.ts | 38 +- .../generated/api/console/rag/zod.gen.ts | 72 +- .../api/console/snippets/types.gen.ts | 36 +- .../generated/api/console/snippets/zod.gen.ts | 37 +- .../api/console/system-features/zod.gen.ts | 45 +- .../generated/api/console/tags/types.gen.ts | 4 +- .../generated/api/console/tags/zod.gen.ts | 2 +- .../generated/api/console/website/orpc.gen.ts | 2 +- .../api/console/website/types.gen.ts | 4 +- .../generated/api/console/website/zod.gen.ts | 2 +- .../api/console/workflow/types.gen.ts | 2 +- .../generated/api/console/workflow/zod.gen.ts | 2 +- .../api/console/workspaces/types.gen.ts | 38 +- .../api/console/workspaces/zod.gen.ts | 236 +- .../generated/api/openapi/types.gen.ts | 20 +- .../generated/api/openapi/zod.gen.ts | 23 +- .../generated/api/service/types.gen.ts | 122 +- .../generated/api/service/zod.gen.ts | 113 +- .../contracts/generated/api/web/types.gen.ts | 8 +- .../contracts/generated/api/web/zod.gen.ts | 49 +- packages/contracts/openapi-ts.api.config.ts | 389 +- 67 files changed, 7054 insertions(+), 8673 deletions(-) rename api/openapi/markdown/{console-swagger.md => console-openapi.md} (70%) rename api/openapi/markdown/{openapi-swagger.md => openapi-openapi.md} (66%) rename api/openapi/markdown/{service-swagger.md => service-openapi.md} (74%) rename api/openapi/markdown/{web-swagger.md => web-openapi.md} (73%) diff --git a/api/controllers/common/schema.py b/api/controllers/common/schema.py index 70d24b005ff..e3172f81c73 100644 --- a/api/controllers/common/schema.py +++ b/api/controllers/common/schema.py @@ -1,9 +1,9 @@ """Helpers for registering Pydantic models with Flask-RESTX namespaces. Flask-RESTX treats `SchemaModel` bodies as opaque JSON schemas; it does not -promote Pydantic's nested `$defs` into top-level Swagger `definitions`. +promote Pydantic's nested `$defs` into top-level OpenAPI component schemas. These helpers keep that translation centralized so models registered through -`register_schema_models` emit resolvable Swagger 2.0 references. +`register_schema_models` emit resolvable OpenAPI 3 references. """ from collections.abc import Iterable, Mapping @@ -14,7 +14,7 @@ from flask import request from flask_restx import Namespace from pydantic import BaseModel, TypeAdapter -DEFAULT_REF_TEMPLATE_SWAGGER_2_0 = "#/definitions/{model}" +DEFAULT_REF_TEMPLATE_OPENAPI_3_0 = "#/components/schemas/{model}" QueryParamDoc = TypedDict( @@ -48,7 +48,6 @@ class QueryArgs(Protocol): def _register_json_schema(namespace: Namespace, name: str, schema: dict) -> None: """Register a JSON schema and promote any nested Pydantic `$defs`.""" - schema = _swagger_2_compatible_schema(schema) nested_definitions = schema.get("$defs") schema_to_register = dict(schema) if isinstance(nested_definitions, dict): @@ -71,41 +70,12 @@ def _register_schema_model(namespace: Namespace, model: type[BaseModel], *, mode _register_json_schema( namespace, model.__name__, - model.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0, mode=mode), + model.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0, mode=mode), ) -def _swagger_2_compatible_schema(value: Any) -> Any: - if isinstance(value, list): - return [_swagger_2_compatible_schema(item) for item in value] - - if not isinstance(value, dict): - return value - - converted = {key: _swagger_2_compatible_schema(child) for key, child in value.items()} - any_of = value.get("anyOf") - if not isinstance(any_of, list): - return converted - - non_null_candidates = [ - candidate for candidate in any_of if isinstance(candidate, Mapping) and candidate.get("type") != "null" - ] - has_null_candidate = any(isinstance(candidate, Mapping) and candidate.get("type") == "null" for candidate in any_of) - if not has_null_candidate or len(non_null_candidates) != 1: - return converted - - non_null_schema = _swagger_2_compatible_schema(dict(non_null_candidates[0])) - if not isinstance(non_null_schema, dict): - return converted - - converted.pop("anyOf", None) - converted.update(non_null_schema) - converted["x-nullable"] = True - return converted - - def register_schema_model(namespace: Namespace, model: type[BaseModel]) -> None: - """Register a BaseModel and its nested schema definitions for Swagger documentation.""" + """Register a BaseModel and its nested component schemas for OpenAPI documentation.""" _register_schema_model(namespace, model, mode="validation") @@ -146,7 +116,7 @@ def register_enum_models(namespace: Namespace, *models: type[StrEnum]) -> None: _register_json_schema( namespace, model.__name__, - TypeAdapter(model).json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0), + TypeAdapter(model).json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0), ) @@ -155,10 +125,10 @@ def query_params_from_model(model: type[BaseModel]) -> dict[str, QueryParamDoc]: `Namespace.expect()` treats Pydantic schema models as request bodies, so GET endpoints should keep runtime validation on the Pydantic model and feed this - derived mapping to `Namespace.doc(params=...)` for Swagger documentation. + derived mapping to `Namespace.doc(params=...)` for OpenAPI documentation. """ - schema = model.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0) + schema = model.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0) properties = schema.get("properties", {}) if not isinstance(properties, Mapping): return {} @@ -203,7 +173,7 @@ def query_params_from_request[ModelT: BaseModel]( def _drop_malformed_defaulted_integer_params(model: type[BaseModel], params: dict[str, Any]) -> None: - properties = model.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0).get("properties", {}) + properties = model.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0).get("properties", {}) if not isinstance(properties, Mapping): return @@ -297,7 +267,7 @@ def _nullable_property_schema(property_schema: Mapping[str, Any]) -> Mapping[str __all__ = [ - "DEFAULT_REF_TEMPLATE_SWAGGER_2_0", + "DEFAULT_REF_TEMPLATE_OPENAPI_3_0", "get_or_create_model", "query_params_from_model", "query_params_from_request", diff --git a/api/controllers/console/app/advanced_prompt_template.py b/api/controllers/console/app/advanced_prompt_template.py index ad21671176f..0ad7eee7cdf 100644 --- a/api/controllers/console/app/advanced_prompt_template.py +++ b/api/controllers/console/app/advanced_prompt_template.py @@ -2,6 +2,7 @@ from flask import request from flask_restx import Resource, fields from pydantic import BaseModel, Field +from controllers.common.schema import DEFAULT_REF_TEMPLATE_OPENAPI_3_0 from controllers.console import console_ns from controllers.console.wraps import account_initialization_required, setup_required from libs.login import login_required @@ -17,7 +18,7 @@ class AdvancedPromptTemplateQuery(BaseModel): console_ns.schema_model( AdvancedPromptTemplateQuery.__name__, - AdvancedPromptTemplateQuery.model_json_schema(ref_template="#/definitions/{model}"), + AdvancedPromptTemplateQuery.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0), ) diff --git a/api/controllers/console/billing/compliance.py b/api/controllers/console/billing/compliance.py index 3d528e1ddd3..b0b364e54d2 100644 --- a/api/controllers/console/billing/compliance.py +++ b/api/controllers/console/billing/compliance.py @@ -7,6 +7,7 @@ from libs.login import login_required from models import Account from services.billing_service import BillingService +from ...common.schema import DEFAULT_REF_TEMPLATE_OPENAPI_3_0 from .. import console_ns from ..wraps import ( account_initialization_required, @@ -23,7 +24,7 @@ class ComplianceDownloadQuery(BaseModel): console_ns.schema_model( ComplianceDownloadQuery.__name__, - ComplianceDownloadQuery.model_json_schema(ref_template="#/definitions/{model}"), + ComplianceDownloadQuery.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0), ) diff --git a/api/controllers/console/extension.py b/api/controllers/console/extension.py index 26a348da404..6f1da8783e9 100644 --- a/api/controllers/console/extension.py +++ b/api/controllers/console/extension.py @@ -14,7 +14,7 @@ from models.api_based_extension import APIBasedExtension from services.api_based_extension_service import APIBasedExtensionService from services.code_based_extension_service import CodeBasedExtensionService -from ..common.schema import DEFAULT_REF_TEMPLATE_SWAGGER_2_0, register_schema_models +from ..common.schema import DEFAULT_REF_TEMPLATE_OPENAPI_3_0, register_schema_models from . import console_ns from .wraps import account_initialization_required, setup_required, with_current_tenant_id @@ -63,7 +63,7 @@ class APIBasedExtensionResponse(ResponseModel): register_schema_models(console_ns, APIBasedExtensionPayload, CodeBasedExtensionResponse, APIBasedExtensionResponse) console_ns.schema_model( "APIBasedExtensionListResponse", - TypeAdapter(list[APIBasedExtensionResponse]).json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0), + TypeAdapter(list[APIBasedExtensionResponse]).json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0), ) diff --git a/api/dev/generate_fastopenapi_specs.py b/api/dev/generate_fastopenapi_specs.py index 5a94d32b939..2a7f9450884 100644 --- a/api/dev/generate_fastopenapi_specs.py +++ b/api/dev/generate_fastopenapi_specs.py @@ -1,4 +1,4 @@ -"""Generate FastOpenAPI OpenAPI 3.0 specs without booting the full backend.""" +"""Generate FastOpenAPI OpenAPI 3.1 specs without booting the full backend.""" from __future__ import annotations diff --git a/api/dev/generate_swagger_markdown_docs.py b/api/dev/generate_swagger_markdown_docs.py index 75575b355b3..7028f740e02 100644 --- a/api/dev/generate_swagger_markdown_docs.py +++ b/api/dev/generate_swagger_markdown_docs.py @@ -1,7 +1,7 @@ """Generate OpenAPI JSON specs and split Markdown API docs. The Markdown step uses `swagger-markdown`, the same converter family as the -Swagger Markdown UI, so CI and local regeneration catch converter-incompatible +legacy Markdown UI, so CI and local regeneration catch converter-incompatible OpenAPI output early. """ @@ -25,19 +25,21 @@ from dev.generate_swagger_specs import SPEC_TARGETS, generate_specs logger = logging.getLogger(__name__) SWAGGER_MARKDOWN_PACKAGE = "swagger-markdown@3.0.0" -CONSOLE_SWAGGER_FILENAME = "console-swagger.json" +CONSOLE_OPENAPI_FILENAME = "console-openapi.json" STALE_COMBINED_MARKDOWN_FILENAME = "api-reference.md" -def _definition_ref_name(schema: object) -> str | None: +def _schema_ref_name(schema: object) -> str | None: if not isinstance(schema, dict): return None ref = schema.get("$ref") - if not isinstance(ref, str) or not ref.startswith("#/definitions/"): + if not isinstance(ref, str): return None - return ref.removeprefix("#/definitions/") + if ref.startswith("#/components/schemas/"): + return ref.removeprefix("#/components/schemas/") + return None def _markdown_anchor(name: str) -> str: @@ -48,7 +50,7 @@ def _schema_markdown_type(schema: object) -> str: if not isinstance(schema, dict): return "" - ref_name = _definition_ref_name(schema) + ref_name = _schema_ref_name(schema) if ref_name is not None: return f"[{ref_name}](#{_markdown_anchor(ref_name)})" @@ -111,15 +113,16 @@ def _has_union_schema(schema: object) -> bool: def _patch_union_schema_markdown(markdown: str, spec_path: Path) -> str: - """Fill Swagger Markdown table cells that `swagger-markdown` leaves blank for union schemas.""" + """Fill Markdown table cells that `swagger-markdown` leaves blank for union schemas.""" spec = json.loads(spec_path.read_text(encoding="utf-8")) - definitions = spec.get("definitions") - if not isinstance(definitions, dict): + components = spec.get("components") + schemas = components.get("schemas") if isinstance(components, dict) else None + if not isinstance(schemas, dict): return markdown - for definition_name, schema in definitions.items(): - if not isinstance(definition_name, str) or not isinstance(schema, dict): + for schema_name, schema in schemas.items(): + if not isinstance(schema_name, str) or not isinstance(schema, dict): continue properties = schema.get("properties") @@ -128,7 +131,7 @@ def _patch_union_schema_markdown(markdown: str, spec_path: Path) -> str: if isinstance(property_name, str) and _has_union_schema(property_schema): markdown = _replace_schema_table_type( markdown, - definition_name, + schema_name, property_name, _schema_markdown_type(property_schema), ) @@ -139,14 +142,14 @@ def _patch_union_schema_markdown(markdown: str, spec_path: Path) -> str: markdown = _replace_schema_table_type( markdown, - definition_name, - definition_name, + schema_name, + schema_name, _schema_markdown_type(schema), ) for variant in union_variants: - variant_name = _definition_ref_name(variant) - variant_schema = definitions.get(variant_name) if variant_name is not None else None + variant_name = _schema_ref_name(variant) + variant_schema = schemas.get(variant_name) if variant_name is not None else None if not isinstance(variant_name, str) or not isinstance(variant_schema, dict): continue properties = variant_schema.get("properties") @@ -229,7 +232,7 @@ def _append_fastopenapi_markdown(console_markdown_path: Path, fastopenapi_markdo "\n\n".join( [ console_markdown, - "## FastOpenAPI Preview (OpenAPI 3.0)", + "## FastOpenAPI Preview (OpenAPI 3.1)", fastopenapi_markdown, ] ) @@ -239,17 +242,17 @@ def _append_fastopenapi_markdown(console_markdown_path: Path, fastopenapi_markdo def generate_markdown_docs( - swagger_dir: Path, + openapi_dir: Path, markdown_dir: Path, *, keep_swagger_json: bool = False, ) -> list[Path]: """Generate intermediate specs, convert them to split Markdown API docs, and return Markdown paths.""" - swagger_paths = generate_specs(swagger_dir) - fastopenapi_paths = generate_fastopenapi_specs(swagger_dir) - spec_paths = [*swagger_paths, *fastopenapi_paths] - swagger_paths_by_name = {path.name: path for path in swagger_paths} + openapi_paths = generate_specs(openapi_dir) + fastopenapi_paths = generate_fastopenapi_specs(openapi_dir) + spec_paths = [*openapi_paths, *fastopenapi_paths] + openapi_paths_by_name = {path.name: path for path in openapi_paths} fastopenapi_paths_by_name = {path.name: path for path in fastopenapi_paths} markdown_dir.mkdir(parents=True, exist_ok=True) @@ -260,9 +263,9 @@ def generate_markdown_docs( temp_markdown_dir = Path(temp_dir) for target in SPEC_TARGETS: - swagger_path = swagger_paths_by_name[target.filename] - markdown_path = markdown_dir / f"{swagger_path.stem}.md" - _convert_spec_to_markdown(swagger_path, markdown_path) + openapi_path = openapi_paths_by_name[target.filename] + markdown_path = markdown_dir / f"{openapi_path.stem}.md" + _convert_spec_to_markdown(openapi_path, markdown_path) written_paths.append(markdown_path) for target in FASTOPENAPI_SPEC_TARGETS: # type: ignore @@ -270,7 +273,7 @@ def generate_markdown_docs( markdown_path = temp_markdown_dir / f"{fastopenapi_path.stem}.md" _convert_spec_to_markdown(fastopenapi_path, markdown_path) - console_markdown_path = markdown_dir / f"{Path(CONSOLE_SWAGGER_FILENAME).stem}.md" + console_markdown_path = markdown_dir / f"{Path(CONSOLE_OPENAPI_FILENAME).stem}.md" _append_fastopenapi_markdown(console_markdown_path, markdown_path) (markdown_dir / STALE_COMBINED_MARKDOWN_FILENAME).unlink(missing_ok=True) @@ -286,6 +289,8 @@ def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "--swagger-dir", + "--openapi-dir", + dest="openapi_dir", type=Path, default=Path("openapi"), help="Directory where intermediate JSON spec files will be written.", @@ -307,7 +312,7 @@ def parse_args() -> argparse.Namespace: def main() -> int: args = parse_args() written_paths = generate_markdown_docs( - args.swagger_dir, + args.openapi_dir, args.markdown_dir, keep_swagger_json=args.keep_swagger_json, ) diff --git a/api/dev/generate_swagger_specs.py b/api/dev/generate_swagger_specs.py index b7c58d64441..6b959b6d17e 100644 --- a/api/dev/generate_swagger_specs.py +++ b/api/dev/generate_swagger_specs.py @@ -1,9 +1,9 @@ -"""Generate Flask-RESTX Swagger 2.0 specs without booting the full backend. +"""Generate Flask-RESTX OpenAPI 3 specs without booting the full backend. This helper intentionally avoids `app_factory.create_app()`. The normal backend startup eagerly initializes database, Redis, Celery, and storage extensions, which is unnecessary when the goal is only to serialize the Flask-RESTX -`/swagger.json` documents. +`/openapi.json` documents. """ from __future__ import annotations @@ -42,10 +42,10 @@ class RestxApi(Protocol): SPEC_TARGETS: tuple[SpecTarget, ...] = ( - SpecTarget(route="/console/api/swagger.json", filename="console-swagger.json", namespace="console"), - SpecTarget(route="/api/swagger.json", filename="web-swagger.json", namespace="web"), - SpecTarget(route="/v1/swagger.json", filename="service-swagger.json", namespace="service"), - SpecTarget(route="/openapi/v1/swagger.json", filename="openapi-swagger.json", namespace="openapi"), + SpecTarget(route="/console/api/openapi.json", filename="console-openapi.json", namespace="console"), + SpecTarget(route="/api/openapi.json", filename="web-openapi.json", namespace="web"), + SpecTarget(route="/v1/openapi.json", filename="service-openapi.json", namespace="service"), + SpecTarget(route="/openapi/v1/openapi.json", filename="openapi-openapi.json", namespace="openapi"), ) @@ -126,7 +126,7 @@ def _inline_model_signature(nested_fields: dict[object, object]) -> object: def _inline_model_name(nested_fields: dict[object, object]) -> str: - """Return a stable Swagger model name for an anonymous inline field map.""" + """Return a stable OpenAPI model name for an anonymous inline field map.""" signature = json.dumps(_inline_model_signature(nested_fields), sort_keys=True, separators=(",", ":")) digest = hashlib.sha1(signature.encode("utf-8")).hexdigest()[:12] @@ -134,7 +134,7 @@ def _inline_model_name(nested_fields: dict[object, object]) -> str: def apply_runtime_defaults() -> None: - """Force the small config surface required for Swagger generation.""" + """Force the small config surface required for OpenAPI generation.""" os.environ.setdefault("SECRET_KEY", "spec-export") os.environ.setdefault("STORAGE_TYPE", "local") @@ -150,7 +150,7 @@ def apply_runtime_defaults() -> None: def create_spec_app() -> Flask: - """Build a minimal Flask app that only mounts the Swagger-producing blueprints.""" + """Build a minimal Flask app that only mounts the OpenAPI-producing blueprints.""" apply_runtime_defaults() @@ -182,7 +182,7 @@ def create_spec_app() -> Flask: def _registered_models(namespace: str) -> dict[str, object]: - """Return the Flask-RESTX models registered for a Swagger namespace.""" + """Return the Flask-RESTX models registered for an OpenAPI namespace.""" if namespace == "console": from controllers.console import console_ns @@ -213,7 +213,7 @@ def _registered_models(namespace: str) -> dict[str, object]: models.update(api.models) return models - raise ValueError(f"unknown Swagger namespace: {namespace}") + raise ValueError(f"unknown OpenAPI namespace: {namespace}") def _materialize_inline_model_definitions(api: RestxApi) -> None: @@ -289,7 +289,7 @@ def drop_null_values(value: object) -> object: def sort_openapi_arrays(value: object, *, parent_key: str | None = None) -> object: - """Sort order-insensitive Swagger arrays so generated Markdown is stable.""" + """Sort order-insensitive OpenAPI arrays so generated Markdown is stable.""" if isinstance(value, dict): return {key: sort_openapi_arrays(item, parent_key=key) for key, item in value.items()} @@ -313,23 +313,174 @@ def sort_openapi_arrays(value: object, *, parent_key: str | None = None) -> obje return sorted_items -def _merge_registered_definitions(payload: dict[str, object], namespace: str) -> dict[str, object]: - """Include registered but route-indirect models in the exported Swagger definitions.""" +def _replace_legacy_refs(value: object) -> object: + if isinstance(value, dict): + replaced: dict[object, object] = {} + for key, item in value.items(): + if key == "$ref" and isinstance(item, str) and item.startswith("#/definitions/"): + replaced[key] = item.replace("#/definitions/", "#/components/schemas/", 1) + else: + replaced[key] = _replace_legacy_refs(item) + return replaced + if isinstance(value, list): + return [_replace_legacy_refs(item) for item in value] + return value - definitions = payload.setdefault("definitions", {}) - if not isinstance(definitions, dict): - raise RuntimeError("unexpected Swagger definitions payload") + +HTTP_METHODS = {"delete", "get", "head", "options", "patch", "post", "put", "trace"} + + +def _resolve_component_schema(payload: dict[str, object], schema: object) -> dict[str, object] | None: + if not isinstance(schema, dict): + return None + + ref = schema.get("$ref") + if isinstance(ref, str) and ref.startswith("#/components/schemas/"): + name = ref.removeprefix("#/components/schemas/") + components = payload.get("components") + if not isinstance(components, dict): + return None + schemas = components.get("schemas") + if not isinstance(schemas, dict): + return None + resolved = schemas.get(name) + return resolved if isinstance(resolved, dict) else None + + return schema + + +def _request_body_schema(request_body: object) -> object | None: + if not isinstance(request_body, dict): + return None + content = request_body.get("content") + if not isinstance(content, dict): + return None + media_type = content.get("application/json") + if not isinstance(media_type, dict): + return None + return media_type.get("schema") + + +def _query_parameters_from_schema(schema: dict[str, object]) -> list[dict[str, object]]: + properties = schema.get("properties") + if not isinstance(properties, dict): + return [] + + required = schema.get("required") + required_names = set(required) if isinstance(required, list) else set() + parameters: list[dict[str, object]] = [] + + for name, property_schema in sorted(properties.items()): + if not isinstance(name, str) or not isinstance(property_schema, dict): + continue + schema_copy = dict(property_schema) + description = schema_copy.get("description") + parameter: dict[str, object] = { + "name": name, + "in": "query", + "required": name in required_names, + "schema": schema_copy, + } + if isinstance(description, str): + parameter["description"] = description + parameters.append(parameter) + + return parameters + + +def _move_get_request_bodies_to_query_parameters(payload: dict[str, object]) -> dict[str, object]: + """Represent GET request bodies as query parameters in exported specs.""" + + paths = payload.get("paths") + if not isinstance(paths, dict): + return payload + + for path_item in paths.values(): + if not isinstance(path_item, dict): + continue + operation = path_item.get("get") + if not isinstance(operation, dict) or "requestBody" not in operation: + continue + + schema = _resolve_component_schema(payload, _request_body_schema(operation.get("requestBody"))) + existing_parameters = operation.get("parameters") + parameters = list(existing_parameters) if isinstance(existing_parameters, list) else [] + existing_query_names = { + parameter.get("name") + for parameter in parameters + if isinstance(parameter, dict) and parameter.get("in") == "query" + } + + if schema is not None: + for parameter in _query_parameters_from_schema(schema): + if parameter["name"] not in existing_query_names: + parameters.append(parameter) + + if parameters: + operation["parameters"] = parameters + operation.pop("requestBody", None) + + return payload + + +def _deduplicate_operation_ids(payload: dict[str, object]) -> dict[str, object]: + """Make operationId values unique while preserving already-unique IDs.""" + + paths = payload.get("paths") + if not isinstance(paths, dict): + return payload + + operations_by_id: dict[str, list[tuple[str, str, dict[str, object]]]] = {} + for path, path_item in paths.items(): + if not isinstance(path, str) or not isinstance(path_item, dict): + continue + for method, operation in path_item.items(): + if method not in HTTP_METHODS or not isinstance(operation, dict): + continue + operation_id = operation.get("operationId") + if isinstance(operation_id, str): + operations_by_id.setdefault(operation_id, []).append((method, path, operation)) + + for operation_id, operations in operations_by_id.items(): + if len(operations) < 2: + continue + for method, path, operation in operations: + digest = hashlib.sha1(f"{method}:{path}".encode()).hexdigest()[:8] + operation["operationId"] = f"{operation_id}_{digest}" + + return payload + + +def _component_schemas(payload: dict[str, object]) -> dict[str, object]: + components = payload.setdefault("components", {}) + if not isinstance(components, dict): + raise RuntimeError("unexpected OpenAPI components payload") + + schemas = components.setdefault("schemas", {}) + if not isinstance(schemas, dict): + raise RuntimeError("unexpected OpenAPI component schemas payload") + + return schemas + + +def _merge_registered_schemas(payload: dict[str, object], namespace: str) -> dict[str, object]: + """Include registered but route-indirect models in exported OpenAPI schemas.""" + + schemas = _component_schemas(payload) for name, model in _registered_models(namespace).items(): schema = getattr(model, "__schema__", None) if isinstance(schema, dict): - definitions.setdefault(name, schema) + schemas.setdefault(name, _replace_legacy_refs(schema)) + + payload.pop("definitions", None) + payload = _replace_legacy_refs(payload) # type: ignore[assignment] return payload def generate_specs(output_dir: Path) -> list[Path]: - """Write all Swagger specs to `output_dir` and return the written paths.""" + """Write all OpenAPI specs to `output_dir` and return the written paths.""" output_dir.mkdir(parents=True, exist_ok=True) @@ -345,7 +496,9 @@ def generate_specs(output_dir: Path) -> list[Path]: payload = response.get_json() if not isinstance(payload, dict): raise RuntimeError(f"unexpected response payload for {target.route}") - payload = _merge_registered_definitions(payload, target.namespace) + payload = _merge_registered_schemas(payload, target.namespace) + payload = _move_get_request_bodies_to_query_parameters(payload) + payload = _deduplicate_operation_ids(payload) payload = drop_null_values(payload) payload = sort_openapi_arrays(payload) @@ -363,7 +516,7 @@ def parse_args() -> argparse.Namespace: "--output-dir", type=Path, default=Path("openapi"), - help="Directory where the Swagger JSON files will be written.", + help="Directory where the OpenAPI JSON files will be written.", ) return parser.parse_args() diff --git a/api/extensions/ext_fastopenapi.py b/api/extensions/ext_fastopenapi.py index 569203e974f..d51f2781fc5 100644 --- a/api/extensions/ext_fastopenapi.py +++ b/api/extensions/ext_fastopenapi.py @@ -26,7 +26,7 @@ def init_app(app: DifyApp) -> None: docs_url=docs_url, redoc_url=redoc_url, openapi_url=openapi_url, - openapi_version="3.0.0", + openapi_version="3.1.0", title="Dify API (FastOpenAPI PoC)", version="1.0", description="FastOpenAPI proof of concept for Dify API", diff --git a/api/libs/flask_restx_compat.py b/api/libs/flask_restx_compat.py index 34e0d586a07..08fd3d9055d 100644 --- a/api/libs/flask_restx_compat.py +++ b/api/libs/flask_restx_compat.py @@ -1,8 +1,8 @@ -"""Compatibility helpers for Dify's Flask-RESTX Swagger integration. +"""Compatibility helpers for Dify's Flask-RESTX OpenAPI integration. These helpers are temporary bridges for legacy Flask-RESTX field contracts while controllers migrate their request and response documentation to Pydantic -models. Keep the behavior centralized so live Swagger endpoints and offline +models. Keep the behavior centralized so live OpenAPI endpoints and offline spec export fail or succeed in the same way. """ @@ -91,7 +91,7 @@ def _inline_model_signature(nested_fields: dict[object, object]) -> object: def _inline_model_name(nested_fields: dict[object, object]) -> str: - """Return a stable Swagger model name for an anonymous inline field map.""" + """Return a stable OpenAPI model name for an anonymous inline field map.""" signature = json.dumps(_inline_model_signature(nested_fields), sort_keys=True, separators=(",", ":")) digest = hashlib.sha1(signature.encode("utf-8")).hexdigest()[:12] @@ -99,11 +99,11 @@ def _inline_model_name(nested_fields: dict[object, object]) -> str: def patch_swagger_for_inline_nested_dicts() -> None: - """Allow Swagger generation to handle legacy inline Flask-RESTX field dicts. + """Allow OpenAPI generation to handle legacy inline Flask-RESTX field dicts. Some existing controllers use raw field mappings in `fields.Nested({...})` or directly in `@namespace.response(...)`. Runtime marshalling accepts that, - but Flask-RESTX Swagger registration expects a named model. Convert those + but Flask-RESTX registration expects a named model. Convert those anonymous mappings into temporary named models during docs generation. """ diff --git a/api/openapi/markdown/console-swagger.md b/api/openapi/markdown/console-openapi.md similarity index 70% rename from api/openapi/markdown/console-swagger.md rename to api/openapi/markdown/console-openapi.md index 2b66bcab70a..c0d9fb653a3 100644 --- a/api/openapi/markdown/console-swagger.md +++ b/api/openapi/markdown/console-openapi.md @@ -3,944 +3,829 @@ Console management APIs for app configuration, monitoring, and administration ## Version: 1.0 -### Security -**Bearer** - -| apiKey | *API Key* | -| ------ | --------- | -| Description | Type: Bearer {your-api-key} | -| In | header | -| Name | Authorization | +### Available authorizations +#### Bearer (API Key Authentication) +Type: Bearer {your-api-key} +**Name:** Authorization +**In:** header --- ## console Console management API operations -### /account/avatar - -#### GET -##### Description - +### [GET] /account/avatar Get account avatar url -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | avatar | query | Avatar file ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [AvatarUrlResponse](#avatarurlresponse) | +| 200 | Success | **application/json**: [AvatarUrlResponse](#avatarurlresponse)
| -#### POST -##### Parameters +### [POST] /account/avatar +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AccountAvatarPayload](#accountavatarpayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AccountAvatarPayload](#accountavatarpayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [Account](#account) | +| 200 | Success | **application/json**: [Account](#account)
| -### /account/change-email +### [POST] /account/change-email +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ChangeEmailSendPayload](#changeemailsendpayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ChangeEmailSendPayload](#changeemailsendpayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultDataResponse](#simpleresultdataresponse) | +| 200 | Success | **application/json**: [SimpleResultDataResponse](#simpleresultdataresponse)
| -### /account/change-email/check-email-unique +### [POST] /account/change-email/check-email-unique +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [CheckEmailUniquePayload](#checkemailuniquepayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [CheckEmailUniquePayload](#checkemailuniquepayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -### /account/change-email/reset +### [POST] /account/change-email/reset +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ChangeEmailResetPayload](#changeemailresetpayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ChangeEmailResetPayload](#changeemailresetpayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [Account](#account) | +| 200 | Success | **application/json**: [Account](#account)
| -### /account/change-email/validity +### [POST] /account/change-email/validity +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ChangeEmailValidityPayload](#changeemailvaliditypayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ChangeEmailValidityPayload](#changeemailvaliditypayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [VerificationTokenResponse](#verificationtokenresponse) | +| 200 | Success | **application/json**: [VerificationTokenResponse](#verificationtokenresponse)
| -### /account/delete +### [POST] /account/delete +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AccountDeletePayload](#accountdeletepayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AccountDeletePayload](#accountdeletepayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -### /account/delete/feedback +### [POST] /account/delete/feedback +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AccountDeletionFeedbackPayload](#accountdeletionfeedbackpayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AccountDeletionFeedbackPayload](#accountdeletionfeedbackpayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -### /account/delete/verify - -#### GET -##### Responses +### [GET] /account/delete/verify +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultDataResponse](#simpleresultdataresponse) | +| 200 | Success | **application/json**: [SimpleResultDataResponse](#simpleresultdataresponse)
| -### /account/education - -#### GET -##### Responses +### [GET] /account/education +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [EducationStatusResponse](#educationstatusresponse) | +| 200 | Success | **application/json**: [EducationStatusResponse](#educationstatusresponse)
| -#### POST -##### Parameters +### [POST] /account/education +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [EducationActivatePayload](#educationactivatepayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [EducationActivatePayload](#educationactivatepayload)
| -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /account/education/autocomplete - -#### GET -##### Parameters +### [GET] /account/education/autocomplete +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [EducationAutocompleteQuery](#educationautocompletequery) | +| keywords | query | | Yes | string | +| limit | query | | No | integer,
**Default:** 20 | +| page | query | | No | integer | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [EducationAutocompleteResponse](#educationautocompleteresponse) | +| 200 | Success | **application/json**: [EducationAutocompleteResponse](#educationautocompleteresponse)
| -### /account/education/verify - -#### GET -##### Responses +### [GET] /account/education/verify +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [EducationVerifyResponse](#educationverifyresponse) | +| 200 | Success | **application/json**: [EducationVerifyResponse](#educationverifyresponse)
| -### /account/init +### [POST] /account/init +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AccountInitPayload](#accountinitpayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AccountInitPayload](#accountinitpayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -### /account/integrates - -#### GET -##### Responses +### [GET] /account/integrates +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [AccountIntegrateListResponse](#accountintegratelistresponse) | +| 200 | Success | **application/json**: [AccountIntegrateListResponse](#accountintegratelistresponse)
| -### /account/interface-language +### [POST] /account/interface-language +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AccountInterfaceLanguagePayload](#accountinterfacelanguagepayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AccountInterfaceLanguagePayload](#accountinterfacelanguagepayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [Account](#account) | +| 200 | Success | **application/json**: [Account](#account)
| -### /account/interface-theme +### [POST] /account/interface-theme +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AccountInterfaceThemePayload](#accountinterfacethemepayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AccountInterfaceThemePayload](#accountinterfacethemepayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [Account](#account) | +| 200 | Success | **application/json**: [Account](#account)
| -### /account/name +### [POST] /account/name +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AccountNamePayload](#accountnamepayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AccountNamePayload](#accountnamepayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [Account](#account) | +| 200 | Success | **application/json**: [Account](#account)
| -### /account/password +### [POST] /account/password +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AccountPasswordPayload](#accountpasswordpayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AccountPasswordPayload](#accountpasswordpayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [Account](#account) | +| 200 | Success | **application/json**: [Account](#account)
| -### /account/profile - -#### GET -##### Responses +### [GET] /account/profile +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [Account](#account) | +| 200 | Success | **application/json**: [Account](#account)
| -### /account/timezone +### [POST] /account/timezone +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AccountTimezonePayload](#accounttimezonepayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AccountTimezonePayload](#accounttimezonepayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [Account](#account) | - -### /activate - -#### POST -##### Description +| 200 | Success | **application/json**: [Account](#account)
| +### [POST] /activate Activate account with invitation token -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ActivatePayload](#activatepayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ActivatePayload](#activatepayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Account activated successfully | [ActivationResponse](#activationresponse) | +| 200 | Account activated successfully | **application/json**: [ActivationResponse](#activationresponse)
| | 400 | Already activated or invalid token | | -### /activate/check - -#### GET -##### Description - +### [GET] /activate/check Check if activation token is valid -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ActivateCheckQuery](#activatecheckquery) | +| email | query | | No | | +| token | query | | Yes | string | +| workspace_id | query | | No | | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [ActivationCheckResponse](#activationcheckresponse) | +| 200 | Success | **application/json**: [ActivationCheckResponse](#activationcheckresponse)
| -### /agents - -#### GET -##### Parameters +### [GET] /agents +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | keyword | query | | No | string | -| limit | query | | No | integer | -| page | query | | No | integer | +| limit | query | | No | integer,
**Default:** 20 | +| page | query | | No | integer,
**Default:** 1 | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Agent roster list | [AgentRosterListResponse](#agentrosterlistresponse) | +| 200 | Agent roster list | **application/json**: [AgentRosterListResponse](#agentrosterlistresponse)
| -#### POST -##### Parameters +### [POST] /agents +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [RosterAgentCreatePayload](#rosteragentcreatepayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [RosterAgentCreatePayload](#rosteragentcreatepayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | Agent created | [AgentRosterResponse](#agentrosterresponse) | +| 201 | Agent created | **application/json**: [AgentRosterResponse](#agentrosterresponse)
| -### /agents/invite-options - -#### GET -##### Parameters +### [GET] /agents/invite-options +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | query | Workflow app id for in-current-workflow markers | No | string | | keyword | query | | No | string | -| limit | query | | No | integer | -| page | query | | No | integer | +| limit | query | | No | integer,
**Default:** 20 | +| page | query | | No | integer,
**Default:** 1 | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Agent invite options | [AgentInviteOptionsResponse](#agentinviteoptionsresponse) | +| 200 | Agent invite options | **application/json**: [AgentInviteOptionsResponse](#agentinviteoptionsresponse)
| -### /agents/{agent_id} - -#### DELETE -##### Parameters +### [DELETE] /agents/{agent_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | agent_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Agent archived | -#### GET -##### Parameters +### [GET] /agents/{agent_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | agent_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Agent detail | [AgentRosterResponse](#agentrosterresponse) | +| 200 | Agent detail | **application/json**: [AgentRosterResponse](#agentrosterresponse)
| -#### PATCH -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | | Yes | string | -| payload | body | | Yes | [RosterAgentUpdatePayload](#rosteragentupdatepayload) | - -##### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Agent updated | [AgentRosterResponse](#agentrosterresponse) | - -### /agents/{agent_id}/versions - -#### GET -##### Parameters +### [PATCH] /agents/{agent_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | agent_id | path | | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [RosterAgentUpdatePayload](#rosteragentupdatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Agent versions | [AgentConfigSnapshotListResponse](#agentconfigsnapshotlistresponse) | +| 200 | Agent updated | **application/json**: [AgentRosterResponse](#agentrosterresponse)
| -### /agents/{agent_id}/versions/{version_id} +### [GET] /agents/{agent_id}/versions +#### Parameters -#### GET -##### Parameters +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| agent_id | path | | Yes | string | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Agent versions | **application/json**: [AgentConfigSnapshotListResponse](#agentconfigsnapshotlistresponse)
| + +### [GET] /agents/{agent_id}/versions/{version_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | agent_id | path | | Yes | string | | version_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Agent version detail | [AgentConfigSnapshotDetailResponse](#agentconfigsnapshotdetailresponse) | +| 200 | Agent version detail | **application/json**: [AgentConfigSnapshotDetailResponse](#agentconfigsnapshotdetailresponse)
| -### /all-workspaces - -#### GET -##### Parameters +### [GET] /all-workspaces +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkspaceListQuery](#workspacelistquery) | +| limit | query | | No | integer,
**Default:** 20 | +| page | query | | No | integer,
**Default:** 1 | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /api-based-extension - -#### GET -##### Description - +### [GET] /api-based-extension Get all API-based extensions for current tenant -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [APIBasedExtensionListResponse](#apibasedextensionlistresponse) | - -#### POST -##### Description +| 200 | Success | **application/json**: [APIBasedExtensionListResponse](#apibasedextensionlistresponse)
| +### [POST] /api-based-extension Create a new API-based extension -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [APIBasedExtensionPayload](#apibasedextensionpayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [APIBasedExtensionPayload](#apibasedextensionpayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | Extension created successfully | [APIBasedExtensionResponse](#apibasedextensionresponse) | - -### /api-based-extension/{id} - -#### DELETE -##### Description +| 201 | Extension created successfully | **application/json**: [APIBasedExtensionResponse](#apibasedextensionresponse)
| +### [DELETE] /api-based-extension/{id} Delete API-based extension -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | id | path | Extension ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Extension deleted successfully | -#### GET -##### Description - +### [GET] /api-based-extension/{id} Get API-based extension by ID -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | id | path | Extension ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [APIBasedExtensionResponse](#apibasedextensionresponse) | - -#### POST -##### Description +| 200 | Success | **application/json**: [APIBasedExtensionResponse](#apibasedextensionresponse)
| +### [POST] /api-based-extension/{id} Update API-based extension -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [APIBasedExtensionPayload](#apibasedextensionpayload) | | id | path | Extension ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [APIBasedExtensionPayload](#apibasedextensionpayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Extension updated successfully | [APIBasedExtensionResponse](#apibasedextensionresponse) | +| 200 | Extension updated successfully | **application/json**: [APIBasedExtensionResponse](#apibasedextensionresponse)
| -### /api-key-auth/data-source - -#### GET -##### Responses +### [GET] /api-key-auth/data-source +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [ApiKeyAuthDataSourceListResponse](#apikeyauthdatasourcelistresponse) | +| 200 | Success | **application/json**: [ApiKeyAuthDataSourceListResponse](#apikeyauthdatasourcelistresponse)
| -### /api-key-auth/data-source/binding +### [POST] /api-key-auth/data-source/binding +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ApiKeyAuthBindingPayload](#apikeyauthbindingpayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ApiKeyAuthBindingPayload](#apikeyauthbindingpayload) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /api-key-auth/data-source/{binding_id} - -#### DELETE -##### Parameters +### [DELETE] /api-key-auth/data-source/{binding_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | binding_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Binding deleted successfully | -### /app-dsl-version - -#### GET -##### Summary - -Get current app DSL version for workflow clipboard compatibility - -##### Description +### [GET] /app-dsl-version +**Get current app DSL version for workflow clipboard compatibility** Get current app DSL version -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [AppDslVersionResponse](#appdslversionresponse) | - -### /app/prompt-templates - -#### GET -##### Description +| 200 | Success | **application/json**: [AppDslVersionResponse](#appdslversionresponse)
| +### [GET] /app/prompt-templates Get advanced prompt templates based on app mode and model configuration -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AdvancedPromptTemplateQuery](#advancedprompttemplatequery) | +| app_mode | query | Application mode | Yes | string | +| has_context | query | Whether has context | No | string,
**Default:** true | +| model_mode | query | Model mode | Yes | string | +| model_name | query | Model name | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Prompt templates retrieved successfully | [ object ] | +| 200 | Prompt templates retrieved successfully | **application/json**: [ object ]
| | 400 | Invalid request parameters | | -### /apps - -#### GET -##### Summary - -Get app list - -##### Description +### [GET] /apps +**Get app list** Get list of applications with pagination and filtering -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AppListQuery](#applistquery) | +| creator_ids | query | Filter by creator account IDs | No | | +| is_created_by_me | query | Filter by creator | No | | +| limit | query | Page size (1-100) | No | integer,
**Default:** 20 | +| mode | query | App mode filter | No | string,
**Available values:** "advanced-chat", "agent", "agent-chat", "all", "channel", "chat", "completion", "workflow",
**Default:** all | +| name | query | Filter by app name | No | | +| page | query | Page number (1-99999) | No | integer,
**Default:** 1 | +| tag_ids | query | Filter by tag IDs | No | | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [AppPagination](#apppagination) | +| 200 | Success | **application/json**: [AppPagination](#apppagination)
| -#### POST -##### Summary - -Create app - -##### Description +### [POST] /apps +**Create app** Create a new application -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [CreateAppPayload](#createapppayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [CreateAppPayload](#createapppayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | App created successfully | [AppDetail](#appdetail) | +| 201 | App created successfully | **application/json**: [AppDetail](#appdetail)
| | 400 | Invalid request parameters | | | 403 | Insufficient permissions | | -### /apps/imports +### [POST] /apps/imports +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AppImportPayload](#appimportpayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AppImportPayload](#appimportpayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Import completed | [Import](#import) | -| 202 | Import pending confirmation | [Import](#import) | -| 400 | Import failed | [Import](#import) | +| 200 | Import completed | **application/json**: [Import](#import)
| +| 202 | Import pending confirmation | **application/json**: [Import](#import)
| +| 400 | Import failed | **application/json**: [Import](#import)
| -### /apps/imports/{app_id}/check-dependencies - -#### GET -##### Parameters +### [GET] /apps/imports/{app_id}/check-dependencies +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Dependencies checked | [CheckDependenciesResult](#checkdependenciesresult) | +| 200 | Dependencies checked | **application/json**: [CheckDependenciesResult](#checkdependenciesresult)
| -### /apps/imports/{import_id}/confirm - -#### POST -##### Parameters +### [POST] /apps/imports/{import_id}/confirm +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | import_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Import confirmed | [Import](#import) | -| 400 | Import failed | [Import](#import) | - -### /apps/workflows/online-users - -#### POST -##### Description +| 200 | Import confirmed | **application/json**: [Import](#import)
| +| 400 | Import failed | **application/json**: [Import](#import)
| +### [POST] /apps/workflows/online-users Get workflow online users -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkflowOnlineUsersPayload](#workflowonlineuserspayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [WorkflowOnlineUsersPayload](#workflowonlineuserspayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow online users retrieved successfully | [WorkflowOnlineUsersResponse](#workflowonlineusersresponse) | +| 200 | Workflow online users retrieved successfully | **application/json**: [WorkflowOnlineUsersResponse](#workflowonlineusersresponse)
| -### /apps/{app_id} - -#### DELETE -##### Summary - -Delete app - -##### Description +### [DELETE] /apps/{app_id} +**Delete app** Delete application -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | App deleted successfully | | 403 | Insufficient permissions | -#### GET -##### Summary - -Get app detail - -##### Description +### [GET] /apps/{app_id} +**Get app detail** Get application details -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [AppDetailWithSite](#appdetailwithsite) | +| 200 | Success | **application/json**: [AppDetailWithSite](#appdetailwithsite)
| -#### PUT -##### Summary - -Update app - -##### Description +### [PUT] /apps/{app_id} +**Update app** Update application details -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [UpdateAppPayload](#updateapppayload) | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [UpdateAppPayload](#updateapppayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | App updated successfully | [AppDetailWithSite](#appdetailwithsite) | +| 200 | App updated successfully | **application/json**: [AppDetailWithSite](#appdetailwithsite)
| | 400 | Invalid request parameters | | | 403 | Insufficient permissions | | -### /apps/{app_id}/advanced-chat/workflow-runs - -#### GET -##### Summary - -Get advanced chat app workflow run list - -##### Description +### [GET] /apps/{app_id}/advanced-chat/workflow-runs +**Get advanced chat app workflow run list** Get advanced chat workflow run list -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | last_id | query | Last run ID for pagination | No | string | -| limit | query | Number of items per page (1-100) | No | integer | -| status | query | Workflow run status filter | No | string | -| triggered_from | query | Filter by trigger source: debugging or app-run. Default: debugging | No | string | +| limit | query | Number of items per page (1-100) | No | integer,
**Default:** 20 | +| status | query | Workflow run status filter | No | string,
**Available values:** "failed", "partial-succeeded", "running", "stopped", "succeeded" | +| triggered_from | query | Filter by trigger source: debugging or app-run. Default: debugging | No | string,
**Available values:** "app-run", "debugging" | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow runs retrieved successfully | [AdvancedChatWorkflowRunPaginationResponse](#advancedchatworkflowrunpaginationresponse) | +| 200 | Workflow runs retrieved successfully | **application/json**: [AdvancedChatWorkflowRunPaginationResponse](#advancedchatworkflowrunpaginationresponse)
| -### /apps/{app_id}/advanced-chat/workflow-runs/count +### [GET] /apps/{app_id}/advanced-chat/workflow-runs/count +**Get advanced chat workflow runs count statistics** -#### GET -##### Summary - -Get advanced chat workflow runs count statistics - -##### Description - -Get advanced chat workflow runs count statistics - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -| status | query | Workflow run status filter | No | string | +| status | query | Workflow run status filter | No | string,
**Available values:** "failed", "partial-succeeded", "running", "stopped", "succeeded" | | time_range | query | Filter by time range (optional): e.g., 7d (7 days), 4h (4 hours), 30m (30 minutes), 30s (30 seconds). Filters by created_at field. | No | string | -| triggered_from | query | Filter by trigger source: debugging or app-run. Default: debugging | No | string | +| triggered_from | query | Filter by trigger source: debugging or app-run. Default: debugging | No | string,
**Available values:** "app-run", "debugging" | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow runs count retrieved successfully | [WorkflowRunCountResponse](#workflowruncountresponse) | +| 200 | Workflow runs count retrieved successfully | **application/json**: [WorkflowRunCountResponse](#workflowruncountresponse)
| -### /apps/{app_id}/advanced-chat/workflows/draft/human-input/nodes/{node_id}/form/preview - -#### POST -##### Summary - -Preview human input form content and placeholders - -##### Description +### [POST] /apps/{app_id}/advanced-chat/workflows/draft/human-input/nodes/{node_id}/form/preview +**Preview human input form content and placeholders** Get human input form preview for advanced chat workflow -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [HumanInputFormPreviewPayload](#humaninputformpreviewpayload) | | app_id | path | Application ID | Yes | string | | node_id | path | Node ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [HumanInputFormPreviewPayload](#humaninputformpreviewpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /apps/{app_id}/advanced-chat/workflows/draft/human-input/nodes/{node_id}/form/run - -#### POST -##### Summary - -Submit human input form preview - -##### Description +### [POST] /apps/{app_id}/advanced-chat/workflows/draft/human-input/nodes/{node_id}/form/run +**Submit human input form preview** Submit human input form preview for advanced chat workflow -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [HumanInputFormSubmitPayload](#humaninputformsubmitpayload) | | app_id | path | Application ID | Yes | string | | node_id | path | Node ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [HumanInputFormSubmitPayload](#humaninputformsubmitpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /apps/{app_id}/advanced-chat/workflows/draft/iteration/nodes/{node_id}/run - -#### POST -##### Summary - -Run draft workflow iteration node - -##### Description +### [POST] /apps/{app_id}/advanced-chat/workflows/draft/iteration/nodes/{node_id}/run +**Run draft workflow iteration node** Run draft workflow iteration node for advanced chat -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [IterationNodeRunPayload](#iterationnoderunpayload) | | app_id | path | Application ID | Yes | string | | node_id | path | Node ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [IterationNodeRunPayload](#iterationnoderunpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | @@ -948,26 +833,25 @@ Run draft workflow iteration node for advanced chat | 403 | Permission denied | | 404 | Node not found | -### /apps/{app_id}/advanced-chat/workflows/draft/loop/nodes/{node_id}/run - -#### POST -##### Summary - -Run draft workflow loop node - -##### Description +### [POST] /apps/{app_id}/advanced-chat/workflows/draft/loop/nodes/{node_id}/run +**Run draft workflow loop node** Run draft workflow loop node for advanced chat -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [LoopNodeRunPayload](#loopnoderunpayload) | | app_id | path | Application ID | Yes | string | | node_id | path | Node ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [LoopNodeRunPayload](#loopnoderunpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | @@ -975,25 +859,24 @@ Run draft workflow loop node for advanced chat | 403 | Permission denied | | 404 | Node not found | -### /apps/{app_id}/advanced-chat/workflows/draft/run - -#### POST -##### Summary - -Run draft workflow - -##### Description +### [POST] /apps/{app_id}/advanced-chat/workflows/draft/run +**Run draft workflow** Run draft workflow for advanced chat application -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AdvancedChatWorkflowRunPayload](#advancedchatworkflowrunpayload) | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AdvancedChatWorkflowRunPayload](#advancedchatworkflowrunpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | @@ -1001,137 +884,130 @@ Run draft workflow for advanced chat application | 400 | Invalid request parameters | | 403 | Permission denied | -### /apps/{app_id}/agent-composer - -#### GET -##### Parameters +### [GET] /apps/{app_id}/agent-composer +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Agent app composer state | [AgentAppComposerResponse](#agentappcomposerresponse) | +| 200 | Agent app composer state | **application/json**: [AgentAppComposerResponse](#agentappcomposerresponse)
| -#### PUT -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | -| payload | body | | Yes | [ComposerSavePayload](#composersavepayload) | - -##### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Agent app composer saved | [AgentAppComposerResponse](#agentappcomposerresponse) | - -### /apps/{app_id}/agent-composer/candidates - -#### GET -##### Parameters +### [PUT] /apps/{app_id}/agent-composer +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ComposerSavePayload](#composersavepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Agent app composer candidates | [AgentComposerCandidatesResponse](#agentcomposercandidatesresponse) | +| 200 | Agent app composer saved | **application/json**: [AgentAppComposerResponse](#agentappcomposerresponse)
| -### /apps/{app_id}/agent-composer/validate - -#### POST -##### Parameters +### [GET] /apps/{app_id}/agent-composer/candidates +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | -| payload | body | | Yes | [ComposerSavePayload](#composersavepayload) | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Agent app composer validation result | [AgentComposerValidateResponse](#agentcomposervalidateresponse) | +| 200 | Agent app composer candidates | **application/json**: [AgentComposerCandidatesResponse](#agentcomposercandidatesresponse)
| -### /apps/{app_id}/agent-features +### [POST] /apps/{app_id}/agent-composer/validate +#### Parameters -#### POST -##### Description +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| app_id | path | | Yes | string | +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ComposerSavePayload](#composersavepayload)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Agent app composer validation result | **application/json**: [AgentComposerValidateResponse](#agentcomposervalidateresponse)
| + +### [POST] /apps/{app_id}/agent-features Update an Agent App's presentation features (opener, follow-up, citations, ...) -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AgentAppFeaturesPayload](#agentappfeaturespayload) | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AgentAppFeaturesPayload](#agentappfeaturespayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Features updated successfully | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Features updated successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| | 400 | Invalid configuration | | | 404 | App not found | | -### /apps/{app_id}/agent-referencing-workflows - -#### GET -##### Description - +### [GET] /apps/{app_id}/agent-referencing-workflows List workflow apps that reference this Agent App's bound Agent (read-only) -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Referencing workflows listed successfully | [AgentReferencingWorkflowsResponse](#agentreferencingworkflowsresponse) | +| 200 | Referencing workflows listed successfully | **application/json**: [AgentReferencingWorkflowsResponse](#agentreferencingworkflowsresponse)
| | 404 | App not found | | -### /apps/{app_id}/agent-sandbox/files - -#### GET -##### Description - +### [GET] /apps/{app_id}/agent-sandbox/files List a directory in an Agent App conversation sandbox -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | conversation_id | query | Agent App conversation ID | Yes | string | -| path | query | Directory path relative to the sandbox workspace | No | string | +| path | query | Directory path relative to the sandbox workspace | No | string,
**Default:** . | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Listing returned | [SandboxListResponse](#sandboxlistresponse) | - -### /apps/{app_id}/agent-sandbox/files/read - -#### GET -##### Description +| 200 | Listing returned | **application/json**: [SandboxListResponse](#sandboxlistresponse)
| +### [GET] /apps/{app_id}/agent-sandbox/files/read Read a text/binary preview file in an Agent App conversation sandbox -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -1139,137 +1015,118 @@ Read a text/binary preview file in an Agent App conversation sandbox | conversation_id | query | Agent App conversation ID | Yes | string | | path | query | File path relative to the sandbox workspace | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Preview returned | [SandboxReadResponse](#sandboxreadresponse) | - -### /apps/{app_id}/agent-sandbox/files/upload - -#### POST -##### Description +| 200 | Preview returned | **application/json**: [SandboxReadResponse](#sandboxreadresponse)
| +### [POST] /apps/{app_id}/agent-sandbox/files/upload Upload one Agent App sandbox file as a Dify ToolFile mapping -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | -| payload | body | | Yes | [AgentSandboxUploadPayload](#agentsandboxuploadpayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AgentSandboxUploadPayload](#agentsandboxuploadpayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Uploaded | [SandboxUploadResponse](#sandboxuploadresponse) | +| 200 | Uploaded | **application/json**: [SandboxUploadResponse](#sandboxuploadresponse)
| -### /apps/{app_id}/agent/logs - -#### GET -##### Summary - -Get agent logs - -##### Description +### [GET] /apps/{app_id}/agent/logs +**Get agent logs** Get agent execution logs for an application -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AgentLogQuery](#agentlogquery) | | app_id | path | Application ID | Yes | string | +| conversation_id | query | Conversation UUID | Yes | string | +| message_id | query | Message UUID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Agent logs retrieved successfully | [ object ] | +| 200 | Agent logs retrieved successfully | **application/json**: [ object ]
| | 400 | Invalid request parameters | | -### /apps/{app_id}/agent/skills/standardize - -#### POST -##### Summary - -Upload a Skill, validate it, and standardize it into the app agent's drive - -##### Description +### [POST] /apps/{app_id}/agent/skills/standardize +**Upload a Skill, validate it, and standardize it into the app agent's drive** Validate + standardize a Skill into the agent drive (ENG-594) -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 201 | Skill standardized into drive | | 400 | Invalid skill package or no bound agent | -### /apps/{app_id}/agent/skills/upload - -#### POST -##### Summary - -Validate an uploaded Skill package and persist the archive - -##### Description +### [POST] /apps/{app_id}/agent/skills/upload +**Validate an uploaded Skill package and persist the archive** Upload + validate a Skill package (.zip/.skill) and extract its manifest Returns a validated skill ref (to bind into the Agent soul config on save) plus its manifest. Standardizing into the agent drive is ENG-594. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 201 | Skill validated | | 400 | Invalid skill package | -### /apps/{app_id}/annotation-reply/{action} - -#### POST -##### Description - +### [POST] /apps/{app_id}/annotation-reply/{action} Enable or disable annotation reply for an app -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AnnotationReplyPayload](#annotationreplypayload) | | action | path | Action to perform (enable/disable) | Yes | string | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AnnotationReplyPayload](#annotationreplypayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Action completed successfully | | 403 | Insufficient permissions | -### /apps/{app_id}/annotation-reply/{action}/status/{job_id} - -#### GET -##### Description - +### [GET] /apps/{app_id}/annotation-reply/{action}/status/{job_id} Get status of annotation reply action job -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -1277,122 +1134,116 @@ Get status of annotation reply action job | app_id | path | Application ID | Yes | string | | job_id | path | Job ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Job status retrieved successfully | | 403 | Insufficient permissions | -### /apps/{app_id}/annotation-setting - -#### GET -##### Description - +### [GET] /apps/{app_id}/annotation-setting Get annotation settings for an app -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Annotation settings retrieved successfully | | 403 | Insufficient permissions | -### /apps/{app_id}/annotation-settings/{annotation_setting_id} - -#### POST -##### Description - +### [POST] /apps/{app_id}/annotation-settings/{annotation_setting_id} Update annotation settings for an app -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AnnotationSettingUpdatePayload](#annotationsettingupdatepayload) | | annotation_setting_id | path | Annotation setting ID | Yes | string | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AnnotationSettingUpdatePayload](#annotationsettingupdatepayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Settings updated successfully | | 403 | Insufficient permissions | -### /apps/{app_id}/annotations - -#### DELETE -##### Parameters +### [DELETE] /apps/{app_id}/annotations +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -#### GET -##### Description - +### [GET] /apps/{app_id}/annotations Get annotations for an app with pagination -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AnnotationListQuery](#annotationlistquery) | | app_id | path | Application ID | Yes | string | +| keyword | query | Search keyword | No | string | +| limit | query | Page size | No | integer,
**Default:** 20 | +| page | query | Page number | No | integer,
**Default:** 1 | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Annotations retrieved successfully | | 403 | Insufficient permissions | -#### POST -##### Description - +### [POST] /apps/{app_id}/annotations Create a new annotation for an app -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [CreateAnnotationPayload](#createannotationpayload) | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [CreateAnnotationPayload](#createannotationpayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | Annotation created successfully | [Annotation](#annotation) | +| 201 | Annotation created successfully | **application/json**: [Annotation](#annotation)
| | 403 | Insufficient permissions | | -### /apps/{app_id}/annotations/batch-import - -#### POST -##### Description - +### [POST] /apps/{app_id}/annotations/batch-import Batch import annotations from CSV file with rate limiting and security checks -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -1402,204 +1253,184 @@ Batch import annotations from CSV file with rate limiting and security checks | 413 | File too large | | 429 | Too many requests or concurrent imports | -### /apps/{app_id}/annotations/batch-import-status/{job_id} - -#### GET -##### Description - +### [GET] /apps/{app_id}/annotations/batch-import-status/{job_id} Get status of batch import job -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | job_id | path | Job ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Job status retrieved successfully | | 403 | Insufficient permissions | -### /apps/{app_id}/annotations/count - -#### GET -##### Description - +### [GET] /apps/{app_id}/annotations/count Get count of message annotations for the app -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Annotation count retrieved successfully | [AnnotationCountResponse](#annotationcountresponse) | - -### /apps/{app_id}/annotations/export - -#### GET -##### Description +| 200 | Annotation count retrieved successfully | **application/json**: [AnnotationCountResponse](#annotationcountresponse)
| +### [GET] /apps/{app_id}/annotations/export Export all annotations for an app with CSV injection protection -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Annotations exported successfully | [AnnotationExportList](#annotationexportlist) | +| 200 | Annotations exported successfully | **application/json**: [AnnotationExportList](#annotationexportlist)
| | 403 | Insufficient permissions | | -### /apps/{app_id}/annotations/{annotation_id} - -#### DELETE -##### Parameters +### [DELETE] /apps/{app_id}/annotations/{annotation_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | annotation_id | path | | Yes | string | | app_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -#### POST -##### Description - +### [POST] /apps/{app_id}/annotations/{annotation_id} Update or delete an annotation -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [UpdateAnnotationPayload](#updateannotationpayload) | | annotation_id | path | Annotation ID | Yes | string | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [UpdateAnnotationPayload](#updateannotationpayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Annotation updated successfully | [Annotation](#annotation) | +| 200 | Annotation updated successfully | **application/json**: [Annotation](#annotation)
| | 204 | Annotation deleted successfully | | | 403 | Insufficient permissions | | -### /apps/{app_id}/annotations/{annotation_id}/hit-histories - -#### GET -##### Description - +### [GET] /apps/{app_id}/annotations/{annotation_id}/hit-histories Get hit histories for an annotation -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | annotation_id | path | Annotation ID | Yes | string | | app_id | path | Application ID | Yes | string | -| limit | query | Page size | No | integer | -| page | query | Page number | No | integer | +| limit | query | Page size | No | integer,
**Default:** 20 | +| page | query | Page number | No | integer,
**Default:** 1 | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Hit histories retrieved successfully | [AnnotationHitHistoryList](#annotationhithistorylist) | +| 200 | Hit histories retrieved successfully | **application/json**: [AnnotationHitHistoryList](#annotationhithistorylist)
| | 403 | Insufficient permissions | | -### /apps/{app_id}/api-enable - -#### POST -##### Description - +### [POST] /apps/{app_id}/api-enable Enable or disable app API -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AppApiStatusPayload](#appapistatuspayload) | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AppApiStatusPayload](#appapistatuspayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | API status updated successfully | [AppDetail](#appdetail) | +| 200 | API status updated successfully | **application/json**: [AppDetail](#appdetail)
| | 403 | Insufficient permissions | | -### /apps/{app_id}/audio-to-text - -#### POST -##### Description - +### [POST] /apps/{app_id}/audio-to-text Transcript audio to text for chat messages -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | App ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Audio transcription successful | [AudioTranscriptResponse](#audiotranscriptresponse) | +| 200 | Audio transcription successful | **application/json**: [AudioTranscriptResponse](#audiotranscriptresponse)
| | 400 | Bad request - No audio uploaded or unsupported type | | | 413 | Audio file too large | | -### /apps/{app_id}/chat-conversations - -#### GET -##### Description - +### [GET] /apps/{app_id}/chat-conversations Get chat conversations with pagination, filtering and summary -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ChatConversationQuery](#chatconversationquery) | | app_id | path | Application ID | Yes | string | +| annotation_status | query | Annotation status filter | No | string,
**Available values:** "all", "annotated", "not_annotated",
**Default:** all | +| end | query | End date (YYYY-MM-DD HH:MM) | No | | +| keyword | query | Search keyword | No | | +| limit | query | Page size (1-100) | No | integer,
**Default:** 20 | +| page | query | Page number | No | integer,
**Default:** 1 | +| sort_by | query | Sort field and direction | No | string,
**Available values:** "-created_at", "-updated_at", "created_at", "updated_at",
**Default:** -updated_at | +| start | query | Start date (YYYY-MM-DD HH:MM) | No | | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [ConversationWithSummaryPagination](#conversationwithsummarypagination) | +| 200 | Success | **application/json**: [ConversationWithSummaryPagination](#conversationwithsummarypagination)
| | 403 | Insufficient permissions | | -### /apps/{app_id}/chat-conversations/{conversation_id} - -#### DELETE -##### Description - +### [DELETE] /apps/{app_id}/chat-conversations/{conversation_id} Delete a chat conversation -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | conversation_id | path | Conversation ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -1607,124 +1438,109 @@ Delete a chat conversation | 403 | Insufficient permissions | | 404 | Conversation not found | -#### GET -##### Description - +### [GET] /apps/{app_id}/chat-conversations/{conversation_id} Get chat conversation details -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | conversation_id | path | Conversation ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [ConversationDetail](#conversationdetail) | +| 200 | Success | **application/json**: [ConversationDetail](#conversationdetail)
| | 403 | Insufficient permissions | | | 404 | Conversation not found | | -### /apps/{app_id}/chat-messages - -#### GET -##### Description - +### [GET] /apps/{app_id}/chat-messages Get chat messages for a conversation with pagination -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ChatMessagesQuery](#chatmessagesquery) | | app_id | path | Application ID | Yes | string | +| conversation_id | query | Conversation ID | Yes | string | +| first_id | query | First message ID for pagination | No | | +| limit | query | Number of messages to return (1-100) | No | integer,
**Default:** 20 | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [MessageInfiniteScrollPaginationResponse](#messageinfinitescrollpaginationresponse) | +| 200 | Success | **application/json**: [MessageInfiniteScrollPaginationResponse](#messageinfinitescrollpaginationresponse)
| | 404 | Conversation not found | | -### /apps/{app_id}/chat-messages/{message_id}/suggested-questions - -#### GET -##### Description - +### [GET] /apps/{app_id}/chat-messages/{message_id}/suggested-questions Get suggested questions for a message -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | message_id | path | Message ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Suggested questions retrieved successfully | [SuggestedQuestionsResponse](#suggestedquestionsresponse) | +| 200 | Suggested questions retrieved successfully | **application/json**: [SuggestedQuestionsResponse](#suggestedquestionsresponse)
| | 404 | Message or conversation not found | | -### /apps/{app_id}/chat-messages/{task_id}/stop - -#### POST -##### Description - +### [POST] /apps/{app_id}/chat-messages/{task_id}/stop Stop a running chat message generation -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | task_id | path | Task ID to stop | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Task stopped successfully | [SimpleResultResponse](#simpleresultresponse) | - -### /apps/{app_id}/completion-conversations - -#### GET -##### Description +| 200 | Task stopped successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| +### [GET] /apps/{app_id}/completion-conversations Get completion conversations with pagination and filtering -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [CompletionConversationQuery](#completionconversationquery) | | app_id | path | Application ID | Yes | string | +| annotation_status | query | Annotation status filter | No | string,
**Available values:** "all", "annotated", "not_annotated",
**Default:** all | +| end | query | End date (YYYY-MM-DD HH:MM) | No | | +| keyword | query | Search keyword | No | | +| limit | query | Page size (1-100) | No | integer,
**Default:** 20 | +| page | query | Page number | No | integer,
**Default:** 1 | +| start | query | Start date (YYYY-MM-DD HH:MM) | No | | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [ConversationPagination](#conversationpagination) | +| 200 | Success | **application/json**: [ConversationPagination](#conversationpagination)
| | 403 | Insufficient permissions | | -### /apps/{app_id}/completion-conversations/{conversation_id} - -#### DELETE -##### Description - +### [DELETE] /apps/{app_id}/completion-conversations/{conversation_id} Delete a completion conversation -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | conversation_id | path | Conversation ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -1732,41 +1548,40 @@ Delete a completion conversation | 403 | Insufficient permissions | | 404 | Conversation not found | -#### GET -##### Description - +### [GET] /apps/{app_id}/completion-conversations/{conversation_id} Get completion conversation details with messages -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | conversation_id | path | Conversation ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [ConversationMessageDetail](#conversationmessagedetail) | +| 200 | Success | **application/json**: [ConversationMessageDetail](#conversationmessagedetail)
| | 403 | Insufficient permissions | | | 404 | Conversation not found | | -### /apps/{app_id}/completion-messages - -#### POST -##### Description - +### [POST] /apps/{app_id}/completion-messages Generate completion message for debugging -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [CompletionMessagePayload](#completionmessagepayload) | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [CompletionMessagePayload](#completionmessagepayload)
| + +#### Responses | Code | Description | | ---- | ----------- | @@ -1774,161 +1589,148 @@ Generate completion message for debugging | 400 | Invalid request parameters | | 404 | App not found | -### /apps/{app_id}/completion-messages/{task_id}/stop - -#### POST -##### Description - +### [POST] /apps/{app_id}/completion-messages/{task_id}/stop Stop a running completion message generation -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | task_id | path | Task ID to stop | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Task stopped successfully | [SimpleResultResponse](#simpleresultresponse) | - -### /apps/{app_id}/conversation-variables - -#### GET -##### Description +| 200 | Task stopped successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| +### [GET] /apps/{app_id}/conversation-variables Get conversation variables for an application -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ConversationVariablesQuery](#conversationvariablesquery) | | app_id | path | Application ID | Yes | string | +| conversation_id | query | Conversation ID to filter variables | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Conversation variables retrieved successfully | [PaginatedConversationVariableResponse](#paginatedconversationvariableresponse) | +| 200 | Conversation variables retrieved successfully | **application/json**: [PaginatedConversationVariableResponse](#paginatedconversationvariableresponse)
| -### /apps/{app_id}/convert-to-workflow - -#### POST -##### Summary - -Convert basic mode of chatbot app to workflow mode - -##### Description +### [POST] /apps/{app_id}/convert-to-workflow +**Convert basic mode of chatbot app to workflow mode** Convert application to workflow mode Convert expert mode of chatbot app to workflow mode Convert Completion App to Workflow App -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ConvertToWorkflowPayload](#converttoworkflowpayload) | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ConvertToWorkflowPayload](#converttoworkflowpayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Application converted to workflow successfully | [NewAppResponse](#newappresponse) | +| 200 | Application converted to workflow successfully | **application/json**: [NewAppResponse](#newappresponse)
| | 400 | Application cannot be converted | | | 403 | Permission denied | | -### /apps/{app_id}/copy - -#### POST -##### Summary - -Copy app - -##### Description +### [POST] /apps/{app_id}/copy +**Copy app** Create a copy of an existing application -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [CopyAppPayload](#copyapppayload) | | app_id | path | Application ID to copy | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [CopyAppPayload](#copyapppayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | App copied successfully | [AppDetailWithSite](#appdetailwithsite) | +| 201 | App copied successfully | **application/json**: [AppDetailWithSite](#appdetailwithsite)
| | 403 | Insufficient permissions | | -### /apps/{app_id}/export - -#### GET -##### Summary - -Export app - -##### Description +### [GET] /apps/{app_id}/export +**Export app** Export application configuration as DSL -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AppExportQuery](#appexportquery) | | app_id | path | Application ID to export | Yes | string | +| include_secret | query | Include secrets in export | No | boolean | +| workflow_id | query | Specific workflow ID to export | No | | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | App exported successfully | [AppExportResponse](#appexportresponse) | +| 200 | App exported successfully | **application/json**: [AppExportResponse](#appexportresponse)
| | 403 | Insufficient permissions | | -### /apps/{app_id}/feedbacks - -#### POST -##### Description - +### [POST] /apps/{app_id}/feedbacks Create or update message feedback (like/dislike) -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [MessageFeedbackPayload](#messagefeedbackpayload) | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [MessageFeedbackPayload](#messagefeedbackpayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Feedback updated successfully | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Feedback updated successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| | 403 | Insufficient permissions | | | 404 | Message not found | | -### /apps/{app_id}/feedbacks/export - -#### GET -##### Description - +### [GET] /apps/{app_id}/feedbacks/export Export user feedback data for Google Sheets -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [FeedbackExportQuery](#feedbackexportquery) | | app_id | path | Application ID | Yes | string | +| end_date | query | End date (YYYY-MM-DD) | No | | +| format | query | Export format | No | string,
**Available values:** "csv", "json",
**Default:** csv | +| from_source | query | Filter by feedback source | No | | +| has_comment | query | Only include feedback with comments | No | | +| rating | query | Filter by rating | No | | +| start_date | query | Start date (YYYY-MM-DD) | No | | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -1936,67 +1738,63 @@ Export user feedback data for Google Sheets | 400 | Invalid parameters | | 500 | Internal server error | -### /apps/{app_id}/icon - -#### POST -##### Description - +### [POST] /apps/{app_id}/icon Update application icon -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AppIconPayload](#appiconpayload) | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AppIconPayload](#appiconpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Icon updated successfully | | 403 | Insufficient permissions | -### /apps/{app_id}/messages/{message_id} - -#### GET -##### Description - +### [GET] /apps/{app_id}/messages/{message_id} Get message details by ID -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | message_id | path | Message ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Message retrieved successfully | [MessageDetailResponse](#messagedetailresponse) | +| 200 | Message retrieved successfully | **application/json**: [MessageDetailResponse](#messagedetailresponse)
| | 404 | Message not found | | -### /apps/{app_id}/model-config - -#### POST -##### Summary - -Modify app model config - -##### Description +### [POST] /apps/{app_id}/model-config +**Modify app model config** Update application model configuration -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ModelConfigRequest](#modelconfigrequest) | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ModelConfigRequest](#modelconfigrequest)
| + +#### Responses | Code | Description | | ---- | ----------- | @@ -2004,745 +1802,666 @@ Update application model configuration | 400 | Invalid configuration | | 404 | App not found | -### /apps/{app_id}/name - -#### POST -##### Description - +### [POST] /apps/{app_id}/name Check if app name is available -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AppNamePayload](#appnamepayload) | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AppNamePayload](#appnamepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Name availability checked | [AppDetail](#appdetail) | +| 200 | Name availability checked | **application/json**: [AppDetail](#appdetail)
| -### /apps/{app_id}/publish-to-creators-platform +### [POST] /apps/{app_id}/publish-to-creators-platform +**Publish app to Creators Platform** -#### POST -##### Summary - -Publish app to Creators Platform - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [RedirectUrlResponse](#redirecturlresponse) | - -### /apps/{app_id}/server - -#### GET -##### Description +| 200 | Success | **application/json**: [RedirectUrlResponse](#redirecturlresponse)
| +### [GET] /apps/{app_id}/server Get MCP server configuration for an application -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | MCP server configuration retrieved successfully | [AppMCPServerResponse](#appmcpserverresponse) | - -#### POST -##### Description +| 200 | MCP server configuration retrieved successfully | **application/json**: [AppMCPServerResponse](#appmcpserverresponse)
| +### [POST] /apps/{app_id}/server Create MCP server configuration for an application -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [MCPServerCreatePayload](#mcpservercreatepayload) | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [MCPServerCreatePayload](#mcpservercreatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | MCP server configuration created successfully | [AppMCPServerResponse](#appmcpserverresponse) | +| 201 | MCP server configuration created successfully | **application/json**: [AppMCPServerResponse](#appmcpserverresponse)
| | 403 | Insufficient permissions | | -#### PUT -##### Description - +### [PUT] /apps/{app_id}/server Update MCP server configuration for an application -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [MCPServerUpdatePayload](#mcpserverupdatepayload) | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [MCPServerUpdatePayload](#mcpserverupdatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | MCP server configuration updated successfully | [AppMCPServerResponse](#appmcpserverresponse) | +| 200 | MCP server configuration updated successfully | **application/json**: [AppMCPServerResponse](#appmcpserverresponse)
| | 403 | Insufficient permissions | | | 404 | Server not found | | -### /apps/{app_id}/site - -#### POST -##### Description - +### [POST] /apps/{app_id}/site Update application site configuration -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AppSiteUpdatePayload](#appsiteupdatepayload) | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AppSiteUpdatePayload](#appsiteupdatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Site configuration updated successfully | [AppSiteResponse](#appsiteresponse) | +| 200 | Site configuration updated successfully | **application/json**: [AppSiteResponse](#appsiteresponse)
| | 403 | Insufficient permissions | | | 404 | App not found | | -### /apps/{app_id}/site-enable - -#### POST -##### Description - +### [POST] /apps/{app_id}/site-enable Enable or disable app site -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AppSiteStatusPayload](#appsitestatuspayload) | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AppSiteStatusPayload](#appsitestatuspayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Site status updated successfully | [AppDetail](#appdetail) | +| 200 | Site status updated successfully | **application/json**: [AppDetail](#appdetail)
| | 403 | Insufficient permissions | | -### /apps/{app_id}/site/access-token-reset - -#### POST -##### Description - +### [POST] /apps/{app_id}/site/access-token-reset Reset access token for application site -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Access token reset successfully | [AppSiteResponse](#appsiteresponse) | +| 200 | Access token reset successfully | **application/json**: [AppSiteResponse](#appsiteresponse)
| | 403 | Insufficient permissions (admin/owner required) | | | 404 | App or site not found | | -### /apps/{app_id}/statistics/average-response-time - -#### GET -##### Description - +### [GET] /apps/{app_id}/statistics/average-response-time Get average response time statistics for an application -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [StatisticTimeRangeQuery](#statistictimerangequery) | | app_id | path | Application ID | Yes | string | +| end | query | End date (YYYY-MM-DD HH:MM) | No | | +| start | query | Start date (YYYY-MM-DD HH:MM) | No | | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Average response time statistics retrieved successfully | [ object ] | - -### /apps/{app_id}/statistics/average-session-interactions - -#### GET -##### Description +| 200 | Average response time statistics retrieved successfully | **application/json**: [ object ]
| +### [GET] /apps/{app_id}/statistics/average-session-interactions Get average session interaction statistics for an application -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [StatisticTimeRangeQuery](#statistictimerangequery) | | app_id | path | Application ID | Yes | string | +| end | query | End date (YYYY-MM-DD HH:MM) | No | | +| start | query | Start date (YYYY-MM-DD HH:MM) | No | | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Average session interaction statistics retrieved successfully | [ object ] | - -### /apps/{app_id}/statistics/daily-conversations - -#### GET -##### Description +| 200 | Average session interaction statistics retrieved successfully | **application/json**: [ object ]
| +### [GET] /apps/{app_id}/statistics/daily-conversations Get daily conversation statistics for an application -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [StatisticTimeRangeQuery](#statistictimerangequery) | | app_id | path | Application ID | Yes | string | +| end | query | End date (YYYY-MM-DD HH:MM) | No | | +| start | query | Start date (YYYY-MM-DD HH:MM) | No | | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Daily conversation statistics retrieved successfully | [ object ] | - -### /apps/{app_id}/statistics/daily-end-users - -#### GET -##### Description +| 200 | Daily conversation statistics retrieved successfully | **application/json**: [ object ]
| +### [GET] /apps/{app_id}/statistics/daily-end-users Get daily terminal/end-user statistics for an application -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [StatisticTimeRangeQuery](#statistictimerangequery) | | app_id | path | Application ID | Yes | string | +| end | query | End date (YYYY-MM-DD HH:MM) | No | | +| start | query | Start date (YYYY-MM-DD HH:MM) | No | | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Daily terminal statistics retrieved successfully | [ object ] | - -### /apps/{app_id}/statistics/daily-messages - -#### GET -##### Description +| 200 | Daily terminal statistics retrieved successfully | **application/json**: [ object ]
| +### [GET] /apps/{app_id}/statistics/daily-messages Get daily message statistics for an application -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [StatisticTimeRangeQuery](#statistictimerangequery) | | app_id | path | Application ID | Yes | string | +| end | query | End date (YYYY-MM-DD HH:MM) | No | | +| start | query | Start date (YYYY-MM-DD HH:MM) | No | | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Daily message statistics retrieved successfully | [ object ] | - -### /apps/{app_id}/statistics/token-costs - -#### GET -##### Description +| 200 | Daily message statistics retrieved successfully | **application/json**: [ object ]
| +### [GET] /apps/{app_id}/statistics/token-costs Get daily token cost statistics for an application -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [StatisticTimeRangeQuery](#statistictimerangequery) | | app_id | path | Application ID | Yes | string | +| end | query | End date (YYYY-MM-DD HH:MM) | No | | +| start | query | Start date (YYYY-MM-DD HH:MM) | No | | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Daily token cost statistics retrieved successfully | [ object ] | - -### /apps/{app_id}/statistics/tokens-per-second - -#### GET -##### Description +| 200 | Daily token cost statistics retrieved successfully | **application/json**: [ object ]
| +### [GET] /apps/{app_id}/statistics/tokens-per-second Get tokens per second statistics for an application -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [StatisticTimeRangeQuery](#statistictimerangequery) | | app_id | path | Application ID | Yes | string | +| end | query | End date (YYYY-MM-DD HH:MM) | No | | +| start | query | Start date (YYYY-MM-DD HH:MM) | No | | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Tokens per second statistics retrieved successfully | [ object ] | - -### /apps/{app_id}/statistics/user-satisfaction-rate - -#### GET -##### Description +| 200 | Tokens per second statistics retrieved successfully | **application/json**: [ object ]
| +### [GET] /apps/{app_id}/statistics/user-satisfaction-rate Get user satisfaction rate statistics for an application -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [StatisticTimeRangeQuery](#statistictimerangequery) | | app_id | path | Application ID | Yes | string | +| end | query | End date (YYYY-MM-DD HH:MM) | No | | +| start | query | Start date (YYYY-MM-DD HH:MM) | No | | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | User satisfaction rate statistics retrieved successfully | [ object ] | - -### /apps/{app_id}/text-to-audio - -#### POST -##### Description +| 200 | User satisfaction rate statistics retrieved successfully | **application/json**: [ object ]
| +### [POST] /apps/{app_id}/text-to-audio Convert text to speech for chat messages -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [TextToSpeechPayload](#texttospeechpayload) | | app_id | path | App ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TextToSpeechPayload](#texttospeechpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Text to speech conversion successful | | 400 | Bad request - Invalid parameters | -### /apps/{app_id}/text-to-audio/voices - -#### GET -##### Description - +### [GET] /apps/{app_id}/text-to-audio/voices Get available TTS voices for a specific language -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [TextToSpeechVoiceQuery](#texttospeechvoicequery) | | app_id | path | App ID | Yes | string | +| language | query | Language code | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | TTS voices retrieved successfully | [ object ] | +| 200 | TTS voices retrieved successfully | **application/json**: [ object ]
| | 400 | Invalid language parameter | | -### /apps/{app_id}/trace - -#### GET -##### Summary - -Get app trace - -##### Description +### [GET] /apps/{app_id}/trace +**Get app trace** Get app tracing configuration -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Trace configuration retrieved successfully | -#### POST -##### Description - +### [POST] /apps/{app_id}/trace Update app tracing configuration -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AppTracePayload](#apptracepayload) | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AppTracePayload](#apptracepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Trace configuration updated successfully | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Trace configuration updated successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| | 403 | Insufficient permissions | | -### /apps/{app_id}/trace-config - -#### DELETE -##### Summary - -Delete an existing trace app configuration - -##### Description +### [DELETE] /apps/{app_id}/trace-config +**Delete an existing trace app configuration** Delete an existing tracing configuration for an application -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [TraceProviderQuery](#traceproviderquery) | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TraceProviderQuery](#traceproviderquery)
| + +#### Responses | Code | Description | | ---- | ----------- | | 204 | Tracing configuration deleted successfully | | 400 | Invalid request parameters or configuration not found | -#### GET -##### Description - +### [GET] /apps/{app_id}/trace-config Get tracing configuration for an application -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [TraceProviderQuery](#traceproviderquery) | | app_id | path | Application ID | Yes | string | +| tracing_provider | query | Tracing provider name | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Tracing configuration retrieved successfully | object | +| 200 | Tracing configuration retrieved successfully | **application/json**: object
| | 400 | Invalid request parameters | | -#### PATCH -##### Summary - -Update an existing trace app configuration - -##### Description +### [PATCH] /apps/{app_id}/trace-config +**Update an existing trace app configuration** Update an existing tracing configuration for an application -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [TraceConfigPayload](#traceconfigpayload) | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TraceConfigPayload](#traceconfigpayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Tracing configuration updated successfully | object | +| 200 | Tracing configuration updated successfully | **application/json**: object
| | 400 | Invalid request parameters or configuration not found | | -#### POST -##### Summary - -Create a new trace app configuration - -##### Description +### [POST] /apps/{app_id}/trace-config +**Create a new trace app configuration** Create a new tracing configuration for an application -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [TraceConfigPayload](#traceconfigpayload) | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TraceConfigPayload](#traceconfigpayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | Tracing configuration created successfully | object | +| 201 | Tracing configuration created successfully | **application/json**: object
| | 400 | Invalid request parameters or configuration already exists | | -### /apps/{app_id}/trigger-enable +### [POST] /apps/{app_id}/trigger-enable +**Update app trigger (enable/disable)** -#### POST -##### Summary - -Update app trigger (enable/disable) - -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | -| payload | body | | Yes | [ParserEnable](#parserenable) | - -##### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Success | [WorkflowTriggerResponse](#workflowtriggerresponse) | - -### /apps/{app_id}/triggers - -#### GET -##### Summary - -Get app triggers list - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserEnable](#parserenable)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [WorkflowTriggerListResponse](#workflowtriggerlistresponse) | +| 200 | Success | **application/json**: [WorkflowTriggerResponse](#workflowtriggerresponse)
| -### /apps/{app_id}/workflow-app-logs +### [GET] /apps/{app_id}/triggers +**Get app triggers list** -#### GET -##### Summary +#### Parameters -Get workflow app logs +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| app_id | path | | Yes | string | -##### Description +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [WorkflowTriggerListResponse](#workflowtriggerlistresponse)
| + +### [GET] /apps/{app_id}/workflow-app-logs +**Get workflow app logs** Get workflow application execution logs -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkflowAppLogQuery](#workflowapplogquery) | | app_id | path | Application ID | Yes | string | +| created_at__after | query | Filter logs created after this timestamp | No | | +| created_at__before | query | Filter logs created before this timestamp | No | | +| created_by_account | query | Filter by account | No | | +| created_by_end_user_session_id | query | Filter by end user session ID | No | | +| detail | query | Whether to return detailed logs | No | boolean | +| keyword | query | Search keyword for filtering logs | No | | +| limit | query | Number of items per page (1-100) | No | integer,
**Default:** 20 | +| page | query | Page number (1-99999) | No | integer,
**Default:** 1 | +| status | query | Execution status filter (succeeded, failed, stopped, partial-succeeded) | No | | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow app logs retrieved successfully | [WorkflowAppLogPaginationResponse](#workflowapplogpaginationresponse) | +| 200 | Workflow app logs retrieved successfully | **application/json**: [WorkflowAppLogPaginationResponse](#workflowapplogpaginationresponse)
| -### /apps/{app_id}/workflow-archived-logs - -#### GET -##### Summary - -Get workflow archived logs - -##### Description +### [GET] /apps/{app_id}/workflow-archived-logs +**Get workflow archived logs** Get workflow archived execution logs -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkflowAppLogQuery](#workflowapplogquery) | | app_id | path | Application ID | Yes | string | +| created_at__after | query | Filter logs created after this timestamp | No | | +| created_at__before | query | Filter logs created before this timestamp | No | | +| created_by_account | query | Filter by account | No | | +| created_by_end_user_session_id | query | Filter by end user session ID | No | | +| detail | query | Whether to return detailed logs | No | boolean | +| keyword | query | Search keyword for filtering logs | No | | +| limit | query | Number of items per page (1-100) | No | integer,
**Default:** 20 | +| page | query | Page number (1-99999) | No | integer,
**Default:** 1 | +| status | query | Execution status filter (succeeded, failed, stopped, partial-succeeded) | No | | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow archived logs retrieved successfully | [WorkflowArchivedLogPaginationResponse](#workflowarchivedlogpaginationresponse) | +| 200 | Workflow archived logs retrieved successfully | **application/json**: [WorkflowArchivedLogPaginationResponse](#workflowarchivedlogpaginationresponse)
| -### /apps/{app_id}/workflow-runs +### [GET] /apps/{app_id}/workflow-runs +**Get workflow run list** -#### GET -##### Summary - -Get workflow run list - -##### Description - -Get workflow run list - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | last_id | query | Last run ID for pagination | No | string | -| limit | query | Number of items per page (1-100) | No | integer | -| status | query | Workflow run status filter | No | string | -| triggered_from | query | Filter by trigger source: debugging or app-run. Default: debugging | No | string | +| limit | query | Number of items per page (1-100) | No | integer,
**Default:** 20 | +| status | query | Workflow run status filter | No | string,
**Available values:** "failed", "partial-succeeded", "running", "stopped", "succeeded" | +| triggered_from | query | Filter by trigger source: debugging or app-run. Default: debugging | No | string,
**Available values:** "app-run", "debugging" | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow runs retrieved successfully | [WorkflowRunPaginationResponse](#workflowrunpaginationresponse) | +| 200 | Workflow runs retrieved successfully | **application/json**: [WorkflowRunPaginationResponse](#workflowrunpaginationresponse)
| -### /apps/{app_id}/workflow-runs/count +### [GET] /apps/{app_id}/workflow-runs/count +**Get workflow runs count statistics** -#### GET -##### Summary - -Get workflow runs count statistics - -##### Description - -Get workflow runs count statistics - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -| status | query | Workflow run status filter | No | string | +| status | query | Workflow run status filter | No | string,
**Available values:** "failed", "partial-succeeded", "running", "stopped", "succeeded" | | time_range | query | Filter by time range (optional): e.g., 7d (7 days), 4h (4 hours), 30m (30 minutes), 30s (30 seconds). Filters by created_at field. | No | string | -| triggered_from | query | Filter by trigger source: debugging or app-run. Default: debugging | No | string | +| triggered_from | query | Filter by trigger source: debugging or app-run. Default: debugging | No | string,
**Available values:** "app-run", "debugging" | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow runs count retrieved successfully | [WorkflowRunCountResponse](#workflowruncountresponse) | +| 200 | Workflow runs count retrieved successfully | **application/json**: [WorkflowRunCountResponse](#workflowruncountresponse)
| -### /apps/{app_id}/workflow-runs/tasks/{task_id}/stop - -#### POST -##### Summary - -Stop workflow task - -##### Description +### [POST] /apps/{app_id}/workflow-runs/tasks/{task_id}/stop +**Stop workflow task** Stop running workflow task -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | task_id | path | Task ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Task stopped successfully | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Task stopped successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| | 403 | Permission denied | | | 404 | Task not found | | -### /apps/{app_id}/workflow-runs/{run_id} +### [GET] /apps/{app_id}/workflow-runs/{run_id} +**Get workflow run detail** -#### GET -##### Summary - -Get workflow run detail - -##### Description - -Get workflow run detail - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | run_id | path | Workflow run ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow run detail retrieved successfully | [WorkflowRunDetailResponse](#workflowrundetailresponse) | +| 200 | Workflow run detail retrieved successfully | **application/json**: [WorkflowRunDetailResponse](#workflowrundetailresponse)
| | 404 | Workflow run not found | | -### /apps/{app_id}/workflow-runs/{run_id}/export - -#### GET -##### Description - +### [GET] /apps/{app_id}/workflow-runs/{run_id}/export Generate a download URL for an archived workflow run. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | run_id | path | Workflow run ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Export URL generated | [WorkflowRunExportResponse](#workflowrunexportresponse) | +| 200 | Export URL generated | **application/json**: [WorkflowRunExportResponse](#workflowrunexportresponse)
| -### /apps/{app_id}/workflow-runs/{run_id}/node-executions +### [GET] /apps/{app_id}/workflow-runs/{run_id}/node-executions +**Get workflow run node execution list** -#### GET -##### Summary - -Get workflow run node execution list - -##### Description - -Get workflow run node execution list - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | run_id | path | Workflow run ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Node executions retrieved successfully | [WorkflowRunNodeExecutionListResponse](#workflowrunnodeexecutionlistresponse) | +| 200 | Node executions retrieved successfully | **application/json**: [WorkflowRunNodeExecutionListResponse](#workflowrunnodeexecutionlistresponse)
| | 404 | Workflow run not found | | -### /apps/{app_id}/workflow-runs/{workflow_run_id}/agent-nodes/{node_id}/sandbox/files - -#### GET -##### Description - +### [GET] /apps/{app_id}/workflow-runs/{workflow_run_id}/agent-nodes/{node_id}/sandbox/files List a directory in a workflow Agent node sandbox -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -2750,22 +2469,18 @@ List a directory in a workflow Agent node sandbox | node_id | path | Workflow Agent node ID | Yes | string | | workflow_run_id | path | Workflow run ID | Yes | string | | node_execution_id | query | Optional workflow node execution ID. When omitted, the latest active session for the node is used. | No | string | -| path | query | Directory path relative to the sandbox workspace | No | string | +| path | query | Directory path relative to the sandbox workspace | No | string,
**Default:** . | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Listing returned | [SandboxListResponse](#sandboxlistresponse) | - -### /apps/{app_id}/workflow-runs/{workflow_run_id}/agent-nodes/{node_id}/sandbox/files/read - -#### GET -##### Description +| 200 | Listing returned | **application/json**: [SandboxListResponse](#sandboxlistresponse)
| +### [GET] /apps/{app_id}/workflow-runs/{workflow_run_id}/agent-nodes/{node_id}/sandbox/files/read Read a text/binary preview file in a workflow Agent node sandbox -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -2775,208 +2490,166 @@ Read a text/binary preview file in a workflow Agent node sandbox | node_execution_id | query | Optional workflow node execution ID. When omitted, the latest active session for the node is used. | No | string | | path | query | File path relative to the sandbox workspace | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Preview returned | [SandboxReadResponse](#sandboxreadresponse) | - -### /apps/{app_id}/workflow-runs/{workflow_run_id}/agent-nodes/{node_id}/sandbox/files/upload - -#### POST -##### Description +| 200 | Preview returned | **application/json**: [SandboxReadResponse](#sandboxreadresponse)
| +### [POST] /apps/{app_id}/workflow-runs/{workflow_run_id}/agent-nodes/{node_id}/sandbox/files/upload Upload one workflow Agent sandbox file as a Dify ToolFile mapping -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | | node_id | path | | Yes | string | | workflow_run_id | path | | Yes | string | -| payload | body | | Yes | [WorkflowAgentSandboxUploadPayload](#workflowagentsandboxuploadpayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [WorkflowAgentSandboxUploadPayload](#workflowagentsandboxuploadpayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Uploaded | [SandboxUploadResponse](#sandboxuploadresponse) | +| 200 | Uploaded | **application/json**: [SandboxUploadResponse](#sandboxuploadresponse)
| -### /apps/{app_id}/workflow/comments +### [GET] /apps/{app_id}/workflow/comments +**Get all comments for a workflow** -#### GET -##### Summary - -Get all comments for a workflow - -##### Description - -Get all comments for a workflow - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Comments retrieved successfully | [WorkflowCommentBasicList](#workflowcommentbasiclist) | +| 200 | Comments retrieved successfully | **application/json**: [WorkflowCommentBasicList](#workflowcommentbasiclist)
| -#### POST -##### Summary +### [POST] /apps/{app_id}/workflow/comments +**Create a new workflow comment** -Create a new workflow comment - -##### Description - -Create a new workflow comment - -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkflowCommentCreatePayload](#workflowcommentcreatepayload) | -| app_id | path | Application ID | Yes | string | - -##### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 201 | Comment created successfully | [WorkflowCommentCreate](#workflowcommentcreate) | - -### /apps/{app_id}/workflow/comments/mention-users - -#### GET -##### Summary - -Get all users in current tenant for mentions - -##### Description - -Get all users in current tenant for mentions - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [WorkflowCommentCreatePayload](#workflowcommentcreatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Mentionable users retrieved successfully | [WorkflowCommentMentionUsersPayload](#workflowcommentmentionuserspayload) | +| 201 | Comment created successfully | **application/json**: [WorkflowCommentCreate](#workflowcommentcreate)
| -### /apps/{app_id}/workflow/comments/{comment_id} +### [GET] /apps/{app_id}/workflow/comments/mention-users +**Get all users in current tenant for mentions** -#### DELETE -##### Summary +#### Parameters -Delete a workflow comment +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| app_id | path | Application ID | Yes | string | -##### Description +#### Responses -Delete a workflow comment +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Mentionable users retrieved successfully | **application/json**: [WorkflowCommentMentionUsersPayload](#workflowcommentmentionuserspayload)
| -##### Parameters +### [DELETE] /apps/{app_id}/workflow/comments/{comment_id} +**Delete a workflow comment** + +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | comment_id | path | Comment ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Comment deleted successfully | -#### GET -##### Summary +### [GET] /apps/{app_id}/workflow/comments/{comment_id} +**Get a specific workflow comment** -Get a specific workflow comment - -##### Description - -Get a specific workflow comment - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | comment_id | path | Comment ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Comment retrieved successfully | [WorkflowCommentDetail](#workflowcommentdetail) | +| 200 | Comment retrieved successfully | **application/json**: [WorkflowCommentDetail](#workflowcommentdetail)
| -#### PUT -##### Summary +### [PUT] /apps/{app_id}/workflow/comments/{comment_id} +**Update a workflow comment** -Update a workflow comment - -##### Description - -Update a workflow comment - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkflowCommentUpdatePayload](#workflowcommentupdatepayload) | | app_id | path | Application ID | Yes | string | | comment_id | path | Comment ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [WorkflowCommentUpdatePayload](#workflowcommentupdatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Comment updated successfully | [WorkflowCommentUpdate](#workflowcommentupdate) | +| 200 | Comment updated successfully | **application/json**: [WorkflowCommentUpdate](#workflowcommentupdate)
| -### /apps/{app_id}/workflow/comments/{comment_id}/replies +### [POST] /apps/{app_id}/workflow/comments/{comment_id}/replies +**Add a reply to a workflow comment** -#### POST -##### Summary - -Add a reply to a workflow comment - -##### Description - -Add a reply to a workflow comment - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkflowCommentReplyPayload](#workflowcommentreplypayload) | | app_id | path | Application ID | Yes | string | | comment_id | path | Comment ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [WorkflowCommentReplyPayload](#workflowcommentreplypayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | Reply created successfully | [WorkflowCommentReplyCreate](#workflowcommentreplycreate) | +| 201 | Reply created successfully | **application/json**: [WorkflowCommentReplyCreate](#workflowcommentreplycreate)
| -### /apps/{app_id}/workflow/comments/{comment_id}/replies/{reply_id} +### [DELETE] /apps/{app_id}/workflow/comments/{comment_id}/replies/{reply_id} +**Delete a comment reply** -#### DELETE -##### Summary - -Delete a comment reply - -##### Description - -Delete a comment reply - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -2984,456 +2657,406 @@ Delete a comment reply | comment_id | path | Comment ID | Yes | string | | reply_id | path | Reply ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Reply deleted successfully | -#### PUT -##### Summary +### [PUT] /apps/{app_id}/workflow/comments/{comment_id}/replies/{reply_id} +**Update a comment reply** -Update a comment reply - -##### Description - -Update a comment reply - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkflowCommentReplyPayload](#workflowcommentreplypayload) | | app_id | path | Application ID | Yes | string | | comment_id | path | Comment ID | Yes | string | | reply_id | path | Reply ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [WorkflowCommentReplyPayload](#workflowcommentreplypayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Reply updated successfully | [WorkflowCommentReplyUpdate](#workflowcommentreplyupdate) | +| 200 | Reply updated successfully | **application/json**: [WorkflowCommentReplyUpdate](#workflowcommentreplyupdate)
| -### /apps/{app_id}/workflow/comments/{comment_id}/resolve +### [POST] /apps/{app_id}/workflow/comments/{comment_id}/resolve +**Resolve a workflow comment** -#### POST -##### Summary - -Resolve a workflow comment - -##### Description - -Resolve a workflow comment - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | comment_id | path | Comment ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Comment resolved successfully | [WorkflowCommentResolve](#workflowcommentresolve) | - -### /apps/{app_id}/workflow/statistics/average-app-interactions - -#### GET -##### Description +| 200 | Comment resolved successfully | **application/json**: [WorkflowCommentResolve](#workflowcommentresolve)
| +### [GET] /apps/{app_id}/workflow/statistics/average-app-interactions Get workflow average app interaction statistics -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkflowStatisticQuery](#workflowstatisticquery) | | app_id | path | Application ID | Yes | string | +| end | query | End date and time (YYYY-MM-DD HH:MM) | No | | +| start | query | Start date and time (YYYY-MM-DD HH:MM) | No | | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Average app interaction statistics retrieved successfully | -### /apps/{app_id}/workflow/statistics/daily-conversations - -#### GET -##### Description - +### [GET] /apps/{app_id}/workflow/statistics/daily-conversations Get workflow daily runs statistics -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkflowStatisticQuery](#workflowstatisticquery) | | app_id | path | Application ID | Yes | string | +| end | query | End date and time (YYYY-MM-DD HH:MM) | No | | +| start | query | Start date and time (YYYY-MM-DD HH:MM) | No | | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Daily runs statistics retrieved successfully | -### /apps/{app_id}/workflow/statistics/daily-terminals - -#### GET -##### Description - +### [GET] /apps/{app_id}/workflow/statistics/daily-terminals Get workflow daily terminals statistics -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkflowStatisticQuery](#workflowstatisticquery) | | app_id | path | Application ID | Yes | string | +| end | query | End date and time (YYYY-MM-DD HH:MM) | No | | +| start | query | Start date and time (YYYY-MM-DD HH:MM) | No | | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Daily terminals statistics retrieved successfully | -### /apps/{app_id}/workflow/statistics/token-costs - -#### GET -##### Description - +### [GET] /apps/{app_id}/workflow/statistics/token-costs Get workflow daily token cost statistics -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkflowStatisticQuery](#workflowstatisticquery) | | app_id | path | Application ID | Yes | string | +| end | query | End date and time (YYYY-MM-DD HH:MM) | No | | +| start | query | Start date and time (YYYY-MM-DD HH:MM) | No | | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Daily token cost statistics retrieved successfully | -### /apps/{app_id}/workflows - -#### GET -##### Summary - -Get published workflows - -##### Description +### [GET] /apps/{app_id}/workflows +**Get published workflows** Get all published workflows for an application -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkflowListQuery](#workflowlistquery) | | app_id | path | Application ID | Yes | string | +| limit | query | | No | integer,
**Default:** 10 | +| named_only | query | | No | boolean | +| page | query | | No | integer,
**Default:** 1 | +| user_id | query | | No | | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Published workflows retrieved successfully | [WorkflowPaginationResponse](#workflowpaginationresponse) | +| 200 | Published workflows retrieved successfully | **application/json**: [WorkflowPaginationResponse](#workflowpaginationresponse)
| -### /apps/{app_id}/workflows/default-workflow-block-configs - -#### GET -##### Summary - -Get default block config - -##### Description +### [GET] /apps/{app_id}/workflows/default-workflow-block-configs +**Get default block config** Get default block configurations for workflow -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Default block configurations retrieved successfully | -### /apps/{app_id}/workflows/default-workflow-block-configs/{block_type} - -#### GET -##### Summary - -Get default block config - -##### Description +### [GET] /apps/{app_id}/workflows/default-workflow-block-configs/{block_type} +**Get default block config** Get default block configuration by type -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [DefaultBlockConfigQuery](#defaultblockconfigquery) | | app_id | path | Application ID | Yes | string | | block_type | path | Block type | Yes | string | +| q | query | | No | | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Default block configuration retrieved successfully | | 404 | Block type not found | -### /apps/{app_id}/workflows/draft - -#### GET -##### Summary - -Get draft workflow - -##### Description +### [GET] /apps/{app_id}/workflows/draft +**Get draft workflow** Get draft workflow for an application -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Draft workflow retrieved successfully | [WorkflowResponse](#workflowresponse) | +| 200 | Draft workflow retrieved successfully | **application/json**: [WorkflowResponse](#workflowresponse)
| | 404 | Draft workflow not found | | -#### POST -##### Summary - -Sync draft workflow - -##### Description +### [POST] /apps/{app_id}/workflows/draft +**Sync draft workflow** Sync draft workflow configuration -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [SyncDraftWorkflowPayload](#syncdraftworkflowpayload) | | app_id | path | | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [SyncDraftWorkflowPayload](#syncdraftworkflowpayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Draft workflow synced successfully | [SyncDraftWorkflowResponse](#syncdraftworkflowresponse) | +| 200 | Draft workflow synced successfully | **application/json**: [SyncDraftWorkflowResponse](#syncdraftworkflowresponse)
| | 400 | Invalid workflow configuration | | | 403 | Permission denied | | -### /apps/{app_id}/workflows/draft/conversation-variables - -#### GET -##### Description - +### [GET] /apps/{app_id}/workflows/draft/conversation-variables Get conversation variables for workflow -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Conversation variables retrieved successfully | [WorkflowDraftVariableList](#workflowdraftvariablelist) | +| 200 | Conversation variables retrieved successfully | **application/json**: [WorkflowDraftVariableList](#workflowdraftvariablelist)
| | 404 | Draft workflow not found | | -#### POST -##### Description - +### [POST] /apps/{app_id}/workflows/draft/conversation-variables Update conversation variables for workflow draft -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ConversationVariableUpdatePayload](#conversationvariableupdatepayload) | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ConversationVariableUpdatePayload](#conversationvariableupdatepayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Conversation variables updated successfully | -### /apps/{app_id}/workflows/draft/environment-variables - -#### GET -##### Summary - -Get draft workflow - -##### Description +### [GET] /apps/{app_id}/workflows/draft/environment-variables +**Get draft workflow** Get environment variables for workflow -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Environment variables retrieved successfully | | 404 | Draft workflow not found | -#### POST -##### Description - +### [POST] /apps/{app_id}/workflows/draft/environment-variables Update environment variables for workflow draft -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [EnvironmentVariableUpdatePayload](#environmentvariableupdatepayload) | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [EnvironmentVariableUpdatePayload](#environmentvariableupdatepayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Environment variables updated successfully | -### /apps/{app_id}/workflows/draft/features - -#### POST -##### Description - +### [POST] /apps/{app_id}/workflows/draft/features Update draft workflow features -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkflowFeaturesPayload](#workflowfeaturespayload) | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [WorkflowFeaturesPayload](#workflowfeaturespayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow features updated successfully | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Workflow features updated successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -### /apps/{app_id}/workflows/draft/human-input/nodes/{node_id}/delivery-test - -#### POST -##### Summary - -Test human input delivery - -##### Description +### [POST] /apps/{app_id}/workflows/draft/human-input/nodes/{node_id}/delivery-test +**Test human input delivery** Test human input delivery for workflow -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [HumanInputDeliveryTestPayload](#humaninputdeliverytestpayload) | | app_id | path | Application ID | Yes | string | | node_id | path | Node ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [HumanInputDeliveryTestPayload](#humaninputdeliverytestpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /apps/{app_id}/workflows/draft/human-input/nodes/{node_id}/form/preview - -#### POST -##### Summary - -Preview human input form content and placeholders - -##### Description +### [POST] /apps/{app_id}/workflows/draft/human-input/nodes/{node_id}/form/preview +**Preview human input form content and placeholders** Get human input form preview for workflow -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [HumanInputFormPreviewPayload](#humaninputformpreviewpayload) | | app_id | path | Application ID | Yes | string | | node_id | path | Node ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [HumanInputFormPreviewPayload](#humaninputformpreviewpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /apps/{app_id}/workflows/draft/human-input/nodes/{node_id}/form/run - -#### POST -##### Summary - -Submit human input form preview - -##### Description +### [POST] /apps/{app_id}/workflows/draft/human-input/nodes/{node_id}/form/run +**Submit human input form preview** Submit human input form preview for workflow -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [HumanInputFormSubmitPayload](#humaninputformsubmitpayload) | | app_id | path | Application ID | Yes | string | | node_id | path | Node ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [HumanInputFormSubmitPayload](#humaninputformsubmitpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /apps/{app_id}/workflows/draft/iteration/nodes/{node_id}/run +### [POST] /apps/{app_id}/workflows/draft/iteration/nodes/{node_id}/run +**Run draft workflow iteration node** -#### POST -##### Summary - -Run draft workflow iteration node - -##### Description - -Run draft workflow iteration node - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [IterationNodeRunPayload](#iterationnoderunpayload) | | app_id | path | Application ID | Yes | string | | node_id | path | Node ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [IterationNodeRunPayload](#iterationnoderunpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | @@ -3441,26 +3064,23 @@ Run draft workflow iteration node | 403 | Permission denied | | 404 | Node not found | -### /apps/{app_id}/workflows/draft/loop/nodes/{node_id}/run +### [POST] /apps/{app_id}/workflows/draft/loop/nodes/{node_id}/run +**Run draft workflow loop node** -#### POST -##### Summary - -Run draft workflow loop node - -##### Description - -Run draft workflow loop node - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [LoopNodeRunPayload](#loopnoderunpayload) | | app_id | path | Application ID | Yes | string | | node_id | path | Node ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [LoopNodeRunPayload](#loopnoderunpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | @@ -3468,172 +3088,167 @@ Run draft workflow loop node | 403 | Permission denied | | 404 | Node not found | -### /apps/{app_id}/workflows/draft/nodes/{node_id}/agent-composer - -#### GET -##### Parameters +### [GET] /apps/{app_id}/workflows/draft/nodes/{node_id}/agent-composer +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | | node_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow agent composer state | [WorkflowAgentComposerResponse](#workflowagentcomposerresponse) | +| 200 | Workflow agent composer state | **application/json**: [WorkflowAgentComposerResponse](#workflowagentcomposerresponse)
| -#### PUT -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | -| node_id | path | | Yes | string | -| payload | body | | Yes | [ComposerSavePayload](#composersavepayload) | - -##### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Workflow agent composer saved | [WorkflowAgentComposerResponse](#workflowagentcomposerresponse) | - -### /apps/{app_id}/workflows/draft/nodes/{node_id}/agent-composer/candidates - -#### GET -##### Parameters +### [PUT] /apps/{app_id}/workflows/draft/nodes/{node_id}/agent-composer +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | | node_id | path | | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ComposerSavePayload](#composersavepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow agent composer candidates | [AgentComposerCandidatesResponse](#agentcomposercandidatesresponse) | +| 200 | Workflow agent composer saved | **application/json**: [WorkflowAgentComposerResponse](#workflowagentcomposerresponse)
| -### /apps/{app_id}/workflows/draft/nodes/{node_id}/agent-composer/impact - -#### POST -##### Parameters +### [GET] /apps/{app_id}/workflows/draft/nodes/{node_id}/agent-composer/candidates +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | | node_id | path | | Yes | string | -| payload | body | | Yes | [ComposerSavePayload](#composersavepayload) | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow agent composer impact | [AgentComposerImpactResponse](#agentcomposerimpactresponse) | +| 200 | Workflow agent composer candidates | **application/json**: [AgentComposerCandidatesResponse](#agentcomposercandidatesresponse)
| -### /apps/{app_id}/workflows/draft/nodes/{node_id}/agent-composer/save-to-roster - -#### POST -##### Parameters +### [POST] /apps/{app_id}/workflows/draft/nodes/{node_id}/agent-composer/impact +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | | node_id | path | | Yes | string | -| payload | body | | Yes | [ComposerSavePayload](#composersavepayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ComposerSavePayload](#composersavepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow agent composer saved to roster | [WorkflowAgentComposerResponse](#workflowagentcomposerresponse) | +| 200 | Workflow agent composer impact | **application/json**: [AgentComposerImpactResponse](#agentcomposerimpactresponse)
| -### /apps/{app_id}/workflows/draft/nodes/{node_id}/agent-composer/validate - -#### POST -##### Parameters +### [POST] /apps/{app_id}/workflows/draft/nodes/{node_id}/agent-composer/save-to-roster +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | | node_id | path | | Yes | string | -| payload | body | | Yes | [ComposerSavePayload](#composersavepayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ComposerSavePayload](#composersavepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow agent composer validation result | [AgentComposerValidateResponse](#agentcomposervalidateresponse) | +| 200 | Workflow agent composer saved to roster | **application/json**: [WorkflowAgentComposerResponse](#workflowagentcomposerresponse)
| -### /apps/{app_id}/workflows/draft/nodes/{node_id}/last-run +### [POST] /apps/{app_id}/workflows/draft/nodes/{node_id}/agent-composer/validate +#### Parameters -#### GET -##### Description +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| app_id | path | | Yes | string | +| node_id | path | | Yes | string | +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ComposerSavePayload](#composersavepayload)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Workflow agent composer validation result | **application/json**: [AgentComposerValidateResponse](#agentcomposervalidateresponse)
| + +### [GET] /apps/{app_id}/workflows/draft/nodes/{node_id}/last-run Get last run result for draft workflow node -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | node_id | path | Node ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Node last run retrieved successfully | [WorkflowRunNodeExecutionResponse](#workflowrunnodeexecutionresponse) | +| 200 | Node last run retrieved successfully | **application/json**: [WorkflowRunNodeExecutionResponse](#workflowrunnodeexecutionresponse)
| | 403 | Permission denied | | | 404 | Node last run not found | | -### /apps/{app_id}/workflows/draft/nodes/{node_id}/run +### [POST] /apps/{app_id}/workflows/draft/nodes/{node_id}/run +**Run draft workflow node** -#### POST -##### Summary - -Run draft workflow node - -##### Description - -Run draft workflow node - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [DraftWorkflowNodeRunPayload](#draftworkflownoderunpayload) | | app_id | path | Application ID | Yes | string | | node_id | path | Node ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DraftWorkflowNodeRunPayload](#draftworkflownoderunpayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Node run started successfully | [WorkflowRunNodeExecutionResponse](#workflowrunnodeexecutionresponse) | +| 200 | Node run started successfully | **application/json**: [WorkflowRunNodeExecutionResponse](#workflowrunnodeexecutionresponse)
| | 403 | Permission denied | | | 404 | Node not found | | -### /apps/{app_id}/workflows/draft/nodes/{node_id}/trigger/run +### [POST] /apps/{app_id}/workflows/draft/nodes/{node_id}/trigger/run +**Poll for trigger events and execute single node when event arrives** -#### POST -##### Summary - -Poll for trigger events and execute single node when event arrives - -##### Description - -Poll for trigger events and execute single node when event arrives - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | node_id | path | Node ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -3641,119 +3256,98 @@ Poll for trigger events and execute single node when event arrives | 403 | Permission denied | | 500 | Internal server error | -### /apps/{app_id}/workflows/draft/nodes/{node_id}/variables - -#### DELETE -##### Description - +### [DELETE] /apps/{app_id}/workflows/draft/nodes/{node_id}/variables Delete all variables for a specific node -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | | node_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Node variables deleted successfully | -#### GET -##### Description - +### [GET] /apps/{app_id}/workflows/draft/nodes/{node_id}/variables Get variables for a specific node -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | node_id | path | Node ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Node variables retrieved successfully | [WorkflowDraftVariableList](#workflowdraftvariablelist) | +| 200 | Node variables retrieved successfully | **application/json**: [WorkflowDraftVariableList](#workflowdraftvariablelist)
| -### /apps/{app_id}/workflows/draft/run +### [POST] /apps/{app_id}/workflows/draft/run +**Run draft workflow** -#### POST -##### Summary - -Run draft workflow - -##### Description - -Run draft workflow - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [DraftWorkflowRunPayload](#draftworkflowrunpayload) | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DraftWorkflowRunPayload](#draftworkflowrunpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Draft workflow run started successfully | | 403 | Permission denied | -### /apps/{app_id}/workflows/draft/runs/{run_id}/node-outputs - -#### GET -##### Description - +### [GET] /apps/{app_id}/workflows/draft/runs/{run_id}/node-outputs Snapshot of every node's declared outputs for a draft workflow run. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | run_id | path | Workflow run ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow run node outputs | [WorkflowRunSnapshotView](#workflowrunsnapshotview) | +| 200 | Workflow run node outputs | **application/json**: [WorkflowRunSnapshotView](#workflowrunsnapshotview)
| | 404 | Workflow run not found | | -### /apps/{app_id}/workflows/draft/runs/{run_id}/node-outputs/events - -#### GET -##### Description - +### [GET] /apps/{app_id}/workflows/draft/runs/{run_id}/node-outputs/events Server-Sent Events stream of inspector deltas for a draft workflow run. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | run_id | path | Workflow run ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Workflow run node output event stream | | 404 | Workflow run not found | -### /apps/{app_id}/workflows/draft/runs/{run_id}/node-outputs/{node_id} - -#### GET -##### Description - +### [GET] /apps/{app_id}/workflows/draft/runs/{run_id}/node-outputs/{node_id} One node's declared outputs for a draft workflow run. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -3761,21 +3355,17 @@ One node's declared outputs for a draft workflow run. | node_id | path | Node ID inside the workflow graph | Yes | string | | run_id | path | Workflow run ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow run node output detail | [NodeOutputsView](#nodeoutputsview) | +| 200 | Workflow run node output detail | **application/json**: [NodeOutputsView](#nodeoutputsview)
| | 404 | Workflow run / node not found | | -### /apps/{app_id}/workflows/draft/runs/{run_id}/node-outputs/{node_id}/{output_name}/preview - -#### GET -##### Description - +### [GET] /apps/{app_id}/workflows/draft/runs/{run_id}/node-outputs/{node_id}/{output_name}/preview Full value for one declared output, including signed download URL for files. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -3784,51 +3374,44 @@ Full value for one declared output, including signed download URL for files. | output_name | path | Declared output name as exposed by Composer | Yes | string | | run_id | path | Workflow run ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow run node output preview | [OutputPreviewView](#outputpreviewview) | +| 200 | Workflow run node output preview | **application/json**: [OutputPreviewView](#outputpreviewview)
| | 404 | Workflow run / node / output not found | | -### /apps/{app_id}/workflows/draft/system-variables - -#### GET -##### Description - +### [GET] /apps/{app_id}/workflows/draft/system-variables Get system variables for workflow -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | System variables retrieved successfully | [WorkflowDraftVariableList](#workflowdraftvariablelist) | +| 200 | System variables retrieved successfully | **application/json**: [WorkflowDraftVariableList](#workflowdraftvariablelist)
| -### /apps/{app_id}/workflows/draft/trigger/run +### [POST] /apps/{app_id}/workflows/draft/trigger/run +**Poll for trigger events and execute full workflow when event arrives** -#### POST -##### Summary - -Poll for trigger events and execute full workflow when event arrives - -##### Description - -Poll for trigger events and execute full workflow when event arrives - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [DraftWorkflowTriggerRunRequest](#draftworkflowtriggerrunrequest) | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DraftWorkflowTriggerRunRequest](#draftworkflowtriggerrunrequest)
| + +#### Responses | Code | Description | | ---- | ----------- | @@ -3836,25 +3419,22 @@ Poll for trigger events and execute full workflow when event arrives | 403 | Permission denied | | 500 | Internal server error | -### /apps/{app_id}/workflows/draft/trigger/run-all +### [POST] /apps/{app_id}/workflows/draft/trigger/run-all +**Full workflow debug when the start node is a trigger** -#### POST -##### Summary - -Full workflow debug when the start node is a trigger - -##### Description - -Full workflow debug when the start node is a trigger - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [DraftWorkflowTriggerRunAllPayload](#draftworkflowtriggerrunallpayload) | | app_id | path | Application ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DraftWorkflowTriggerRunAllPayload](#draftworkflowtriggerrunallpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | @@ -3862,222 +3442,191 @@ Full workflow debug when the start node is a trigger | 403 | Permission denied | | 500 | Internal server error | -### /apps/{app_id}/workflows/draft/variables - -#### DELETE -##### Description - +### [DELETE] /apps/{app_id}/workflows/draft/variables Delete all draft workflow variables -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Workflow variables deleted successfully | -#### GET -##### Summary - -Get draft workflow - -##### Description +### [GET] /apps/{app_id}/workflows/draft/variables +**Get draft workflow** Get draft workflow variables -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkflowDraftVariableListQuery](#workflowdraftvariablelistquery) | | app_id | path | Application ID | Yes | string | | limit | query | Number of items per page (1-100) | No | string | | page | query | Page number (1-100000) | No | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow variables retrieved successfully | [WorkflowDraftVariableListWithoutValue](#workflowdraftvariablelistwithoutvalue) | - -### /apps/{app_id}/workflows/draft/variables/{variable_id} - -#### DELETE -##### Description +| 200 | Workflow variables retrieved successfully | **application/json**: [WorkflowDraftVariableListWithoutValue](#workflowdraftvariablelistwithoutvalue)
| +### [DELETE] /apps/{app_id}/workflows/draft/variables/{variable_id} Delete a workflow variable -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | | variable_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Variable deleted successfully | | 404 | Variable not found | -#### GET -##### Description - +### [GET] /apps/{app_id}/workflows/draft/variables/{variable_id} Get a specific workflow variable -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | variable_id | path | Variable ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Variable retrieved successfully | [WorkflowDraftVariable](#workflowdraftvariable) | +| 200 | Variable retrieved successfully | **application/json**: [WorkflowDraftVariable](#workflowdraftvariable)
| | 404 | Variable not found | | -#### PATCH -##### Description - +### [PATCH] /apps/{app_id}/workflows/draft/variables/{variable_id} Update a workflow variable -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkflowDraftVariableUpdatePayload](#workflowdraftvariableupdatepayload) | | app_id | path | | Yes | string | | variable_id | path | | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [WorkflowDraftVariableUpdatePayload](#workflowdraftvariableupdatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Variable updated successfully | [WorkflowDraftVariable](#workflowdraftvariable) | +| 200 | Variable updated successfully | **application/json**: [WorkflowDraftVariable](#workflowdraftvariable)
| | 404 | Variable not found | | -### /apps/{app_id}/workflows/draft/variables/{variable_id}/reset - -#### PUT -##### Description - +### [PUT] /apps/{app_id}/workflows/draft/variables/{variable_id}/reset Reset a workflow variable to its default value -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | variable_id | path | Variable ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Variable reset successfully | [WorkflowDraftVariable](#workflowdraftvariable) | +| 200 | Variable reset successfully | **application/json**: [WorkflowDraftVariable](#workflowdraftvariable)
| | 204 | Variable reset (no content) | | | 404 | Variable not found | | -### /apps/{app_id}/workflows/publish - -#### GET -##### Summary - -Get published workflow - -##### Description +### [GET] /apps/{app_id}/workflows/publish +**Get published workflow** Get published workflow for an application -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Published workflow retrieved successfully, or null if not found | [WorkflowResponse](#workflowresponse) | +| 200 | Published workflow retrieved successfully, or null if not found | **application/json**: [WorkflowResponse](#workflowresponse)
| -#### POST -##### Summary +### [POST] /apps/{app_id}/workflows/publish +**Publish workflow** -Publish workflow - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [PublishWorkflowPayload](#publishworkflowpayload) | | app_id | path | | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [PublishWorkflowPayload](#publishworkflowpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /apps/{app_id}/workflows/published/runs/{run_id}/node-outputs - -#### GET -##### Description - +### [GET] /apps/{app_id}/workflows/published/runs/{run_id}/node-outputs Snapshot of every node's declared outputs for a published workflow run. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | run_id | path | Workflow run ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow run node outputs | [WorkflowRunSnapshotView](#workflowrunsnapshotview) | +| 200 | Workflow run node outputs | **application/json**: [WorkflowRunSnapshotView](#workflowrunsnapshotview)
| | 404 | Workflow run not found | | -### /apps/{app_id}/workflows/published/runs/{run_id}/node-outputs/events - -#### GET -##### Description - +### [GET] /apps/{app_id}/workflows/published/runs/{run_id}/node-outputs/events Server-Sent Events stream of inspector deltas for a published workflow run. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | run_id | path | Workflow run ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Workflow run node output event stream | | 404 | Workflow run not found | -### /apps/{app_id}/workflows/published/runs/{run_id}/node-outputs/{node_id} - -#### GET -##### Description - +### [GET] /apps/{app_id}/workflows/published/runs/{run_id}/node-outputs/{node_id} One node's declared outputs for a published workflow run. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -4085,21 +3634,17 @@ One node's declared outputs for a published workflow run. | node_id | path | Node ID inside the workflow graph | Yes | string | | run_id | path | Workflow run ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow run node output detail | [NodeOutputsView](#nodeoutputsview) | +| 200 | Workflow run node output detail | **application/json**: [NodeOutputsView](#nodeoutputsview)
| | 404 | Workflow run / node not found | | -### /apps/{app_id}/workflows/published/runs/{run_id}/node-outputs/{node_id}/{output_name}/preview - -#### GET -##### Description - +### [GET] /apps/{app_id}/workflows/published/runs/{run_id}/node-outputs/{node_id}/{output_name}/preview Full value for one declared output of a published run. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -4108,93 +3653,84 @@ Full value for one declared output of a published run. | output_name | path | Declared output name as exposed by Composer | Yes | string | | run_id | path | Workflow run ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow run node output preview | [OutputPreviewView](#outputpreviewview) | +| 200 | Workflow run node output preview | **application/json**: [OutputPreviewView](#outputpreviewview)
| | 404 | Workflow run / node / output not found | | -### /apps/{app_id}/workflows/triggers/webhook +### [GET] /apps/{app_id}/workflows/triggers/webhook +**Get webhook trigger for a node** -#### GET -##### Summary - -Get webhook trigger for a node - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | +| credential_id | query | | No | | +| datasource_type | query | | Yes | string | +| inputs | query | | Yes | object | | app_id | path | | Yes | string | -| payload | body | | Yes | [Parser](#parser) | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [WebhookTriggerResponse](#webhooktriggerresponse) | +| 200 | Success | **application/json**: [WebhookTriggerResponse](#webhooktriggerresponse)
| -### /apps/{app_id}/workflows/{workflow_id} +### [DELETE] /apps/{app_id}/workflows/{workflow_id} +**Delete workflow** -#### DELETE -##### Summary - -Delete workflow - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | | workflow_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -#### PATCH -##### Summary - -Update workflow attributes - -##### Description +### [PATCH] /apps/{app_id}/workflows/{workflow_id} +**Update workflow attributes** Update workflow by ID -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkflowUpdatePayload](#workflowupdatepayload) | | app_id | path | Application ID | Yes | string | | workflow_id | path | Workflow ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [WorkflowUpdatePayload](#workflowupdatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow updated successfully | [WorkflowResponse](#workflowresponse) | +| 200 | Workflow updated successfully | **application/json**: [WorkflowResponse](#workflowresponse)
| | 403 | Permission denied | | | 404 | Workflow not found | | -### /apps/{app_id}/workflows/{workflow_id}/restore - -#### POST -##### Description - +### [POST] /apps/{app_id}/workflows/{workflow_id}/restore Restore a published workflow version into the draft workflow -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | workflow_id | path | Published workflow ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -4202,512 +3738,462 @@ Restore a published workflow version into the draft workflow | 400 | Source workflow must be published | | 404 | Workflow not found | -### /apps/{resource_id}/api-keys +### [GET] /apps/{resource_id}/api-keys +**Get all API keys for an app** -#### GET -##### Summary - -Get all API keys for an app - -##### Description - -Get all API keys for an app - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | resource_id | path | App ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | API keys retrieved successfully | [ApiKeyList](#apikeylist) | +| 200 | API keys retrieved successfully | **application/json**: [ApiKeyList](#apikeylist)
| -#### POST -##### Summary +### [POST] /apps/{resource_id}/api-keys +**Create a new API key for an app** -Create a new API key for an app - -##### Description - -Create a new API key for an app - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | resource_id | path | App ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | API key created successfully | [ApiKeyItem](#apikeyitem) | +| 201 | API key created successfully | **application/json**: [ApiKeyItem](#apikeyitem)
| | 400 | Maximum keys exceeded | | -### /apps/{resource_id}/api-keys/{api_key_id} +### [DELETE] /apps/{resource_id}/api-keys/{api_key_id} +**Delete an API key for an app** -#### DELETE -##### Summary - -Delete an API key for an app - -##### Description - -Delete an API key for an app - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | api_key_id | path | API key ID | Yes | string | | resource_id | path | App ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | API key deleted successfully | -### /apps/{server_id}/server/refresh - -#### GET -##### Description - +### [GET] /apps/{server_id}/server/refresh Refresh MCP server configuration and regenerate server code -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | server_id | path | Server ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | MCP server refreshed successfully | [AppMCPServerResponse](#appmcpserverresponse) | +| 200 | MCP server refreshed successfully | **application/json**: [AppMCPServerResponse](#appmcpserverresponse)
| | 403 | Insufficient permissions | | | 404 | Server not found | | -### /auth/plugin/datasource/default-list - -#### GET -##### Responses +### [GET] /auth/plugin/datasource/default-list +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /auth/plugin/datasource/list - -#### GET -##### Responses +### [GET] /auth/plugin/datasource/list +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /auth/plugin/datasource/{provider_id} - -#### GET -##### Parameters +### [GET] /auth/plugin/datasource/{provider_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -#### POST -##### Parameters +### [POST] /auth/plugin/datasource/{provider_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider_id | path | | Yes | string | -| payload | body | | Yes | [DatasourceCredentialPayload](#datasourcecredentialpayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DatasourceCredentialPayload](#datasourcecredentialpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /auth/plugin/datasource/{provider_id}/custom-client - -#### DELETE -##### Parameters +### [DELETE] /auth/plugin/datasource/{provider_id}/custom-client +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -#### POST -##### Parameters +### [POST] /auth/plugin/datasource/{provider_id}/custom-client +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider_id | path | | Yes | string | -| payload | body | | Yes | [DatasourceCustomClientPayload](#datasourcecustomclientpayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DatasourceCustomClientPayload](#datasourcecustomclientpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /auth/plugin/datasource/{provider_id}/default - -#### POST -##### Parameters +### [POST] /auth/plugin/datasource/{provider_id}/default +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider_id | path | | Yes | string | -| payload | body | | Yes | [DatasourceDefaultPayload](#datasourcedefaultpayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DatasourceDefaultPayload](#datasourcedefaultpayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -### /auth/plugin/datasource/{provider_id}/delete - -#### POST -##### Parameters +### [POST] /auth/plugin/datasource/{provider_id}/delete +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider_id | path | | Yes | string | -| payload | body | | Yes | [DatasourceCredentialDeletePayload](#datasourcecredentialdeletepayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DatasourceCredentialDeletePayload](#datasourcecredentialdeletepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -### /auth/plugin/datasource/{provider_id}/update - -#### POST -##### Parameters +### [POST] /auth/plugin/datasource/{provider_id}/update +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider_id | path | | Yes | string | -| payload | body | | Yes | [DatasourceCredentialUpdatePayload](#datasourcecredentialupdatepayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DatasourceCredentialUpdatePayload](#datasourcecredentialupdatepayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /auth/plugin/datasource/{provider_id}/update-name - -#### POST -##### Parameters +### [POST] /auth/plugin/datasource/{provider_id}/update-name +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider_id | path | | Yes | string | -| payload | body | | Yes | [DatasourceUpdateNamePayload](#datasourceupdatenamepayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DatasourceUpdateNamePayload](#datasourceupdatenamepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -### /billing/invoices - -#### GET -##### Responses +### [GET] /billing/invoices +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /billing/partners/{partner_key}/tenants - -#### PUT -##### Description - +### [PUT] /billing/partners/{partner_key}/tenants Sync partner tenants bindings -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [PartnerTenantsPayload](#partnertenantspayload) | | partner_key | path | Partner key | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [PartnerTenantsPayload](#partnertenantspayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Tenants synced to partner successfully | | 400 | Invalid partner information | -### /billing/subscription - -#### GET -##### Responses +### [GET] /billing/subscription +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /code-based-extension - -#### GET -##### Description - +### [GET] /code-based-extension Get code-based extension data by module name -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | module | query | Extension module name | No | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [CodeBasedExtensionResponse](#codebasedextensionresponse) | - -### /compliance/download - -#### GET -##### Description +| 200 | Success | **application/json**: [CodeBasedExtensionResponse](#codebasedextensionresponse)
| +### [GET] /compliance/download Get compliance document download link -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ComplianceDownloadQuery](#compliancedownloadquery) | +| doc_name | query | Compliance document name | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /data-source/integrates - -#### GET -##### Responses +### [GET] /data-source/integrates +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [DataSourceIntegrateListResponse](#datasourceintegratelistresponse) | +| 200 | Success | **application/json**: [DataSourceIntegrateListResponse](#datasourceintegratelistresponse)
| -#### PATCH -##### Responses +### [PATCH] /data-source/integrates +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -### /data-source/integrates/{binding_id}/{action} - -#### GET -##### Parameters +### [GET] /data-source/integrates/{binding_id}/{action} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | action | path | | Yes | string | | binding_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [DataSourceIntegrateListResponse](#datasourceintegratelistresponse) | +| 200 | Success | **application/json**: [DataSourceIntegrateListResponse](#datasourceintegratelistresponse)
| -#### PATCH -##### Parameters +### [PATCH] /data-source/integrates/{binding_id}/{action} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | action | path | | Yes | string | | binding_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | - -### /datasets - -#### GET -##### Description +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| +### [GET] /datasets Get list of datasets -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | ids | query | Filter by dataset IDs | No | [ string ] | | include_all | query | Include all datasets | No | boolean | | keyword | query | Search keyword | No | string | -| limit | query | Number of items per page | No | integer | -| page | query | Page number | No | integer | +| limit | query | Number of items per page | No | integer,
**Default:** 20 | +| page | query | Page number | No | integer,
**Default:** 1 | | tag_ids | query | Filter by tag IDs | No | [ string ] | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Datasets retrieved successfully | [DatasetListResponse](#datasetlistresponse) | - -#### POST -##### Description +| 200 | Datasets retrieved successfully | **application/json**: [DatasetListResponse](#datasetlistresponse)
| +### [POST] /datasets Create a new dataset -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [DatasetCreatePayload](#datasetcreatepayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DatasetCreatePayload](#datasetcreatepayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | Dataset created successfully | [DatasetDetailResponse](#datasetdetailresponse) | +| 201 | Dataset created successfully | **application/json**: [DatasetDetailResponse](#datasetdetailresponse)
| | 400 | Invalid request parameters | | -### /datasets/api-base-info - -#### GET -##### Description - +### [GET] /datasets/api-base-info Get dataset API base information -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | API base info retrieved successfully | [ApiBaseUrlResponse](#apibaseurlresponse) | - -### /datasets/api-keys - -#### GET -##### Description +| 200 | API base info retrieved successfully | **application/json**: [ApiBaseUrlResponse](#apibaseurlresponse)
| +### [GET] /datasets/api-keys Get dataset API keys -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | API keys retrieved successfully | [ApiKeyList](#apikeylist) | +| 200 | API keys retrieved successfully | **application/json**: [ApiKeyList](#apikeylist)
| -#### POST -##### Responses +### [POST] /datasets/api-keys +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | API key created successfully | [ApiKeyItem](#apikeyitem) | +| 200 | API key created successfully | **application/json**: [ApiKeyItem](#apikeyitem)
| | 400 | Maximum keys exceeded | | -### /datasets/api-keys/{api_key_id} - -#### DELETE -##### Description - +### [DELETE] /datasets/api-keys/{api_key_id} Delete dataset API key -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | api_key_id | path | API key ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | API key deleted successfully | -### /datasets/batch_import_status/{job_id} - -#### GET -##### Parameters +### [GET] /datasets/batch_import_status/{job_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | job_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Batch import status | [SegmentBatchImportStatusResponse](#segmentbatchimportstatusresponse) | +| 200 | Batch import status | **application/json**: [SegmentBatchImportStatusResponse](#segmentbatchimportstatusresponse)
| -#### POST -##### Parameters +### [POST] /datasets/batch_import_status/{job_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | job_id | path | | Yes | string | -| payload | body | | Yes | [BatchImportPayload](#batchimportpayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [BatchImportPayload](#batchimportpayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Batch import started | [SegmentBatchImportStatusResponse](#segmentbatchimportstatusresponse) | - -### /datasets/external - -#### POST -##### Description +| 200 | Batch import started | **application/json**: [SegmentBatchImportStatusResponse](#segmentbatchimportstatusresponse)
| +### [POST] /datasets/external Create external knowledge dataset -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ExternalDatasetCreatePayload](#externaldatasetcreatepayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ExternalDatasetCreatePayload](#externaldatasetcreatepayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | External dataset created successfully | [DatasetDetail](#datasetdetail) | +| 201 | External dataset created successfully | **application/json**: [DatasetDetail](#datasetdetail)
| | 400 | Invalid parameters | | | 403 | Permission denied | | -### /datasets/external-knowledge-api - -#### GET -##### Description - +### [GET] /datasets/external-knowledge-api Get external knowledge API templates -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -4715,348 +4201,306 @@ Get external knowledge API templates | limit | query | Number of items per page (default: 20) | No | string | | page | query | Page number (default: 1) | No | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | External API templates retrieved successfully | -#### POST -##### Parameters +### [POST] /datasets/external-knowledge-api +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ExternalKnowledgeApiPayload](#externalknowledgeapipayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ExternalKnowledgeApiPayload](#externalknowledgeapipayload)
| -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /datasets/external-knowledge-api/{external_knowledge_api_id} - -#### DELETE -##### Parameters +### [DELETE] /datasets/external-knowledge-api/{external_knowledge_api_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | external_knowledge_api_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | External knowledge API deleted successfully | -#### GET -##### Description - +### [GET] /datasets/external-knowledge-api/{external_knowledge_api_id} Get external knowledge API template details -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | external_knowledge_api_id | path | External knowledge API ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | External API template retrieved successfully | | 404 | Template not found | -#### PATCH -##### Parameters +### [PATCH] /datasets/external-knowledge-api/{external_knowledge_api_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ExternalKnowledgeApiPayload](#externalknowledgeapipayload) | | external_knowledge_api_id | path | | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ExternalKnowledgeApiPayload](#externalknowledgeapipayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /datasets/external-knowledge-api/{external_knowledge_api_id}/use-check - -#### GET -##### Description - +### [GET] /datasets/external-knowledge-api/{external_knowledge_api_id}/use-check Check if external knowledge API is being used -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | external_knowledge_api_id | path | External knowledge API ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Usage check completed successfully | [UsageCountResponse](#usagecountresponse) | - -### /datasets/indexing-estimate - -#### POST -##### Description +| 200 | Usage check completed successfully | **application/json**: [UsageCountResponse](#usagecountresponse)
| +### [POST] /datasets/indexing-estimate Estimate dataset indexing cost -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [IndexingEstimatePayload](#indexingestimatepayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [IndexingEstimatePayload](#indexingestimatepayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Indexing estimate calculated successfully | [IndexingEstimateResponse](#indexingestimateresponse) | - -### /datasets/init - -#### POST -##### Description +| 200 | Indexing estimate calculated successfully | **application/json**: [IndexingEstimateResponse](#indexingestimateresponse)
| +### [POST] /datasets/init Initialize dataset with documents -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [KnowledgeConfig](#knowledgeconfig) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [KnowledgeConfig](#knowledgeconfig)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | Dataset initialized successfully | [DatasetAndDocumentResponse](#datasetanddocumentresponse) | +| 201 | Dataset initialized successfully | **application/json**: [DatasetAndDocumentResponse](#datasetanddocumentresponse)
| | 400 | Invalid request parameters | | -### /datasets/metadata/built-in - -#### GET -##### Responses +### [GET] /datasets/metadata/built-in +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Built-in fields retrieved successfully | [DatasetMetadataBuiltInFieldsResponse](#datasetmetadatabuiltinfieldsresponse) | +| 200 | Built-in fields retrieved successfully | **application/json**: [DatasetMetadataBuiltInFieldsResponse](#datasetmetadatabuiltinfieldsresponse)
| -### /datasets/notion-indexing-estimate +### [POST] /datasets/notion-indexing-estimate +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [NotionEstimatePayload](#notionestimatepayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [NotionEstimatePayload](#notionestimatepayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [IndexingEstimate](#indexingestimate) | - -### /datasets/process-rule - -#### GET -##### Description +| 200 | Success | **application/json**: [IndexingEstimate](#indexingestimate)
| +### [GET] /datasets/process-rule Get dataset document processing rules -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | document_id | query | Document ID (optional) | No | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Process rules retrieved successfully | -### /datasets/retrieval-setting - -#### GET -##### Description - +### [GET] /datasets/retrieval-setting Get dataset retrieval settings -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Retrieval settings retrieved successfully | [RetrievalSettingResponse](#retrievalsettingresponse) | - -### /datasets/retrieval-setting/{vector_type} - -#### GET -##### Description +| 200 | Retrieval settings retrieved successfully | **application/json**: [RetrievalSettingResponse](#retrievalsettingresponse)
| +### [GET] /datasets/retrieval-setting/{vector_type} Get mock dataset retrieval settings by vector type -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | vector_type | path | Vector store type | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Mock retrieval settings retrieved successfully | [RetrievalSettingResponse](#retrievalsettingresponse) | +| 200 | Mock retrieval settings retrieved successfully | **application/json**: [RetrievalSettingResponse](#retrievalsettingresponse)
| -### /datasets/{dataset_id} - -#### DELETE -##### Parameters +### [DELETE] /datasets/{dataset_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Dataset deleted successfully | -#### GET -##### Description - +### [GET] /datasets/{dataset_id} Get dataset details -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Dataset retrieved successfully | [DatasetDetailWithPartialMembersResponse](#datasetdetailwithpartialmembersresponse) | +| 200 | Dataset retrieved successfully | **application/json**: [DatasetDetailWithPartialMembersResponse](#datasetdetailwithpartialmembersresponse)
| | 403 | Permission denied | | | 404 | Dataset not found | | -#### PATCH -##### Description - +### [PATCH] /datasets/{dataset_id} Update dataset details -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [DatasetUpdatePayload](#datasetupdatepayload) | | dataset_id | path | | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DatasetUpdatePayload](#datasetupdatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Dataset updated successfully | [DatasetDetailWithPartialMembersResponse](#datasetdetailwithpartialmembersresponse) | +| 200 | Dataset updated successfully | **application/json**: [DatasetDetailWithPartialMembersResponse](#datasetdetailwithpartialmembersresponse)
| | 403 | Permission denied | | | 404 | Dataset not found | | -### /datasets/{dataset_id}/api-keys/{status} - -#### POST -##### Parameters +### [POST] /datasets/{dataset_id}/api-keys/{status} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string | | status | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | - -### /datasets/{dataset_id}/auto-disable-logs - -#### GET -##### Description +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| +### [GET] /datasets/{dataset_id}/auto-disable-logs Get dataset auto disable logs -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Auto disable logs retrieved successfully | [AutoDisableLogsResponse](#autodisablelogsresponse) | +| 200 | Auto disable logs retrieved successfully | **application/json**: [AutoDisableLogsResponse](#autodisablelogsresponse)
| | 404 | Dataset not found | | -### /datasets/{dataset_id}/batch/{batch}/indexing-estimate - -#### GET -##### Parameters +### [GET] /datasets/{dataset_id}/batch/{batch}/indexing-estimate +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | batch | path | | Yes | string | | dataset_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /datasets/{dataset_id}/batch/{batch}/indexing-status - -#### GET -##### Parameters +### [GET] /datasets/{dataset_id}/batch/{batch}/indexing-status +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | batch | path | | Yes | string | | dataset_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Indexing status retrieved successfully | [DocumentStatusListResponse](#documentstatuslistresponse) | +| 200 | Indexing status retrieved successfully | **application/json**: [DocumentStatusListResponse](#documentstatuslistresponse)
| -### /datasets/{dataset_id}/documents - -#### DELETE -##### Parameters +### [DELETE] /datasets/{dataset_id}/documents +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Documents deleted successfully | -#### GET -##### Description - +### [GET] /datasets/{dataset_id}/documents Get documents in a dataset -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -5068,134 +4512,134 @@ Get documents in a dataset | sort | query | Sort order (default: -created_at) | No | string | | status | query | Filter documents by display status | No | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Documents retrieved successfully | [DocumentWithSegmentsListResponse](#documentwithsegmentslistresponse) | +| 200 | Documents retrieved successfully | **application/json**: [DocumentWithSegmentsListResponse](#documentwithsegmentslistresponse)
| -#### POST -##### Parameters +### [POST] /datasets/{dataset_id}/documents +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [KnowledgeConfig](#knowledgeconfig) | | dataset_id | path | | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [KnowledgeConfig](#knowledgeconfig)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Documents created successfully | [DatasetAndDocumentResponse](#datasetanddocumentresponse) | +| 200 | Documents created successfully | **application/json**: [DatasetAndDocumentResponse](#datasetanddocumentresponse)
| -### /datasets/{dataset_id}/documents/download-zip - -#### POST -##### Summary - -Stream a ZIP archive containing the requested uploaded documents - -##### Description +### [POST] /datasets/{dataset_id}/documents/download-zip +**Stream a ZIP archive containing the requested uploaded documents** Download selected dataset documents as a single ZIP archive (upload-file only) -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string | -| payload | body | | Yes | [DocumentBatchDownloadZipPayload](#documentbatchdownloadzippayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DocumentBatchDownloadZipPayload](#documentbatchdownloadzippayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /datasets/{dataset_id}/documents/generate-summary - -#### POST -##### Summary - -Generate summary index for specified documents - -##### Description +### [POST] /datasets/{dataset_id}/documents/generate-summary +**Generate summary index for specified documents** Generate summary index for documents This endpoint checks if the dataset configuration supports summary generation (indexing_technique must be 'high_quality' and summary_index_setting.enable must be true), then asynchronously generates summary indexes for the provided documents. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [GenerateSummaryPayload](#generatesummarypayload) | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [GenerateSummaryPayload](#generatesummarypayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Summary generation started successfully | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Summary generation started successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| | 400 | Invalid request or dataset configuration | | | 403 | Permission denied | | | 404 | Dataset not found | | -### /datasets/{dataset_id}/documents/metadata - -#### POST -##### Parameters +### [POST] /datasets/{dataset_id}/documents/metadata +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string | -| payload | body | | Yes | [MetadataOperationData](#metadataoperationdata) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [MetadataOperationData](#metadataoperationdata)
| + +#### Responses | Code | Description | | ---- | ----------- | | 204 | Documents metadata updated successfully | -### /datasets/{dataset_id}/documents/status/{action}/batch - -#### PATCH -##### Parameters +### [PATCH] /datasets/{dataset_id}/documents/status/{action}/batch +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | action | path | | Yes | string | | dataset_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -### /datasets/{dataset_id}/documents/{document_id} - -#### DELETE -##### Parameters +### [DELETE] /datasets/{dataset_id}/documents/{document_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string | | document_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Document deleted successfully | -#### GET -##### Description - +### [GET] /datasets/{dataset_id}/documents/{document_id} Get document details -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -5203,48 +4647,40 @@ Get document details | document_id | path | Document ID | Yes | string | | metadata | query | Metadata inclusion (all/only/without) | No | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Document retrieved successfully | | 404 | Document not found | -### /datasets/{dataset_id}/documents/{document_id}/download - -#### GET -##### Description - +### [GET] /datasets/{dataset_id}/documents/{document_id}/download Get a signed download URL for a dataset document's original uploaded file -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string | | document_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Download URL generated successfully | [UrlResponse](#urlresponse) | - -### /datasets/{dataset_id}/documents/{document_id}/indexing-estimate - -#### GET -##### Description +| 200 | Download URL generated successfully | **application/json**: [UrlResponse](#urlresponse)
| +### [GET] /datasets/{dataset_id}/documents/{document_id}/indexing-estimate Estimate document indexing cost -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | Dataset ID | Yes | string | | document_id | path | Document ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -5252,130 +4688,111 @@ Estimate document indexing cost | 400 | Document already finished | | 404 | Document not found | -### /datasets/{dataset_id}/documents/{document_id}/indexing-status - -#### GET -##### Description - +### [GET] /datasets/{dataset_id}/documents/{document_id}/indexing-status Get document indexing status -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | Dataset ID | Yes | string | | document_id | path | Document ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Indexing status retrieved successfully | [DocumentStatusResponse](#documentstatusresponse) | +| 200 | Indexing status retrieved successfully | **application/json**: [DocumentStatusResponse](#documentstatusresponse)
| | 404 | Document not found | | -### /datasets/{dataset_id}/documents/{document_id}/metadata - -#### PUT -##### Description - +### [PUT] /datasets/{dataset_id}/documents/{document_id}/metadata Update document metadata -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [DocumentMetadataUpdatePayload](#documentmetadataupdatepayload) | | dataset_id | path | Dataset ID | Yes | string | | document_id | path | Document ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DocumentMetadataUpdatePayload](#documentmetadataupdatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Document metadata updated successfully | [SimpleResultMessageResponse](#simpleresultmessageresponse) | +| 200 | Document metadata updated successfully | **application/json**: [SimpleResultMessageResponse](#simpleresultmessageresponse)
| | 403 | Permission denied | | | 404 | Document not found | | -### /datasets/{dataset_id}/documents/{document_id}/notion/sync - -#### GET -##### Parameters +### [GET] /datasets/{dataset_id}/documents/{document_id}/notion/sync +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string | | document_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -### /datasets/{dataset_id}/documents/{document_id}/pipeline-execution-log - -#### GET -##### Parameters +### [GET] /datasets/{dataset_id}/documents/{document_id}/pipeline-execution-log +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string | | document_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /datasets/{dataset_id}/documents/{document_id}/processing/pause +### [PATCH] /datasets/{dataset_id}/documents/{document_id}/processing/pause +**pause document** -#### PATCH -##### Summary - -pause document - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string | | document_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Document paused successfully | -### /datasets/{dataset_id}/documents/{document_id}/processing/resume +### [PATCH] /datasets/{dataset_id}/documents/{document_id}/processing/resume +**recover document** -#### PATCH -##### Summary - -recover document - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string | | document_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Document resumed successfully | -### /datasets/{dataset_id}/documents/{document_id}/processing/{action} - -#### PATCH -##### Description - +### [PATCH] /datasets/{dataset_id}/documents/{document_id}/processing/{action} Update document processing status (pause/resume) -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -5383,52 +4800,56 @@ Update document processing status (pause/resume) | dataset_id | path | Dataset ID | Yes | string | | document_id | path | Document ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Processing status updated successfully | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Processing status updated successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| | 400 | Invalid action | | | 404 | Document not found | | -### /datasets/{dataset_id}/documents/{document_id}/rename - -#### POST -##### Parameters +### [POST] /datasets/{dataset_id}/documents/{document_id}/rename +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string | | document_id | path | | Yes | string | -| payload | body | | Yes | [DocumentRenamePayload](#documentrenamepayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DocumentRenamePayload](#documentrenamepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Document renamed successfully | [DocumentResponse](#documentresponse) | +| 200 | Document renamed successfully | **application/json**: [DocumentResponse](#documentresponse)
| -### /datasets/{dataset_id}/documents/{document_id}/segment - -#### POST -##### Parameters +### [POST] /datasets/{dataset_id}/documents/{document_id}/segment +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [SegmentCreatePayload](#segmentcreatepayload) | | dataset_id | path | Dataset ID | Yes | string | | document_id | path | Document ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [SegmentCreatePayload](#segmentcreatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Segment created successfully | [SegmentDetailResponse](#segmentdetailresponse) | +| 200 | Segment created successfully | **application/json**: [SegmentDetailResponse](#segmentdetailresponse)
| -### /datasets/{dataset_id}/documents/{document_id}/segment/{action} - -#### PATCH -##### Parameters +### [PATCH] /datasets/{dataset_id}/documents/{document_id}/segment/{action} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -5437,16 +4858,14 @@ Update document processing status (pause/resume) | document_id | path | Document ID | Yes | string | | segment_id | query | Segment IDs | No | [ string ] | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -### /datasets/{dataset_id}/documents/{document_id}/segments - -#### DELETE -##### Parameters +### [DELETE] /datasets/{dataset_id}/documents/{document_id}/segments +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -5454,67 +4873,68 @@ Update document processing status (pause/resume) | document_id | path | Document ID | Yes | string | | segment_id | query | Segment IDs | No | [ string ] | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Segments deleted successfully | -#### GET -##### Parameters +### [GET] /datasets/{dataset_id}/documents/{document_id}/segments +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | Dataset ID | Yes | string | | document_id | path | Document ID | Yes | string | -| enabled | query | | No | string | +| enabled | query | | No | string,
**Default:** all | | hit_count_gte | query | | No | integer | | keyword | query | | No | string | -| limit | query | | No | integer | -| page | query | | No | integer | +| limit | query | | No | integer,
**Default:** 20 | +| page | query | | No | integer,
**Default:** 1 | | status | query | | No | [ string ] | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Segments retrieved successfully | [ConsoleSegmentListResponse](#consolesegmentlistresponse) | +| 200 | Segments retrieved successfully | **application/json**: [ConsoleSegmentListResponse](#consolesegmentlistresponse)
| -### /datasets/{dataset_id}/documents/{document_id}/segments/batch_import - -#### GET -##### Parameters +### [GET] /datasets/{dataset_id}/documents/{document_id}/segments/batch_import +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string | | document_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Batch import status | [SegmentBatchImportStatusResponse](#segmentbatchimportstatusresponse) | +| 200 | Batch import status | **application/json**: [SegmentBatchImportStatusResponse](#segmentbatchimportstatusresponse)
| -#### POST -##### Parameters +### [POST] /datasets/{dataset_id}/documents/{document_id}/segments/batch_import +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string | | document_id | path | | Yes | string | -| payload | body | | Yes | [BatchImportPayload](#batchimportpayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [BatchImportPayload](#batchimportpayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Batch import started | [SegmentBatchImportStatusResponse](#segmentbatchimportstatusresponse) | +| 200 | Batch import started | **application/json**: [SegmentBatchImportStatusResponse](#segmentbatchimportstatusresponse)
| -### /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id} - -#### DELETE -##### Parameters +### [DELETE] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -5522,32 +4942,35 @@ Update document processing status (pause/resume) | document_id | path | Document ID | Yes | string | | segment_id | path | Segment ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Segment deleted successfully | -#### PATCH -##### Parameters +### [PATCH] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [SegmentUpdatePayload](#segmentupdatepayload) | | dataset_id | path | Dataset ID | Yes | string | | document_id | path | Document ID | Yes | string | | segment_id | path | Segment ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [SegmentUpdatePayload](#segmentupdatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Segment updated successfully | [SegmentDetailResponse](#segmentdetailresponse) | +| 200 | Segment updated successfully | **application/json**: [SegmentDetailResponse](#segmentdetailresponse)
| -### /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks - -#### GET -##### Parameters +### [GET] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -5555,51 +4978,59 @@ Update document processing status (pause/resume) | document_id | path | Document ID | Yes | string | | segment_id | path | Parent segment ID | Yes | string | | keyword | query | | No | string | -| limit | query | | No | integer | -| page | query | | No | integer | +| limit | query | | No | integer,
**Default:** 20 | +| page | query | | No | integer,
**Default:** 1 | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Child chunks retrieved successfully | [ChildChunkListResponse](#childchunklistresponse) | +| 200 | Child chunks retrieved successfully | **application/json**: [ChildChunkListResponse](#childchunklistresponse)
| -#### PATCH -##### Parameters +### [PATCH] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ChildChunkBatchUpdatePayload](#childchunkbatchupdatepayload) | | dataset_id | path | Dataset ID | Yes | string | | document_id | path | Document ID | Yes | string | | segment_id | path | Parent segment ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ChildChunkBatchUpdatePayload](#childchunkbatchupdatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Child chunks updated successfully | [ChildChunkBatchUpdateResponse](#childchunkbatchupdateresponse) | +| 200 | Child chunks updated successfully | **application/json**: [ChildChunkBatchUpdateResponse](#childchunkbatchupdateresponse)
| -#### POST -##### Parameters +### [POST] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ChildChunkCreatePayload](#childchunkcreatepayload) | | dataset_id | path | Dataset ID | Yes | string | | document_id | path | Document ID | Yes | string | | segment_id | path | Parent segment ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ChildChunkCreatePayload](#childchunkcreatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Child chunk created successfully | [ChildChunkDetailResponse](#childchunkdetailresponse) | +| 200 | Child chunk created successfully | **application/json**: [ChildChunkDetailResponse](#childchunkdetailresponse)
| -### /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id} - -#### DELETE -##### Parameters +### [DELETE] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -5608,37 +5039,36 @@ Update document processing status (pause/resume) | document_id | path | Document ID | Yes | string | | segment_id | path | Parent segment ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Child chunk deleted successfully | -#### PATCH -##### Parameters +### [PATCH] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ChildChunkUpdatePayload](#childchunkupdatepayload) | | child_chunk_id | path | Child chunk ID | Yes | string | | dataset_id | path | Dataset ID | Yes | string | | document_id | path | Document ID | Yes | string | | segment_id | path | Parent segment ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ChildChunkUpdatePayload](#childchunkupdatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Child chunk updated successfully | [ChildChunkDetailResponse](#childchunkdetailresponse) | +| 200 | Child chunk updated successfully | **application/json**: [ChildChunkDetailResponse](#childchunkdetailresponse)
| -### /datasets/{dataset_id}/documents/{document_id}/summary-status - -#### GET -##### Summary - -Get summary index generation status for a document - -##### Description +### [GET] /datasets/{dataset_id}/documents/{document_id}/summary-status +**Get summary index generation status for a document** Get summary index generation status for a document Returns: @@ -5650,75 +5080,68 @@ Returns: - not_started: Number of segments without summary records - summaries: List of summary records with status and content preview -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | Dataset ID | Yes | string | | document_id | path | Document ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Summary status retrieved successfully | | 404 | Document not found | -### /datasets/{dataset_id}/documents/{document_id}/website-sync +### [GET] /datasets/{dataset_id}/documents/{document_id}/website-sync +**sync website document** -#### GET -##### Summary - -sync website document - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string | | document_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | - -### /datasets/{dataset_id}/error-docs - -#### GET -##### Description +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| +### [GET] /datasets/{dataset_id}/error-docs Get dataset error documents -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Error documents retrieved successfully | [ErrorDocsResponse](#errordocsresponse) | +| 200 | Error documents retrieved successfully | **application/json**: [ErrorDocsResponse](#errordocsresponse)
| | 404 | Dataset not found | | -### /datasets/{dataset_id}/external-hit-testing - -#### POST -##### Description - +### [POST] /datasets/{dataset_id}/external-hit-testing Test external knowledge retrieval for dataset -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ExternalHitTestingPayload](#externalhittestingpayload) | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ExternalHitTestingPayload](#externalhittestingpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | @@ -5726,555 +5149,459 @@ Test external knowledge retrieval for dataset | 400 | Invalid parameters | | 404 | Dataset not found | -### /datasets/{dataset_id}/hit-testing - -#### POST -##### Description - +### [POST] /datasets/{dataset_id}/hit-testing Test dataset knowledge retrieval -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [HitTestingPayload](#hittestingpayload) | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [HitTestingPayload](#hittestingpayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Hit testing completed successfully | [HitTestingResponse](#hittestingresponse) | +| 200 | Hit testing completed successfully | **application/json**: [HitTestingResponse](#hittestingresponse)
| | 400 | Invalid parameters | | | 404 | Dataset not found | | -### /datasets/{dataset_id}/indexing-status - -#### GET -##### Description - +### [GET] /datasets/{dataset_id}/indexing-status Get dataset indexing status -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Indexing status retrieved successfully | [DocumentStatusListResponse](#documentstatuslistresponse) | +| 200 | Indexing status retrieved successfully | **application/json**: [DocumentStatusListResponse](#documentstatuslistresponse)
| -### /datasets/{dataset_id}/metadata - -#### GET -##### Parameters +### [GET] /datasets/{dataset_id}/metadata +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Metadata retrieved successfully | [DatasetMetadataListResponse](#datasetmetadatalistresponse) | +| 200 | Metadata retrieved successfully | **application/json**: [DatasetMetadataListResponse](#datasetmetadatalistresponse)
| -#### POST -##### Parameters +### [POST] /datasets/{dataset_id}/metadata +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string | -| payload | body | | Yes | [MetadataArgs](#metadataargs) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [MetadataArgs](#metadataargs)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | Metadata created successfully | [DatasetMetadataResponse](#datasetmetadataresponse) | +| 201 | Metadata created successfully | **application/json**: [DatasetMetadataResponse](#datasetmetadataresponse)
| -### /datasets/{dataset_id}/metadata/built-in/{action} - -#### POST -##### Parameters +### [POST] /datasets/{dataset_id}/metadata/built-in/{action} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | action | path | | Yes | string | | dataset_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Action completed successfully | -### /datasets/{dataset_id}/metadata/{metadata_id} - -#### DELETE -##### Parameters +### [DELETE] /datasets/{dataset_id}/metadata/{metadata_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string | | metadata_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Metadata deleted successfully | -#### PATCH -##### Parameters +### [PATCH] /datasets/{dataset_id}/metadata/{metadata_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string | | metadata_id | path | | Yes | string | -| payload | body | | Yes | [MetadataUpdatePayload](#metadataupdatepayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [MetadataUpdatePayload](#metadataupdatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Metadata updated successfully | [DatasetMetadataResponse](#datasetmetadataresponse) | +| 200 | Metadata updated successfully | **application/json**: [DatasetMetadataResponse](#datasetmetadataresponse)
| -### /datasets/{dataset_id}/notion/sync - -#### GET -##### Parameters +### [GET] /datasets/{dataset_id}/notion/sync +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | - -### /datasets/{dataset_id}/permission-part-users - -#### GET -##### Description +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| +### [GET] /datasets/{dataset_id}/permission-part-users Get dataset permission user list -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Permission users retrieved successfully | [PartialMemberListResponse](#partialmemberlistresponse) | +| 200 | Permission users retrieved successfully | **application/json**: [PartialMemberListResponse](#partialmemberlistresponse)
| | 403 | Permission denied | | | 404 | Dataset not found | | -### /datasets/{dataset_id}/queries - -#### GET -##### Description - +### [GET] /datasets/{dataset_id}/queries Get dataset query history -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Query history retrieved successfully | [DatasetQueryListResponse](#datasetquerylistresponse) | - -### /datasets/{dataset_id}/related-apps - -#### GET -##### Description +| 200 | Query history retrieved successfully | **application/json**: [DatasetQueryListResponse](#datasetquerylistresponse)
| +### [GET] /datasets/{dataset_id}/related-apps Get applications related to dataset -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Related apps retrieved successfully | [RelatedAppListResponse](#relatedapplistresponse) | +| 200 | Related apps retrieved successfully | **application/json**: [RelatedAppListResponse](#relatedapplistresponse)
| -### /datasets/{dataset_id}/retry +### [POST] /datasets/{dataset_id}/retry +**retry document** -#### POST -##### Summary - -retry document - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string | -| payload | body | | Yes | [DocumentRetryPayload](#documentretrypayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DocumentRetryPayload](#documentretrypayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 204 | Documents retry started successfully | -### /datasets/{dataset_id}/use-check - -#### GET -##### Description - +### [GET] /datasets/{dataset_id}/use-check Check if dataset is in use -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Dataset use status retrieved successfully | [UsageCheckResponse](#usagecheckresponse) | +| 200 | Dataset use status retrieved successfully | **application/json**: [UsageCheckResponse](#usagecheckresponse)
| -### /datasets/{resource_id}/api-keys +### [GET] /datasets/{resource_id}/api-keys +**Get all API keys for a dataset** -#### GET -##### Summary - -Get all API keys for a dataset - -##### Description - -Get all API keys for a dataset - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | resource_id | path | Dataset ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | API keys retrieved successfully | [ApiKeyList](#apikeylist) | +| 200 | API keys retrieved successfully | **application/json**: [ApiKeyList](#apikeylist)
| -#### POST -##### Summary +### [POST] /datasets/{resource_id}/api-keys +**Create a new API key for a dataset** -Create a new API key for a dataset - -##### Description - -Create a new API key for a dataset - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | resource_id | path | Dataset ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | API key created successfully | [ApiKeyItem](#apikeyitem) | +| 201 | API key created successfully | **application/json**: [ApiKeyItem](#apikeyitem)
| | 400 | Maximum keys exceeded | | -### /datasets/{resource_id}/api-keys/{api_key_id} +### [DELETE] /datasets/{resource_id}/api-keys/{api_key_id} +**Delete an API key for a dataset** -#### DELETE -##### Summary - -Delete an API key for a dataset - -##### Description - -Delete an API key for a dataset - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | api_key_id | path | API key ID | Yes | string | | resource_id | path | Dataset ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | API key deleted successfully | -### /email-code-login +### [POST] /email-code-login +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [EmailPayload](#emailpayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [EmailPayload](#emailpayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultDataResponse](#simpleresultdataresponse) | +| 200 | Success | **application/json**: [SimpleResultDataResponse](#simpleresultdataresponse)
| -### /email-code-login/validity +### [POST] /email-code-login/validity +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [EmailCodeLoginPayload](#emailcodeloginpayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [EmailCodeLoginPayload](#emailcodeloginpayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -### /email-register - -#### POST -##### Responses +### [POST] /email-register +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /email-register/send-email - -#### POST -##### Responses +### [POST] /email-register/send-email +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultDataResponse](#simpleresultdataresponse) | +| 200 | Success | **application/json**: [SimpleResultDataResponse](#simpleresultdataresponse)
| -### /email-register/validity - -#### POST -##### Responses +### [POST] /email-register/validity +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [VerificationTokenResponse](#verificationtokenresponse) | +| 200 | Success | **application/json**: [VerificationTokenResponse](#verificationtokenresponse)
| -### /explore/apps - -#### GET -##### Parameters +### [GET] /explore/apps +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | language | query | Language code for recommended app localization | No | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [RecommendedAppListResponse](#recommendedapplistresponse) | +| 200 | Success | **application/json**: [RecommendedAppListResponse](#recommendedapplistresponse)
| -### /explore/apps/{app_id} - -#### GET -##### Parameters +### [GET] /explore/apps/{app_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /features +### [GET] /features +**Get feature configuration for current tenant** -#### GET -##### Summary - -Get feature configuration for current tenant - -##### Description - -Get feature configuration for current tenant - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [FeatureModel](#featuremodel) | +| 200 | Success | **application/json**: [FeatureModel](#featuremodel)
| -### /features/vector-space +### [GET] /features/vector-space +**Get vector-space usage and limit for current tenant** -#### GET -##### Summary - -Get vector-space usage and limit for current tenant - -##### Description - -Get vector-space usage and limit for current tenant - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [LimitationModel](#limitationmodel) | +| 200 | Success | **application/json**: [LimitationModel](#limitationmodel)
| -### /files/support-type - -#### GET -##### Responses +### [GET] /files/support-type +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [AllowedExtensionsResponse](#allowedextensionsresponse) | +| 200 | Success | **application/json**: [AllowedExtensionsResponse](#allowedextensionsresponse)
| -### /files/upload - -#### GET -##### Responses +### [GET] /files/upload +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [UploadConfig](#uploadconfig) | +| 200 | Success | **application/json**: [UploadConfig](#uploadconfig)
| -#### POST -##### Responses +### [POST] /files/upload +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | File uploaded successfully | [FileResponse](#fileresponse) | +| 201 | File uploaded successfully | **application/json**: [FileResponse](#fileresponse)
| -### /files/{file_id}/preview - -#### GET -##### Parameters +### [GET] /files/{file_id}/preview +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | file_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [TextContentResponse](#textcontentresponse) | - -### /forgot-password - -#### POST -##### Description +| 200 | Success | **application/json**: [TextContentResponse](#textcontentresponse)
| +### [POST] /forgot-password Send password reset email -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ForgotPasswordSendPayload](#forgotpasswordsendpayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ForgotPasswordSendPayload](#forgotpasswordsendpayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Email sent successfully | [ForgotPasswordEmailResponse](#forgotpasswordemailresponse) | +| 200 | Email sent successfully | **application/json**: [ForgotPasswordEmailResponse](#forgotpasswordemailresponse)
| | 400 | Invalid email or rate limit exceeded | | -### /forgot-password/resets - -#### POST -##### Description - +### [POST] /forgot-password/resets Reset password with verification token -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ForgotPasswordResetPayload](#forgotpasswordresetpayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ForgotPasswordResetPayload](#forgotpasswordresetpayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Password reset successfully | [ForgotPasswordResetResponse](#forgotpasswordresetresponse) | +| 200 | Password reset successfully | **application/json**: [ForgotPasswordResetResponse](#forgotpasswordresetresponse)
| | 400 | Invalid token or password mismatch | | -### /forgot-password/validity - -#### POST -##### Description - +### [POST] /forgot-password/validity Verify password reset code -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ForgotPasswordCheckPayload](#forgotpasswordcheckpayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ForgotPasswordCheckPayload](#forgotpasswordcheckpayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Code verified successfully | [ForgotPasswordCheckResponse](#forgotpasswordcheckresponse) | +| 200 | Code verified successfully | **application/json**: [ForgotPasswordCheckResponse](#forgotpasswordcheckresponse)
| | 400 | Invalid code or token | | -### /form/human_input/{form_token} - -#### GET -##### Summary - -Get human input form definition by form token - -##### Description +### [GET] /form/human_input/{form_token} +**Get human input form definition by form token** GET /console/api/form/human_input/ -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | form_token | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -#### POST -##### Summary - -Submit human input form by form token - -##### Description +### [POST] /form/human_input/{form_token} +**Submit human input form by form token** POST /console/api/form/human_input/ @@ -6286,452 +5613,437 @@ Request body: "action": "Approve" } -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | form_token | path | | Yes | string | -| payload | body | | Yes | [HumanInputFormSubmitPayload](#humaninputformsubmitpayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [HumanInputFormSubmitPayload](#humaninputformsubmitpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /info - -#### POST -##### Responses +### [POST] /info +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [TenantInfoResponse](#tenantinforesponse) | +| 200 | Success | **application/json**: [TenantInfoResponse](#tenantinforesponse)
| -### /installed-apps - -#### GET -##### Responses +### [GET] /installed-apps +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [InstalledAppListResponse](#installedapplistresponse) | +| 200 | Success | **application/json**: [InstalledAppListResponse](#installedapplistresponse)
| -#### POST -##### Responses +### [POST] /installed-apps +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleMessageResponse](#simplemessageresponse) | +| 200 | Success | **application/json**: [SimpleMessageResponse](#simplemessageresponse)
| -### /installed-apps/{installed_app_id} - -#### DELETE -##### Parameters +### [DELETE] /installed-apps/{installed_app_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | installed_app_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | App uninstalled successfully | -#### PATCH -##### Parameters +### [PATCH] /installed-apps/{installed_app_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | installed_app_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultMessageResponse](#simpleresultmessageresponse) | +| 200 | Success | **application/json**: [SimpleResultMessageResponse](#simpleresultmessageresponse)
| -### /installed-apps/{installed_app_id}/audio-to-text - -#### POST -##### Parameters +### [POST] /installed-apps/{installed_app_id}/audio-to-text +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | installed_app_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /installed-apps/{installed_app_id}/chat-messages - -#### POST -##### Parameters +### [POST] /installed-apps/{installed_app_id}/chat-messages +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | installed_app_id | path | | Yes | string | -| payload | body | | Yes | [ChatMessagePayload](#chatmessagepayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ChatMessagePayload](#chatmessagepayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /installed-apps/{installed_app_id}/chat-messages/{task_id}/stop - -#### POST -##### Parameters +### [POST] /installed-apps/{installed_app_id}/chat-messages/{task_id}/stop +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | installed_app_id | path | | Yes | string | | task_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -### /installed-apps/{installed_app_id}/completion-messages - -#### POST -##### Parameters +### [POST] /installed-apps/{installed_app_id}/completion-messages +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | installed_app_id | path | | Yes | string | -| payload | body | | Yes | [CompletionMessageExplorePayload](#completionmessageexplorepayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [CompletionMessageExplorePayload](#completionmessageexplorepayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /installed-apps/{installed_app_id}/completion-messages/{task_id}/stop - -#### POST -##### Parameters +### [POST] /installed-apps/{installed_app_id}/completion-messages/{task_id}/stop +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | installed_app_id | path | | Yes | string | | task_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -### /installed-apps/{installed_app_id}/conversations - -#### GET -##### Parameters +### [GET] /installed-apps/{installed_app_id}/conversations +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | +| last_id | query | | No | | +| limit | query | | No | integer,
**Default:** 20 | +| pinned | query | | No | | | installed_app_id | path | | Yes | string | -| payload | body | | Yes | [ConversationListQuery](#conversationlistquery) | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /installed-apps/{installed_app_id}/conversations/{c_id} - -#### DELETE -##### Parameters +### [DELETE] /installed-apps/{installed_app_id}/conversations/{c_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | c_id | path | | Yes | string | | installed_app_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Conversation deleted successfully | -### /installed-apps/{installed_app_id}/conversations/{c_id}/name - -#### POST -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| c_id | path | | Yes | string | -| installed_app_id | path | | Yes | string | -| payload | body | | Yes | [ConversationRenamePayload](#conversationrenamepayload) | - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 200 | Success | - -### /installed-apps/{installed_app_id}/conversations/{c_id}/pin - -#### PATCH -##### Parameters +### [POST] /installed-apps/{installed_app_id}/conversations/{c_id}/name +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | c_id | path | | Yes | string | | installed_app_id | path | | Yes | string | -##### Responses +#### Request Body -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Success | [ResultResponse](#resultresponse) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ConversationRenamePayload](#conversationrenamepayload)
| -### /installed-apps/{installed_app_id}/conversations/{c_id}/unpin +#### Responses -#### PATCH -##### Parameters +| Code | Description | +| ---- | ----------- | +| 200 | Success | + +### [PATCH] /installed-apps/{installed_app_id}/conversations/{c_id}/pin +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | c_id | path | | Yes | string | | installed_app_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [ResultResponse](#resultresponse) | +| 200 | Success | **application/json**: [ResultResponse](#resultresponse)
| -### /installed-apps/{installed_app_id}/messages - -#### GET -##### Parameters +### [PATCH] /installed-apps/{installed_app_id}/conversations/{c_id}/unpin +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | +| c_id | path | | Yes | string | | installed_app_id | path | | Yes | string | -| payload | body | | Yes | [MessageListQuery](#messagelistquery) | -##### Responses +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [ResultResponse](#resultresponse)
| + +### [GET] /installed-apps/{installed_app_id}/messages +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| conversation_id | query | Conversation UUID | Yes | string | +| first_id | query | First message ID for pagination | No | | +| limit | query | Number of messages to return (1-100) | No | integer,
**Default:** 20 | +| installed_app_id | path | | Yes | string | + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /installed-apps/{installed_app_id}/messages/{message_id}/feedbacks - -#### POST -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| installed_app_id | path | | Yes | string | -| message_id | path | | Yes | string | -| payload | body | | Yes | [MessageFeedbackPayload](#messagefeedbackpayload) | - -##### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Feedback submitted successfully | [ResultResponse](#resultresponse) | - -### /installed-apps/{installed_app_id}/messages/{message_id}/more-like-this - -#### GET -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| installed_app_id | path | | Yes | string | -| message_id | path | | Yes | string | -| payload | body | | Yes | [MoreLikeThisQuery](#morelikethisquery) | - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 200 | Success | - -### /installed-apps/{installed_app_id}/messages/{message_id}/suggested-questions - -#### GET -##### Parameters +### [POST] /installed-apps/{installed_app_id}/messages/{message_id}/feedbacks +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | installed_app_id | path | | Yes | string | | message_id | path | | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [MessageFeedbackPayload](#messagefeedbackpayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SuggestedQuestionsResponse](#suggestedquestionsresponse) | +| 200 | Feedback submitted successfully | **application/json**: [ResultResponse](#resultresponse)
| -### /installed-apps/{installed_app_id}/meta - -#### GET -##### Summary - -Get app meta - -##### Parameters +### [GET] /installed-apps/{installed_app_id}/messages/{message_id}/more-like-this +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | +| response_mode | query | | Yes | string,
**Available values:** "blocking", "streaming" | | installed_app_id | path | | Yes | string | +| message_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /installed-apps/{installed_app_id}/parameters - -#### GET -##### Summary - -Retrieve app parameters - -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| installed_app_id | path | | Yes | string | - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 200 | Success | - -### /installed-apps/{installed_app_id}/saved-messages - -#### GET -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| installed_app_id | path | | Yes | string | -| payload | body | | Yes | [SavedMessageListQuery](#savedmessagelistquery) | - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 200 | Success | - -#### POST -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| installed_app_id | path | | Yes | string | -| payload | body | | Yes | [SavedMessageCreatePayload](#savedmessagecreatepayload) | - -##### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Success | [ResultResponse](#resultresponse) | - -### /installed-apps/{installed_app_id}/saved-messages/{message_id} - -#### DELETE -##### Parameters +### [GET] /installed-apps/{installed_app_id}/messages/{message_id}/suggested-questions +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | installed_app_id | path | | Yes | string | | message_id | path | | Yes | string | -##### Responses +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [SuggestedQuestionsResponse](#suggestedquestionsresponse)
| + +### [GET] /installed-apps/{installed_app_id}/meta +**Get app meta** + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| installed_app_id | path | | Yes | string | + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Success | + +### [GET] /installed-apps/{installed_app_id}/parameters +**Retrieve app parameters** + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| installed_app_id | path | | Yes | string | + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Success | + +### [GET] /installed-apps/{installed_app_id}/saved-messages +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| last_id | query | | No | | +| limit | query | | No | integer,
**Default:** 20 | +| installed_app_id | path | | Yes | string | + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Success | + +### [POST] /installed-apps/{installed_app_id}/saved-messages +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| installed_app_id | path | | Yes | string | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [SavedMessageCreatePayload](#savedmessagecreatepayload)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Success | **application/json**: [ResultResponse](#resultresponse)
| + +### [DELETE] /installed-apps/{installed_app_id}/saved-messages/{message_id} +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| installed_app_id | path | | Yes | string | +| message_id | path | | Yes | string | + +#### Responses | Code | Description | | ---- | ----------- | | 204 | Saved message deleted successfully | -### /installed-apps/{installed_app_id}/text-to-audio - -#### POST -##### Parameters +### [POST] /installed-apps/{installed_app_id}/text-to-audio +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | installed_app_id | path | | Yes | string | -| payload | body | | Yes | [TextToAudioPayload](#texttoaudiopayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TextToAudioPayload](#texttoaudiopayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /installed-apps/{installed_app_id}/workflows/run +### [POST] /installed-apps/{installed_app_id}/workflows/run +**Run workflow** -#### POST -##### Summary - -Run workflow - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | installed_app_id | path | | Yes | string | -| payload | body | | Yes | [WorkflowRunPayload](#workflowrunpayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [WorkflowRunPayload](#workflowrunpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /installed-apps/{installed_app_id}/workflows/tasks/{task_id}/stop +### [POST] /installed-apps/{installed_app_id}/workflows/tasks/{task_id}/stop +**Stop workflow task** -#### POST -##### Summary - -Stop workflow task - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | installed_app_id | path | | Yes | string | | task_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | - -### /instruction-generate - -#### POST -##### Description +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| +### [POST] /instruction-generate Generate instruction for workflow nodes or general use -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [InstructionGeneratePayload](#instructiongeneratepayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [InstructionGeneratePayload](#instructiongeneratepayload)
| -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -6739,132 +6051,104 @@ Generate instruction for workflow nodes or general use | 400 | Invalid request parameters or flow/workflow not found | | 402 | Provider quota exceeded | -### /instruction-generate/template - -#### POST -##### Description - +### [POST] /instruction-generate/template Get instruction generation template -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [InstructionTemplatePayload](#instructiontemplatepayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [InstructionTemplatePayload](#instructiontemplatepayload)
| -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Template retrieved successfully | | 400 | Invalid request parameters | -### /login +### [POST] /login +**Authenticate user and login** -#### POST -##### Summary +#### Request Body -Authenticate user and login +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [LoginPayload](#loginpayload)
| -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [LoginPayload](#loginpayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultOptionalDataResponse](#simpleresultoptionaldataresponse) | +| 200 | Success | **application/json**: [SimpleResultOptionalDataResponse](#simpleresultoptionaldataresponse)
| -### /logout - -#### POST -##### Responses +### [POST] /logout +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -### /mcp/oauth/callback - -#### GET -##### Responses +### [GET] /mcp/oauth/callback +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /notification - -#### GET -##### Description - +### [GET] /notification Return the active in-product notification for the current user in their interface language (falls back to English if unavailable). The notification is NOT marked as seen here; call POST /notification/dismiss when the user explicitly closes the modal. -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success — inspect should_show to decide whether to render the modal | | 401 | Unauthorized | -### /notification/dismiss - -#### POST -##### Description - +### [POST] /notification/dismiss Mark a notification as dismissed for the current user. -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| | 401 | Unauthorized | | -### /notion/pages/{page_id}/{page_type}/preview - -#### GET -##### Parameters +### [GET] /notion/pages/{page_id}/{page_type}/preview +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | +| credential_id | query | Credential ID | Yes | string | | page_id | path | | Yes | string | | page_type | path | | Yes | string | -| credential_id | query | Credential ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [TextContentResponse](#textcontentresponse) | +| 200 | Success | **application/json**: [TextContentResponse](#textcontentresponse)
| -### /notion/pre-import/pages - -#### GET -##### Parameters +### [GET] /notion/pre-import/pages +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | credential_id | query | Credential ID | Yes | string | | dataset_id | query | Dataset ID | No | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [NotionIntegrateInfoListResponse](#notionintegrateinfolistresponse) | - -### /oauth/authorize/{provider} - -#### GET -##### Description +| 200 | Success | **application/json**: [NotionIntegrateInfoListResponse](#notionintegrateinfolistresponse)
| +### [GET] /oauth/authorize/{provider} Handle OAuth callback and complete login process -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -6872,42 +6156,34 @@ Handle OAuth callback and complete login process | code | query | Authorization code from OAuth provider | No | string | | state | query | Optional state parameter (used for invite token) | No | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 302 | Redirect to console with access token | | 400 | OAuth process failed | -### /oauth/data-source/binding/{provider} - -#### GET -##### Description - +### [GET] /oauth/data-source/binding/{provider} Bind OAuth data source with authorization code -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | Data source provider name (notion) | Yes | string | | code | query | Authorization code from OAuth provider | No | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Data source binding success | [OAuthDataSourceBindingResponse](#oauthdatasourcebindingresponse) | +| 200 | Data source binding success | **application/json**: [OAuthDataSourceBindingResponse](#oauthdatasourcebindingresponse)
| | 400 | Invalid provider or code | | -### /oauth/data-source/callback/{provider} - -#### GET -##### Description - +### [GET] /oauth/data-source/callback/{provider} Handle OAuth callback from data source provider -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -6915,1173 +6191,1045 @@ Handle OAuth callback from data source provider | code | query | Authorization code from OAuth provider | No | string | | error | query | Error message from OAuth provider | No | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 302 | Redirect to console with result | | 400 | Invalid provider | -### /oauth/data-source/{provider} - -#### GET -##### Description - +### [GET] /oauth/data-source/{provider} Get OAuth authorization URL for data source provider -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | Data source provider name (notion) | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Authorization URL or internal setup success | [OAuthDataSourceResponse](#oauthdatasourceresponse) | +| 200 | Authorization URL or internal setup success | **application/json**: [OAuthDataSourceResponse](#oauthdatasourceresponse)
| | 400 | Invalid provider | | | 403 | Admin privileges required | | -### /oauth/data-source/{provider}/{binding_id}/sync - -#### GET -##### Description - +### [GET] /oauth/data-source/{provider}/{binding_id}/sync Sync data from OAuth data source -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | binding_id | path | Data source binding ID | Yes | string | | provider | path | Data source provider name (notion) | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Data source sync success | [OAuthDataSourceSyncResponse](#oauthdatasourcesyncresponse) | +| 200 | Data source sync success | **application/json**: [OAuthDataSourceSyncResponse](#oauthdatasourcesyncresponse)
| | 400 | Invalid provider or sync failed | | -### /oauth/login/{provider} - -#### GET -##### Description - +### [GET] /oauth/login/{provider} Initiate OAuth login process -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | OAuth provider name (github/google) | Yes | string | | invite_token | query | Optional invitation token | No | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 302 | Redirect to OAuth authorization URL | | 400 | Invalid provider | -### /oauth/plugin/{provider_id}/datasource/callback - -#### GET -##### Parameters +### [GET] /oauth/plugin/{provider_id}/datasource/callback +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /oauth/plugin/{provider_id}/datasource/get-authorization-url - -#### GET -##### Parameters +### [GET] /oauth/plugin/{provider_id}/datasource/get-authorization-url +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /oauth/plugin/{provider}/tool/authorization-url - -#### GET -##### Parameters +### [GET] /oauth/plugin/{provider}/tool/authorization-url +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /oauth/plugin/{provider}/tool/callback - -#### GET -##### Parameters +### [GET] /oauth/plugin/{provider}/tool/callback +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /oauth/plugin/{provider}/trigger/callback +### [GET] /oauth/plugin/{provider}/trigger/callback +**Handle OAuth callback for trigger provider** -#### GET -##### Summary - -Handle OAuth callback for trigger provider - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /oauth/provider - -#### POST -##### Responses +### [POST] /oauth/provider +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /oauth/provider/account - -#### POST -##### Responses +### [POST] /oauth/provider/account +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /oauth/provider/authorize - -#### POST -##### Responses +### [POST] /oauth/provider/authorize +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /oauth/provider/token - -#### POST -##### Responses +### [POST] /oauth/provider/token +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /rag/pipeline/customized/templates/{template_id} - -#### DELETE -##### Parameters +### [DELETE] /rag/pipeline/customized/templates/{template_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | template_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Pipeline template deleted | -#### PATCH -##### Parameters +### [PATCH] /rag/pipeline/customized/templates/{template_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | template_id | path | | Yes | string | -| payload | body | | Yes | [CustomizedPipelineTemplatePayload](#customizedpipelinetemplatepayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [CustomizedPipelineTemplatePayload](#customizedpipelinetemplatepayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 204 | Pipeline template updated | -#### POST -##### Parameters +### [POST] /rag/pipeline/customized/templates/{template_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | template_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleDataResponse](#simpledataresponse) | +| 200 | Success | **application/json**: [SimpleDataResponse](#simpledataresponse)
| -### /rag/pipeline/dataset +### [POST] /rag/pipeline/dataset +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [RagPipelineDatasetImportPayload](#ragpipelinedatasetimportpayload)
| + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | RAG pipeline dataset import started | **application/json**: [RagPipelineImportResponse](#ragpipelineimportresponse)
| + +### [POST] /rag/pipeline/empty-dataset +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | RAG pipeline dataset created | **application/json**: [DatasetDetailResponse](#datasetdetailresponse)
| + +### [GET] /rag/pipeline/templates +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [RagPipelineDatasetImportPayload](#ragpipelinedatasetimportpayload) | +| language | query | Template language | No | string,
**Default:** en-US | +| type | query | Template source: built-in or customized | No | string,
**Default:** built-in | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | RAG pipeline dataset import started | [RagPipelineImportResponse](#ragpipelineimportresponse) | +| 200 | Pipeline templates | **application/json**: [PipelineTemplateListResponse](#pipelinetemplatelistresponse)
| -### /rag/pipeline/empty-dataset - -#### POST -##### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 201 | RAG pipeline dataset created | [DatasetDetailResponse](#datasetdetailresponse) | - -### /rag/pipeline/templates - -#### GET -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| language | query | Template language | No | string | -| type | query | Template source: built-in or customized | No | string | - -##### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Pipeline templates | [PipelineTemplateListResponse](#pipelinetemplatelistresponse) | - -### /rag/pipeline/templates/{template_id} - -#### GET -##### Parameters +### [GET] /rag/pipeline/templates/{template_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | +| type | query | Template source: built-in or customized | No | string,
**Default:** built-in | | template_id | path | | Yes | string | -| type | query | Template source: built-in or customized | No | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Pipeline template | [PipelineTemplateDetailResponse](#pipelinetemplatedetailresponse) | +| 200 | Pipeline template | **application/json**: [PipelineTemplateDetailResponse](#pipelinetemplatedetailresponse)
| -### /rag/pipelines/datasource-plugins - -#### GET -##### Responses +### [GET] /rag/pipelines/datasource-plugins +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /rag/pipelines/imports +### [POST] /rag/pipelines/imports +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [RagPipelineImportPayload](#ragpipelineimportpayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [RagPipelineImportPayload](#ragpipelineimportpayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Import completed | [RagPipelineImportResponse](#ragpipelineimportresponse) | -| 202 | Import pending confirmation | [RagPipelineImportResponse](#ragpipelineimportresponse) | -| 400 | Import failed | [RagPipelineImportResponse](#ragpipelineimportresponse) | +| 200 | Import completed | **application/json**: [RagPipelineImportResponse](#ragpipelineimportresponse)
| +| 202 | Import pending confirmation | **application/json**: [RagPipelineImportResponse](#ragpipelineimportresponse)
| +| 400 | Import failed | **application/json**: [RagPipelineImportResponse](#ragpipelineimportresponse)
| -### /rag/pipelines/imports/{import_id}/confirm - -#### POST -##### Parameters +### [POST] /rag/pipelines/imports/{import_id}/confirm +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | import_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Import confirmed | [RagPipelineImportResponse](#ragpipelineimportresponse) | -| 400 | Import failed | [RagPipelineImportResponse](#ragpipelineimportresponse) | +| 200 | Import confirmed | **application/json**: [RagPipelineImportResponse](#ragpipelineimportresponse)
| +| 400 | Import failed | **application/json**: [RagPipelineImportResponse](#ragpipelineimportresponse)
| -### /rag/pipelines/imports/{pipeline_id}/check-dependencies - -#### GET -##### Parameters +### [GET] /rag/pipelines/imports/{pipeline_id}/check-dependencies +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Dependencies checked | [RagPipelineImportCheckDependenciesResponse](#ragpipelineimportcheckdependenciesresponse) | +| 200 | Dependencies checked | **application/json**: [RagPipelineImportCheckDependenciesResponse](#ragpipelineimportcheckdependenciesresponse)
| -### /rag/pipelines/recommended-plugins - -#### GET -##### Responses +### [GET] /rag/pipelines/recommended-plugins +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /rag/pipelines/transform/datasets/{dataset_id} - -#### POST -##### Parameters +### [POST] /rag/pipelines/transform/datasets/{dataset_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /rag/pipelines/{pipeline_id}/customized/publish - -#### POST -##### Parameters +### [POST] /rag/pipelines/{pipeline_id}/customized/publish +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | -| payload | body | | Yes | [CustomizedPipelineTemplatePayload](#customizedpipelinetemplatepayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [CustomizedPipelineTemplatePayload](#customizedpipelinetemplatepayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 204 | Pipeline template published | -### /rag/pipelines/{pipeline_id}/exports - -#### GET -##### Parameters +### [GET] /rag/pipelines/{pipeline_id}/exports +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | +| include_secret | query | Whether to include secret values in the exported DSL | No | string,
**Default:** false | | pipeline_id | path | | Yes | string | -| include_secret | query | Whether to include secret values in the exported DSL | No | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Pipeline exported | [SimpleDataResponse](#simpledataresponse) | +| 200 | Pipeline exported | **application/json**: [SimpleDataResponse](#simpledataresponse)
| -### /rag/pipelines/{pipeline_id}/workflow-runs +### [GET] /rag/pipelines/{pipeline_id}/workflow-runs +**Get workflow run list** -#### GET -##### Summary - -Get workflow run list - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow runs retrieved successfully | [WorkflowRunPaginationResponse](#workflowrunpaginationresponse) | +| 200 | Workflow runs retrieved successfully | **application/json**: [WorkflowRunPaginationResponse](#workflowrunpaginationresponse)
| -### /rag/pipelines/{pipeline_id}/workflow-runs/tasks/{task_id}/stop +### [POST] /rag/pipelines/{pipeline_id}/workflow-runs/tasks/{task_id}/stop +**Stop workflow task** -#### POST -##### Summary - -Stop workflow task - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | | task_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Task stopped successfully | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Task stopped successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -### /rag/pipelines/{pipeline_id}/workflow-runs/{run_id} +### [GET] /rag/pipelines/{pipeline_id}/workflow-runs/{run_id} +**Get workflow run detail** -#### GET -##### Summary - -Get workflow run detail - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | | run_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow run detail retrieved successfully | [WorkflowRunDetailResponse](#workflowrundetailresponse) | +| 200 | Workflow run detail retrieved successfully | **application/json**: [WorkflowRunDetailResponse](#workflowrundetailresponse)
| -### /rag/pipelines/{pipeline_id}/workflow-runs/{run_id}/node-executions +### [GET] /rag/pipelines/{pipeline_id}/workflow-runs/{run_id}/node-executions +**Get workflow run node execution list** -#### GET -##### Summary - -Get workflow run node execution list - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | | run_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Node executions retrieved successfully | [WorkflowRunNodeExecutionListResponse](#workflowrunnodeexecutionlistresponse) | +| 200 | Node executions retrieved successfully | **application/json**: [WorkflowRunNodeExecutionListResponse](#workflowrunnodeexecutionlistresponse)
| -### /rag/pipelines/{pipeline_id}/workflows +### [GET] /rag/pipelines/{pipeline_id}/workflows +**Get published workflows** -#### GET -##### Summary - -Get published workflows - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Published workflows retrieved successfully | [WorkflowPaginationResponse](#workflowpaginationresponse) | +| 200 | Published workflows retrieved successfully | **application/json**: [WorkflowPaginationResponse](#workflowpaginationresponse)
| | 403 | Permission denied | | -### /rag/pipelines/{pipeline_id}/workflows/default-workflow-block-configs +### [GET] /rag/pipelines/{pipeline_id}/workflows/default-workflow-block-configs +**Get default block config** -#### GET -##### Summary - -Get default block config - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /rag/pipelines/{pipeline_id}/workflows/default-workflow-block-configs/{block_type} +### [GET] /rag/pipelines/{pipeline_id}/workflows/default-workflow-block-configs/{block_type} +**Get default block config** -#### GET -##### Summary - -Get default block config - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | block_type | path | | Yes | string | | pipeline_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /rag/pipelines/{pipeline_id}/workflows/draft +### [GET] /rag/pipelines/{pipeline_id}/workflows/draft +**Get draft rag pipeline's workflow** -#### GET -##### Summary - -Get draft rag pipeline's workflow - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Draft workflow retrieved successfully | [WorkflowResponse](#workflowresponse) | +| 200 | Draft workflow retrieved successfully | **application/json**: [WorkflowResponse](#workflowresponse)
| | 404 | Draft workflow not found | | -#### POST -##### Summary +### [POST] /rag/pipelines/{pipeline_id}/workflows/draft +**Sync draft workflow** -Sync draft workflow - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [RagPipelineWorkflowSyncResponse](#ragpipelineworkflowsyncresponse) | +| 200 | Success | **application/json**: [RagPipelineWorkflowSyncResponse](#ragpipelineworkflowsyncresponse)
| -### /rag/pipelines/{pipeline_id}/workflows/draft/datasource/nodes/{node_id}/run +### [POST] /rag/pipelines/{pipeline_id}/workflows/draft/datasource/nodes/{node_id}/run +**Run rag pipeline datasource** -#### POST -##### Summary - -Run rag pipeline datasource - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | path | | Yes | string | | pipeline_id | path | | Yes | string | -| payload | body | | Yes | [DatasourceNodeRunPayload](#datasourcenoderunpayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DatasourceNodeRunPayload](#datasourcenoderunpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /rag/pipelines/{pipeline_id}/workflows/draft/datasource/variables-inspect +### [POST] /rag/pipelines/{pipeline_id}/workflows/draft/datasource/variables-inspect +**Set datasource variables** -#### POST -##### Summary - -Set datasource variables - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | -| payload | body | | Yes | [DatasourceVariablesPayload](#datasourcevariablespayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DatasourceVariablesPayload](#datasourcevariablespayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Datasource variables set successfully | [WorkflowRunNodeExecutionResponse](#workflowrunnodeexecutionresponse) | +| 200 | Datasource variables set successfully | **application/json**: [WorkflowRunNodeExecutionResponse](#workflowrunnodeexecutionresponse)
| -### /rag/pipelines/{pipeline_id}/workflows/draft/environment-variables +### [GET] /rag/pipelines/{pipeline_id}/workflows/draft/environment-variables +**Get draft workflow** -#### GET -##### Summary - -Get draft workflow - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /rag/pipelines/{pipeline_id}/workflows/draft/iteration/nodes/{node_id}/run +### [POST] /rag/pipelines/{pipeline_id}/workflows/draft/iteration/nodes/{node_id}/run +**Run draft workflow iteration node** -#### POST -##### Summary - -Run draft workflow iteration node - -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| node_id | path | | Yes | string | -| pipeline_id | path | | Yes | string | -| payload | body | | Yes | [NodeRunPayload](#noderunpayload) | - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 200 | Success | - -### /rag/pipelines/{pipeline_id}/workflows/draft/loop/nodes/{node_id}/run - -#### POST -##### Summary - -Run draft workflow loop node - -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| node_id | path | | Yes | string | -| pipeline_id | path | | Yes | string | -| payload | body | | Yes | [NodeRunPayload](#noderunpayload) | - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 200 | Success | - -### /rag/pipelines/{pipeline_id}/workflows/draft/nodes/{node_id}/last-run - -#### GET -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | path | | Yes | string | | pipeline_id | path | | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [NodeRunPayload](#noderunpayload)
| + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Success | + +### [POST] /rag/pipelines/{pipeline_id}/workflows/draft/loop/nodes/{node_id}/run +**Run draft workflow loop node** + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| node_id | path | | Yes | string | +| pipeline_id | path | | Yes | string | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [NodeRunPayload](#noderunpayload)
| + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Success | + +### [GET] /rag/pipelines/{pipeline_id}/workflows/draft/nodes/{node_id}/last-run +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| node_id | path | | Yes | string | +| pipeline_id | path | | Yes | string | + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Node last run retrieved successfully | [WorkflowRunNodeExecutionResponse](#workflowrunnodeexecutionresponse) | +| 200 | Node last run retrieved successfully | **application/json**: [WorkflowRunNodeExecutionResponse](#workflowrunnodeexecutionresponse)
| -### /rag/pipelines/{pipeline_id}/workflows/draft/nodes/{node_id}/run +### [POST] /rag/pipelines/{pipeline_id}/workflows/draft/nodes/{node_id}/run +**Run draft workflow node** -#### POST -##### Summary - -Run draft workflow node - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | path | | Yes | string | | pipeline_id | path | | Yes | string | -| payload | body | | Yes | [NodeRunRequiredPayload](#noderunrequiredpayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [NodeRunRequiredPayload](#noderunrequiredpayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Node run started successfully | [WorkflowRunNodeExecutionResponse](#workflowrunnodeexecutionresponse) | +| 200 | Node run started successfully | **application/json**: [WorkflowRunNodeExecutionResponse](#workflowrunnodeexecutionresponse)
| -### /rag/pipelines/{pipeline_id}/workflows/draft/nodes/{node_id}/variables - -#### DELETE -##### Parameters +### [DELETE] /rag/pipelines/{pipeline_id}/workflows/draft/nodes/{node_id}/variables +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | path | | Yes | string | | pipeline_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -#### GET -##### Parameters +### [GET] /rag/pipelines/{pipeline_id}/workflows/draft/nodes/{node_id}/variables +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | path | | Yes | string | | pipeline_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /rag/pipelines/{pipeline_id}/workflows/draft/pre-processing/parameters +### [GET] /rag/pipelines/{pipeline_id}/workflows/draft/pre-processing/parameters +**Get first step parameters of rag pipeline** -#### GET -##### Summary - -Get first step parameters of rag pipeline - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /rag/pipelines/{pipeline_id}/workflows/draft/processing/parameters +### [GET] /rag/pipelines/{pipeline_id}/workflows/draft/processing/parameters +**Get second step parameters of rag pipeline** -#### GET -##### Summary - -Get second step parameters of rag pipeline - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /rag/pipelines/{pipeline_id}/workflows/draft/run +### [POST] /rag/pipelines/{pipeline_id}/workflows/draft/run +**Run draft workflow** -#### POST -##### Summary - -Run draft workflow - -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| pipeline_id | path | | Yes | string | -| payload | body | | Yes | [DraftWorkflowRunPayload](#draftworkflowrunpayload) | - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 200 | Success | - -### /rag/pipelines/{pipeline_id}/workflows/draft/system-variables - -#### GET -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DraftWorkflowRunPayload](#draftworkflowrunpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /rag/pipelines/{pipeline_id}/workflows/draft/variables - -#### DELETE -##### Parameters +### [GET] /rag/pipelines/{pipeline_id}/workflows/draft/system-variables +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -#### GET -##### Summary - -Get draft workflow - -##### Parameters +### [DELETE] /rag/pipelines/{pipeline_id}/workflows/draft/variables +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /rag/pipelines/{pipeline_id}/workflows/draft/variables/{variable_id} +### [GET] /rag/pipelines/{pipeline_id}/workflows/draft/variables +**Get draft workflow** -#### DELETE -##### Parameters +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| pipeline_id | path | | Yes | string | + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Success | + +### [DELETE] /rag/pipelines/{pipeline_id}/workflows/draft/variables/{variable_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | | variable_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -#### GET -##### Parameters +### [GET] /rag/pipelines/{pipeline_id}/workflows/draft/variables/{variable_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | | variable_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -#### PATCH -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| pipeline_id | path | | Yes | string | -| variable_id | path | | Yes | string | -| payload | body | | Yes | [WorkflowDraftVariablePatchPayload](#workflowdraftvariablepatchpayload) | - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 200 | Success | - -### /rag/pipelines/{pipeline_id}/workflows/draft/variables/{variable_id}/reset - -#### PUT -##### Parameters +### [PATCH] /rag/pipelines/{pipeline_id}/workflows/draft/variables/{variable_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | | variable_id | path | | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [WorkflowDraftVariablePatchPayload](#workflowdraftvariablepatchpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /rag/pipelines/{pipeline_id}/workflows/publish +### [PUT] /rag/pipelines/{pipeline_id}/workflows/draft/variables/{variable_id}/reset +#### Parameters -#### GET -##### Summary +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| pipeline_id | path | | Yes | string | +| variable_id | path | | Yes | string | -Get published pipeline +#### Responses -##### Parameters +| Code | Description | +| ---- | ----------- | +| 200 | Success | + +### [GET] /rag/pipelines/{pipeline_id}/workflows/publish +**Get published pipeline** + +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Published workflow retrieved successfully, or null if not exist | [WorkflowResponse](#workflowresponse) | +| 200 | Published workflow retrieved successfully, or null if not exist | **application/json**: [WorkflowResponse](#workflowresponse)
| -#### POST -##### Summary +### [POST] /rag/pipelines/{pipeline_id}/workflows/publish +**Publish workflow** -Publish workflow - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [RagPipelineWorkflowPublishResponse](#ragpipelineworkflowpublishresponse) | +| 200 | Success | **application/json**: [RagPipelineWorkflowPublishResponse](#ragpipelineworkflowpublishresponse)
| -### /rag/pipelines/{pipeline_id}/workflows/published/datasource/nodes/{node_id}/preview +### [POST] /rag/pipelines/{pipeline_id}/workflows/published/datasource/nodes/{node_id}/preview +**Run datasource content preview** -#### POST -##### Summary - -Run datasource content preview - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | path | | Yes | string | | pipeline_id | path | | Yes | string | -| payload | body | | Yes | [Parser](#parser) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [Parser](#parser)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /rag/pipelines/{pipeline_id}/workflows/published/datasource/nodes/{node_id}/run +### [POST] /rag/pipelines/{pipeline_id}/workflows/published/datasource/nodes/{node_id}/run +**Run rag pipeline datasource** -#### POST -##### Summary - -Run rag pipeline datasource - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | path | | Yes | string | | pipeline_id | path | | Yes | string | -| payload | body | | Yes | [DatasourceNodeRunPayload](#datasourcenoderunpayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DatasourceNodeRunPayload](#datasourcenoderunpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /rag/pipelines/{pipeline_id}/workflows/published/pre-processing/parameters +### [GET] /rag/pipelines/{pipeline_id}/workflows/published/pre-processing/parameters +**Get first step parameters of rag pipeline** -#### GET -##### Summary - -Get first step parameters of rag pipeline - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /rag/pipelines/{pipeline_id}/workflows/published/processing/parameters +### [GET] /rag/pipelines/{pipeline_id}/workflows/published/processing/parameters +**Get second step parameters of rag pipeline** -#### GET -##### Summary - -Get second step parameters of rag pipeline - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /rag/pipelines/{pipeline_id}/workflows/published/run +### [POST] /rag/pipelines/{pipeline_id}/workflows/published/run +**Run published workflow** -#### POST -##### Summary - -Run published workflow - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | -| payload | body | | Yes | [PublishedWorkflowRunPayload](#publishedworkflowrunpayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [PublishedWorkflowRunPayload](#publishedworkflowrunpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /rag/pipelines/{pipeline_id}/workflows/{workflow_id} +### [DELETE] /rag/pipelines/{pipeline_id}/workflows/{workflow_id} +**Delete a published workflow version that is not currently active on the pipeline** -#### DELETE -##### Summary - -Delete a published workflow version that is not currently active on the pipeline - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | | workflow_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Workflow deleted successfully | -#### PATCH -##### Summary +### [PATCH] /rag/pipelines/{pipeline_id}/workflows/{workflow_id} +**Update workflow attributes** -Update workflow attributes - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | | workflow_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow updated successfully | [WorkflowResponse](#workflowresponse) | +| 200 | Workflow updated successfully | **application/json**: [WorkflowResponse](#workflowresponse)
| | 400 | No valid fields to update | | | 403 | Permission denied | | | 404 | Workflow not found | | -### /rag/pipelines/{pipeline_id}/workflows/{workflow_id}/restore - -#### POST -##### Parameters +### [POST] /rag/pipelines/{pipeline_id}/workflows/{workflow_id}/restore +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | pipeline_id | path | | Yes | string | | workflow_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [RagPipelineWorkflowSyncResponse](#ragpipelineworkflowsyncresponse) | +| 200 | Success | **application/json**: [RagPipelineWorkflowSyncResponse](#ragpipelineworkflowsyncresponse)
| -### /refresh-token - -#### POST -##### Responses +### [POST] /refresh-token +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -### /remote-files/upload +### [POST] /remote-files/upload +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [RemoteFileUploadPayload](#remotefileuploadpayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [RemoteFileUploadPayload](#remotefileuploadpayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | File uploaded successfully | [FileWithSignedUrl](#filewithsignedurl) | +| 201 | File uploaded successfully | **application/json**: [FileWithSignedUrl](#filewithsignedurl)
| -### /remote-files/{url} - -#### GET -##### Parameters +### [GET] /remote-files/{url} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | url | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [RemoteFileInfo](#remotefileinfo) | +| 200 | Success | **application/json**: [RemoteFileInfo](#remotefileinfo)
| -### /reset-password +### [POST] /reset-password +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [EmailPayload](#emailpayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [EmailPayload](#emailpayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultDataResponse](#simpleresultdataresponse) | - -### /rule-code-generate - -#### POST -##### Description +| 200 | Success | **application/json**: [SimpleResultDataResponse](#simpleresultdataresponse)
| +### [POST] /rule-code-generate Generate code rules using LLM -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [RuleCodeGeneratePayload](#rulecodegeneratepayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [RuleCodeGeneratePayload](#rulecodegeneratepayload)
| -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -8089,20 +7237,16 @@ Generate code rules using LLM | 400 | Invalid request parameters | | 402 | Provider quota exceeded | -### /rule-generate - -#### POST -##### Description - +### [POST] /rule-generate Generate rule configuration using LLM -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [RuleGeneratePayload](#rulegeneratepayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [RuleGeneratePayload](#rulegeneratepayload)
| -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -8110,20 +7254,16 @@ Generate rule configuration using LLM | 400 | Invalid request parameters | | 402 | Provider quota exceeded | -### /rule-structured-output-generate - -#### POST -##### Description - +### [POST] /rule-structured-output-generate Generate structured output rules using LLM -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [RuleStructuredOutputPayload](#rulestructuredoutputpayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [RuleStructuredOutputPayload](#rulestructuredoutputpayload)
| -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -8131,603 +7271,522 @@ Generate structured output rules using LLM | 400 | Invalid request parameters | | 402 | Provider quota exceeded | -### /snippets/{snippet_id}/workflow-runs +### [GET] /snippets/{snippet_id}/workflow-runs +**List workflow runs for snippet** -#### GET -##### Summary - -List workflow runs for snippet - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | snippet_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow runs retrieved successfully | [WorkflowRunPaginationResponse](#workflowrunpaginationresponse) | +| 200 | Workflow runs retrieved successfully | **application/json**: [WorkflowRunPaginationResponse](#workflowrunpaginationresponse)
| -### /snippets/{snippet_id}/workflow-runs/tasks/{task_id}/stop - -#### POST -##### Summary - -Stop a running snippet workflow task - -##### Description +### [POST] /snippets/{snippet_id}/workflow-runs/tasks/{task_id}/stop +**Stop a running snippet workflow task** Uses both the legacy stop flag mechanism and the graph engine command channel for backward compatibility. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | snippet_id | path | | Yes | string | | task_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Task stopped successfully | | 404 | Snippet not found | -### /snippets/{snippet_id}/workflow-runs/{run_id} +### [GET] /snippets/{snippet_id}/workflow-runs/{run_id} +**Get workflow run detail for snippet** -#### GET -##### Summary - -Get workflow run detail for snippet - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | run_id | path | | Yes | string | | snippet_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow run detail retrieved successfully | [WorkflowRunDetailResponse](#workflowrundetailresponse) | +| 200 | Workflow run detail retrieved successfully | **application/json**: [WorkflowRunDetailResponse](#workflowrundetailresponse)
| | 404 | Workflow run not found | | -### /snippets/{snippet_id}/workflow-runs/{run_id}/node-executions +### [GET] /snippets/{snippet_id}/workflow-runs/{run_id}/node-executions +**List node executions for a workflow run** -#### GET -##### Summary - -List node executions for a workflow run - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | run_id | path | | Yes | string | | snippet_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Node executions retrieved successfully | [WorkflowRunNodeExecutionListResponse](#workflowrunnodeexecutionlistresponse) | +| 200 | Node executions retrieved successfully | **application/json**: [WorkflowRunNodeExecutionListResponse](#workflowrunnodeexecutionlistresponse)
| -### /snippets/{snippet_id}/workflows - -#### GET -##### Summary - -Get all published workflow versions for snippet - -##### Description +### [GET] /snippets/{snippet_id}/workflows +**Get all published workflow versions for snippet** Get all published workflows for a snippet -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [SnippetWorkflowListQuery](#snippetworkflowlistquery) | | snippet_id | path | Snippet ID | Yes | string | +| limit | query | | No | integer,
**Default:** 10 | +| page | query | | No | integer,
**Default:** 1 | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Published workflows retrieved successfully | [WorkflowPaginationResponse](#workflowpaginationresponse) | +| 200 | Published workflows retrieved successfully | **application/json**: [WorkflowPaginationResponse](#workflowpaginationresponse)
| -### /snippets/{snippet_id}/workflows/default-workflow-block-configs +### [GET] /snippets/{snippet_id}/workflows/default-workflow-block-configs +**Get default block configurations for snippet workflow** -#### GET -##### Summary - -Get default block configurations for snippet workflow - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | snippet_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Default block configs retrieved successfully | -### /snippets/{snippet_id}/workflows/draft +### [GET] /snippets/{snippet_id}/workflows/draft +**Get draft workflow for snippet** -#### GET -##### Summary - -Get draft workflow for snippet - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | snippet_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Draft workflow retrieved successfully | [SnippetWorkflowResponse](#snippetworkflowresponse) | +| 200 | Draft workflow retrieved successfully | **application/json**: [SnippetWorkflowResponse](#snippetworkflowresponse)
| | 404 | Snippet or draft workflow not found | | -#### POST -##### Summary +### [POST] /snippets/{snippet_id}/workflows/draft +**Sync draft workflow for snippet** -Sync draft workflow for snippet - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | snippet_id | path | | Yes | string | -| payload | body | | Yes | [SnippetDraftSyncPayload](#snippetdraftsyncpayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [SnippetDraftSyncPayload](#snippetdraftsyncpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Draft workflow synced successfully | | 400 | Hash mismatch | -### /snippets/{snippet_id}/workflows/draft/config +### [GET] /snippets/{snippet_id}/workflows/draft/config +**Get snippet draft workflow configuration limits** -#### GET -##### Summary - -Get snippet draft workflow configuration limits - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | snippet_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Draft config retrieved successfully | -### /snippets/{snippet_id}/workflows/draft/conversation-variables - -#### GET -##### Description - +### [GET] /snippets/{snippet_id}/workflows/draft/conversation-variables Conversation variables are not used in snippet workflows; returns an empty list for API parity -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | snippet_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Conversation variables retrieved successfully | [WorkflowDraftVariableList](#workflowdraftvariablelist) | - -### /snippets/{snippet_id}/workflows/draft/environment-variables - -#### GET -##### Description +| 200 | Conversation variables retrieved successfully | **application/json**: [WorkflowDraftVariableList](#workflowdraftvariablelist)
| +### [GET] /snippets/{snippet_id}/workflows/draft/environment-variables Get environment variables from snippet draft workflow graph -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | snippet_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Environment variables retrieved successfully | | 404 | Draft workflow not found | -### /snippets/{snippet_id}/workflows/draft/iteration/nodes/{node_id}/run - -#### POST -##### Summary - -Run a draft workflow iteration node for snippet - -##### Description +### [POST] /snippets/{snippet_id}/workflows/draft/iteration/nodes/{node_id}/run +**Run a draft workflow iteration node for snippet** Run draft workflow iteration node for snippet Iteration nodes execute their internal sub-graph multiple times over an input list. Returns an SSE event stream with iteration progress and results. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [SnippetIterationNodeRunPayload](#snippetiterationnoderunpayload) | | node_id | path | Node ID | Yes | string | | snippet_id | path | Snippet ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [SnippetIterationNodeRunPayload](#snippetiterationnoderunpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Iteration node run started successfully (SSE stream) | | 404 | Snippet or draft workflow not found | -### /snippets/{snippet_id}/workflows/draft/loop/nodes/{node_id}/run - -#### POST -##### Summary - -Run a draft workflow loop node for snippet - -##### Description +### [POST] /snippets/{snippet_id}/workflows/draft/loop/nodes/{node_id}/run +**Run a draft workflow loop node for snippet** Run draft workflow loop node for snippet Loop nodes execute their internal sub-graph repeatedly until a condition is met. Returns an SSE event stream with loop progress and results. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [SnippetLoopNodeRunPayload](#snippetloopnoderunpayload) | | node_id | path | Node ID | Yes | string | | snippet_id | path | Snippet ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [SnippetLoopNodeRunPayload](#snippetloopnoderunpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Loop node run started successfully (SSE stream) | | 404 | Snippet or draft workflow not found | -### /snippets/{snippet_id}/workflows/draft/nodes/{node_id}/last-run - -#### GET -##### Summary - -Get the last run result for a specific node in snippet draft workflow - -##### Description +### [GET] /snippets/{snippet_id}/workflows/draft/nodes/{node_id}/last-run +**Get the last run result for a specific node in snippet draft workflow** Get last run result for a node in snippet draft workflow Returns the most recent execution record for the given node, including status, inputs, outputs, and timing information. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | path | Node ID | Yes | string | | snippet_id | path | Snippet ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Node last run retrieved successfully | [WorkflowRunNodeExecutionResponse](#workflowrunnodeexecutionresponse) | +| 200 | Node last run retrieved successfully | **application/json**: [WorkflowRunNodeExecutionResponse](#workflowrunnodeexecutionresponse)
| | 404 | Snippet, draft workflow, or node last run not found | | -### /snippets/{snippet_id}/workflows/draft/nodes/{node_id}/run - -#### POST -##### Summary - -Run a single node in snippet draft workflow - -##### Description +### [POST] /snippets/{snippet_id}/workflows/draft/nodes/{node_id}/run +**Run a single node in snippet draft workflow** Run a single node in snippet draft workflow (single-step debugging) Executes a specific node with provided inputs for single-step debugging. Returns the node execution result including status, outputs, and timing. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [SnippetDraftNodeRunPayload](#snippetdraftnoderunpayload) | | node_id | path | Node ID | Yes | string | | snippet_id | path | Snippet ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [SnippetDraftNodeRunPayload](#snippetdraftnoderunpayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Node run completed successfully | [WorkflowRunNodeExecutionResponse](#workflowrunnodeexecutionresponse) | +| 200 | Node run completed successfully | **application/json**: [WorkflowRunNodeExecutionResponse](#workflowrunnodeexecutionresponse)
| | 404 | Snippet or draft workflow not found | | -### /snippets/{snippet_id}/workflows/draft/nodes/{node_id}/variables - -#### DELETE -##### Description - +### [DELETE] /snippets/{snippet_id}/workflows/draft/nodes/{node_id}/variables Delete all variables for a specific node (snippet draft workflow) -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | path | | Yes | string | | snippet_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Node variables deleted successfully | -#### GET -##### Description - +### [GET] /snippets/{snippet_id}/workflows/draft/nodes/{node_id}/variables Get variables for a specific node (snippet draft workflow) -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | node_id | path | | Yes | string | | snippet_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Node variables retrieved successfully | [WorkflowDraftVariableList](#workflowdraftvariablelist) | +| 200 | Node variables retrieved successfully | **application/json**: [WorkflowDraftVariableList](#workflowdraftvariablelist)
| -### /snippets/{snippet_id}/workflows/draft/run - -#### POST -##### Summary - -Run draft workflow for snippet - -##### Description +### [POST] /snippets/{snippet_id}/workflows/draft/run +**Run draft workflow for snippet** Executes the snippet's draft workflow with the provided inputs and returns an SSE event stream with execution progress and results. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | snippet_id | path | | Yes | string | -| payload | body | | Yes | [SnippetDraftRunPayload](#snippetdraftrunpayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [SnippetDraftRunPayload](#snippetdraftrunpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Draft workflow run started successfully (SSE stream) | | 404 | Snippet or draft workflow not found | -### /snippets/{snippet_id}/workflows/draft/system-variables - -#### GET -##### Description - +### [GET] /snippets/{snippet_id}/workflows/draft/system-variables System variables are not used in snippet workflows; returns an empty list for API parity -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | snippet_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | System variables retrieved successfully | [WorkflowDraftVariableList](#workflowdraftvariablelist) | - -### /snippets/{snippet_id}/workflows/draft/variables - -#### DELETE -##### Description +| 200 | System variables retrieved successfully | **application/json**: [WorkflowDraftVariableList](#workflowdraftvariablelist)
| +### [DELETE] /snippets/{snippet_id}/workflows/draft/variables Delete all draft workflow variables for the current user (snippet scope) -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | snippet_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Workflow variables deleted successfully | -#### GET -##### Description - +### [GET] /snippets/{snippet_id}/workflows/draft/variables List draft workflow variables without values (paginated, snippet scope) -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | +| limit | query | Items per page | No | integer,
**Default:** 20 | +| page | query | Page number | No | integer,
**Default:** 1 | | snippet_id | path | | Yes | string | -| payload | body | | Yes | [WorkflowDraftVariableListQuery](#workflowdraftvariablelistquery) | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow variables retrieved successfully | [WorkflowDraftVariableListWithoutValue](#workflowdraftvariablelistwithoutvalue) | - -### /snippets/{snippet_id}/workflows/draft/variables/{variable_id} - -#### DELETE -##### Description +| 200 | Workflow variables retrieved successfully | **application/json**: [WorkflowDraftVariableListWithoutValue](#workflowdraftvariablelistwithoutvalue)
| +### [DELETE] /snippets/{snippet_id}/workflows/draft/variables/{variable_id} Delete a draft workflow variable (snippet scope) -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | snippet_id | path | | Yes | string | | variable_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Variable deleted successfully | | 404 | Variable not found | -#### GET -##### Description - +### [GET] /snippets/{snippet_id}/workflows/draft/variables/{variable_id} Get a specific draft workflow variable (snippet scope) -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | snippet_id | path | | Yes | string | | variable_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Variable retrieved successfully | [WorkflowDraftVariable](#workflowdraftvariable) | +| 200 | Variable retrieved successfully | **application/json**: [WorkflowDraftVariable](#workflowdraftvariable)
| | 404 | Variable not found | | -#### PATCH -##### Description - +### [PATCH] /snippets/{snippet_id}/workflows/draft/variables/{variable_id} Update a draft workflow variable (snippet scope) -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | snippet_id | path | | Yes | string | | variable_id | path | | Yes | string | -| payload | body | | Yes | [WorkflowDraftVariableUpdatePayload](#workflowdraftvariableupdatepayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [WorkflowDraftVariableUpdatePayload](#workflowdraftvariableupdatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Variable updated successfully | [WorkflowDraftVariable](#workflowdraftvariable) | +| 200 | Variable updated successfully | **application/json**: [WorkflowDraftVariable](#workflowdraftvariable)
| | 404 | Variable not found | | -### /snippets/{snippet_id}/workflows/draft/variables/{variable_id}/reset - -#### PUT -##### Description - +### [PUT] /snippets/{snippet_id}/workflows/draft/variables/{variable_id}/reset Reset a draft workflow variable to its default value (snippet scope) -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | snippet_id | path | | Yes | string | | variable_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Variable reset successfully | [WorkflowDraftVariable](#workflowdraftvariable) | +| 200 | Variable reset successfully | **application/json**: [WorkflowDraftVariable](#workflowdraftvariable)
| | 204 | Variable reset (no content) | | | 404 | Variable not found | | -### /snippets/{snippet_id}/workflows/publish +### [GET] /snippets/{snippet_id}/workflows/publish +**Get published workflow for snippet** -#### GET -##### Summary - -Get published workflow for snippet - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | snippet_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Published workflow retrieved successfully | [SnippetWorkflowResponse](#snippetworkflowresponse) | +| 200 | Published workflow retrieved successfully | **application/json**: [SnippetWorkflowResponse](#snippetworkflowresponse)
| | 404 | Snippet not found | | -#### POST -##### Summary +### [POST] /snippets/{snippet_id}/workflows/publish +**Publish snippet workflow** -Publish snippet workflow - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | snippet_id | path | | Yes | string | -| payload | body | | Yes | [PublishWorkflowPayload](#publishworkflowpayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [PublishWorkflowPayload](#publishworkflowpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Workflow published successfully | | 400 | No draft workflow found | -### /snippets/{snippet_id}/workflows/{workflow_id}/restore +### [POST] /snippets/{snippet_id}/workflows/{workflow_id}/restore +**Restore a published snippet workflow version into the draft workflow** -#### POST -##### Summary - -Restore a published snippet workflow version into the draft workflow - -##### Description - -Restore a published snippet workflow version into the draft workflow - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | snippet_id | path | Snippet ID | Yes | string | | workflow_id | path | Published workflow ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -8735,31 +7794,19 @@ Restore a published snippet workflow version into the draft workflow | 400 | Source workflow must be published | | 404 | Workflow not found | -### /spec/schema-definitions - -#### GET -##### Summary - -Get system JSON Schema definitions specification - -##### Description +### [GET] /spec/schema-definitions +**Get system JSON Schema definitions specification** Used for frontend component type mapping -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /system-features - -#### GET -##### Summary - -Get system-wide feature configuration - -##### Description +### [GET] /system-features +**Get system-wide feature configuration** Get system-wide feature configuration NOTE: This endpoint is unauthenticated by design, as it provides system features @@ -8769,390 +7816,348 @@ Authentication would create circular dependency (can't login without dashboard l Only non-sensitive configuration data should be returned by this endpoint. -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SystemFeatureModel](#systemfeaturemodel) | +| 200 | Success | **application/json**: [SystemFeatureModel](#systemfeaturemodel)
| -### /tag-bindings +### [POST] /tag-bindings +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TagBindingPayload](#tagbindingpayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [TagBindingPayload](#tagbindingpayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | - -### /tag-bindings/remove - -#### POST -##### Description +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| +### [POST] /tag-bindings/remove Remove one or more tag bindings from a target. -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [TagBindingRemovePayload](#tagbindingremovepayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TagBindingRemovePayload](#tagbindingremovepayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -### /tags - -#### GET -##### Parameters +### [GET] /tags +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | keyword | query | Search keyword for tag name. | No | string | | type | query | Tag type filter. Can be "knowledge", "app", or "snippet". | No | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [ [TagResponse](#tagresponse) ] | +| 200 | Success | **application/json**: [ [TagResponse](#tagresponse) ]
| -#### POST -##### Parameters +### [POST] /tags +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [TagBasePayload](#tagbasepayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TagBasePayload](#tagbasepayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [TagResponse](#tagresponse) | +| 200 | Success | **application/json**: [TagResponse](#tagresponse)
| -### /tags/{tag_id} - -#### DELETE -##### Parameters +### [DELETE] /tags/{tag_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | tag_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Tag deleted successfully | -#### PATCH -##### Parameters +### [PATCH] /tags/{tag_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | tag_id | path | | Yes | string | -| payload | body | | Yes | [TagUpdateRequestPayload](#tagupdaterequestpayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TagUpdateRequestPayload](#tagupdaterequestpayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [TagResponse](#tagresponse) | - -### /test/retrieval - -#### POST -##### Description +| 200 | Success | **application/json**: [TagResponse](#tagresponse)
| +### [POST] /test/retrieval Bedrock retrieval test (internal use only) -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [BedrockRetrievalPayload](#bedrockretrievalpayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [BedrockRetrievalPayload](#bedrockretrievalpayload)
| -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Bedrock retrieval test completed | -### /trial-apps/{app_id} +### [GET] /trial-apps/{app_id} +**Get app detail** -#### GET -##### Summary - -Get app detail - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /trial-apps/{app_id}/audio-to-text - -#### POST -##### Parameters +### [POST] /trial-apps/{app_id}/audio-to-text +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /trial-apps/{app_id}/chat-messages - -#### POST -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | -| payload | body | | Yes | [ChatRequest](#chatrequest) | - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 200 | Success | - -### /trial-apps/{app_id}/completion-messages - -#### POST -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | -| payload | body | | Yes | [CompletionRequest](#completionrequest) | - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 200 | Success | - -### /trial-apps/{app_id}/datasets - -#### GET -##### Parameters +### [POST] /trial-apps/{app_id}/chat-messages +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ChatRequest](#chatrequest)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /trial-apps/{app_id}/messages/{message_id}/suggested-questions +### [POST] /trial-apps/{app_id}/completion-messages +#### Parameters -#### GET -##### Parameters +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| app_id | path | | Yes | string | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [CompletionRequest](#completionrequest)
| + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Success | + +### [GET] /trial-apps/{app_id}/datasets +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| app_id | path | | Yes | string | + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Success | + +### [GET] /trial-apps/{app_id}/messages/{message_id}/suggested-questions +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | | message_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /trial-apps/{app_id}/parameters +### [GET] /trial-apps/{app_id}/parameters +**Retrieve app parameters** -#### GET -##### Summary - -Retrieve app parameters - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /trial-apps/{app_id}/site - -#### GET -##### Summary - -Retrieve app site info - -##### Description +### [GET] /trial-apps/{app_id}/site +**Retrieve app site info** Returns the site configuration for the application including theme, icons, and text. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /trial-apps/{app_id}/text-to-audio - -#### POST -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | -| payload | body | | Yes | [TextToSpeechRequest](#texttospeechrequest) | - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 200 | Success | - -### /trial-apps/{app_id}/workflows - -#### GET -##### Summary - -Get workflow detail - -##### Parameters +### [POST] /trial-apps/{app_id}/text-to-audio +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TextToSpeechRequest](#texttospeechrequest)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /trial-apps/{app_id}/workflows/run +### [GET] /trial-apps/{app_id}/workflows +**Get workflow detail** -#### POST -##### Summary - -Run workflow - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | -| payload | body | | Yes | [WorkflowRunRequest](#workflowrunrequest) | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /trial-apps/{app_id}/workflows/tasks/{task_id}/stop +### [POST] /trial-apps/{app_id}/workflows/run +**Run workflow** -#### POST -##### Summary +#### Parameters -Stop workflow task +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| app_id | path | | Yes | string | -##### Parameters +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [WorkflowRunRequest](#workflowrunrequest)
| + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Success | + +### [POST] /trial-apps/{app_id}/workflows/tasks/{task_id}/stop +**Stop workflow task** + +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | | task_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /trial-models - -#### GET -##### Summary - -Get hosted trial model provider configuration for model-provider pages - -##### Description +### [GET] /trial-models +**Get hosted trial model provider configuration for model-provider pages** Get hosted trial model provider configuration -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [TrialModelsResponse](#trialmodelsresponse) | - -### /website/crawl - -#### POST -##### Description +| 200 | Success | **application/json**: [TrialModelsResponse](#trialmodelsresponse)
| +### [POST] /website/crawl Crawl website content -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WebsiteCrawlPayload](#websitecrawlpayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [WebsiteCrawlPayload](#websitecrawlpayload)
| -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Website crawl initiated successfully | | 400 | Invalid crawl parameters | -### /website/crawl/status/{job_id} - -#### GET -##### Description - +### [GET] /website/crawl/status/{job_id} Get website crawl status -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WebsiteCrawlStatusQuery](#websitecrawlstatusquery) | | job_id | path | Crawl job ID | Yes | string | | provider | query | Crawl provider (firecrawl/watercrawl/jinareader) | No | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -9160,20 +8165,16 @@ Get website crawl status | 400 | Invalid provider | | 404 | Crawl job not found | -### /workflow-generate - -#### POST -##### Description - +### [POST] /workflow-generate Generate a Dify workflow graph from natural language -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkflowGeneratePayload](#workflowgeneratepayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [WorkflowGeneratePayload](#workflowgeneratepayload)
| -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -9181,163 +8182,130 @@ Generate a Dify workflow graph from natural language | 400 | Invalid request parameters | | 402 | Provider quota exceeded | -### /workflow/{workflow_run_id}/events - -#### GET -##### Summary - -Get workflow execution events stream after resume - -##### Description +### [GET] /workflow/{workflow_run_id}/events +**Get workflow execution events stream after resume** GET /console/api/workflow//events Returns Server-Sent Events stream. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | workflow_run_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workflow/{workflow_run_id}/pause-details - -#### GET -##### Summary - -Get workflow pause details - -##### Description +### [GET] /workflow/{workflow_run_id}/pause-details +**Get workflow pause details** Get workflow pause details GET /console/api/workflow//pause-details Returns information about why and where the workflow is paused. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | workflow_run_id | path | Workflow run ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow pause details retrieved successfully | [WorkflowPauseDetailsResponse](#workflowpausedetailsresponse) | +| 200 | Workflow pause details retrieved successfully | **application/json**: [WorkflowPauseDetailsResponse](#workflowpausedetailsresponse)
| | 404 | Workflow run not found | | -### /workspaces - -#### GET -##### Responses +### [GET] /workspaces +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current - -#### POST -##### Responses +### [POST] /workspaces/current +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [TenantInfoResponse](#tenantinforesponse) | - -### /workspaces/current/agent-provider/{provider_name} - -#### GET -##### Description +| 200 | Success | **application/json**: [TenantInfoResponse](#tenantinforesponse)
| +### [GET] /workspaces/current/agent-provider/{provider_name} Get specific agent provider details -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider_name | path | Agent provider name | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | object | - -### /workspaces/current/agent-providers - -#### GET -##### Description +| 200 | Success | **application/json**: object
| +### [GET] /workspaces/current/agent-providers Get list of available agent providers -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [ object ] | +| 200 | Success | **application/json**: [ object ]
| -### /workspaces/current/customized-snippets +### [GET] /workspaces/current/customized-snippets +**List customized snippets with pagination and search** -#### GET -##### Summary - -List customized snippets with pagination and search - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [SnippetListQuery](#snippetlistquery) | +| creators | query | Filter by creator account IDs | No | | +| is_published | query | Filter by published status | No | | +| keyword | query | | No | | +| limit | query | | No | integer,
**Default:** 20 | +| page | query | | No | integer,
**Default:** 1 | +| tag_ids | query | Filter by tag IDs | No | | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Snippets retrieved successfully | [SnippetPagination](#snippetpagination) | +| 200 | Snippets retrieved successfully | **application/json**: [SnippetPagination](#snippetpagination)
| -#### POST -##### Summary +### [POST] /workspaces/current/customized-snippets +**Create a new customized snippet** -Create a new customized snippet +#### Request Body -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [CreateSnippetPayload](#createsnippetpayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [CreateSnippetPayload](#createsnippetpayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | Snippet created successfully | [Snippet](#snippet) | +| 201 | Snippet created successfully | **application/json**: [Snippet](#snippet)
| | 400 | Invalid request | | -### /workspaces/current/customized-snippets/imports +### [POST] /workspaces/current/customized-snippets/imports +**Import snippet from DSL** -#### POST -##### Summary +#### Request Body -Import snippet from DSL +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [SnippetImportPayload](#snippetimportpayload)
| -##### Description - -Import snippet from DSL - -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [SnippetImportPayload](#snippetimportpayload) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -9345,2196 +8313,2049 @@ Import snippet from DSL | 202 | Import pending confirmation | | 400 | Import failed | -### /workspaces/current/customized-snippets/imports/{import_id}/confirm +### [POST] /workspaces/current/customized-snippets/imports/{import_id}/confirm +**Confirm a pending snippet import** -#### POST -##### Summary - -Confirm a pending snippet import - -##### Description - -Confirm a pending snippet import - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | import_id | path | Import ID to confirm | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Import confirmed successfully | | 400 | Import failed | -### /workspaces/current/customized-snippets/{snippet_id} +### [DELETE] /workspaces/current/customized-snippets/{snippet_id} +**Delete customized snippet** -#### DELETE -##### Summary - -Delete customized snippet - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | snippet_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 204 | Snippet deleted successfully | | 404 | Snippet not found | -#### GET -##### Summary +### [GET] /workspaces/current/customized-snippets/{snippet_id} +**Get customized snippet details** -Get customized snippet details - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | snippet_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Snippet retrieved successfully | [Snippet](#snippet) | +| 200 | Snippet retrieved successfully | **application/json**: [Snippet](#snippet)
| | 404 | Snippet not found | | -#### PATCH -##### Summary +### [PATCH] /workspaces/current/customized-snippets/{snippet_id} +**Update customized snippet** -Update customized snippet - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | snippet_id | path | | Yes | string | -| payload | body | | Yes | [UpdateSnippetPayload](#updatesnippetpayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [UpdateSnippetPayload](#updatesnippetpayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Snippet updated successfully | [Snippet](#snippet) | +| 200 | Snippet updated successfully | **application/json**: [Snippet](#snippet)
| | 400 | Invalid request | | | 404 | Snippet not found | | -### /workspaces/current/customized-snippets/{snippet_id}/check-dependencies +### [GET] /workspaces/current/customized-snippets/{snippet_id}/check-dependencies +**Check dependencies for a snippet** -#### GET -##### Summary - -Check dependencies for a snippet - -##### Description - -Check dependencies for a snippet - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | snippet_id | path | Snippet ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Dependencies checked successfully | | 404 | Snippet not found | -### /workspaces/current/customized-snippets/{snippet_id}/export - -#### GET -##### Summary - -Export snippet as DSL - -##### Description +### [GET] /workspaces/current/customized-snippets/{snippet_id}/export +**Export snippet as DSL** Export snippet configuration as DSL -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | snippet_id | path | Snippet ID to export | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Snippet exported successfully | | 404 | Snippet not found | -### /workspaces/current/customized-snippets/{snippet_id}/use-count/increment - -#### POST -##### Summary - -Increment snippet use count when it is inserted into a workflow - -##### Description +### [POST] /workspaces/current/customized-snippets/{snippet_id}/use-count/increment +**Increment snippet use count when it is inserted into a workflow** Increment snippet use count by 1 -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | snippet_id | path | Snippet ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Use count incremented successfully | | 404 | Snippet not found | -### /workspaces/current/dataset-operators - -#### GET -##### Responses +### [GET] /workspaces/current/dataset-operators +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [AccountWithRoleList](#accountwithrolelist) | +| 200 | Success | **application/json**: [AccountWithRoleList](#accountwithrolelist)
| -### /workspaces/current/default-model - -#### GET -##### Parameters +### [GET] /workspaces/current/default-model +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ParserGetDefault](#parsergetdefault) | +| model_type | query | | Yes | [ModelType](#modeltype) | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -#### POST -##### Parameters +### [POST] /workspaces/current/default-model +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ParserPostDefault](#parserpostdefault) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserPostDefault](#parserpostdefault)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | - -### /workspaces/current/endpoints - -#### POST -##### Description +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| +### [POST] /workspaces/current/endpoints Create a new plugin endpoint -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [EndpointCreatePayload](#endpointcreatepayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [EndpointCreatePayload](#endpointcreatepayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Endpoint created successfully | [EndpointCreateResponse](#endpointcreateresponse) | +| 200 | Endpoint created successfully | **application/json**: [EndpointCreateResponse](#endpointcreateresponse)
| | 403 | Admin privileges required | | -### /workspaces/current/endpoints/create +### ~~[POST] /workspaces/current/endpoints/create~~ -#### POST ***DEPRECATED*** -##### Description Deprecated legacy alias for creating a plugin endpoint. Use POST /workspaces/current/endpoints instead. -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [EndpointCreatePayload](#endpointcreatepayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [EndpointCreatePayload](#endpointcreatepayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Endpoint created successfully | [EndpointCreateResponse](#endpointcreateresponse) | +| 200 | Endpoint created successfully | **application/json**: [EndpointCreateResponse](#endpointcreateresponse)
| | 403 | Admin privileges required | | -### /workspaces/current/endpoints/delete +### ~~[POST] /workspaces/current/endpoints/delete~~ -#### POST ***DEPRECATED*** -##### Description Deprecated legacy alias for deleting a plugin endpoint. Use DELETE /workspaces/current/endpoints/{id} instead. -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [EndpointIdPayload](#endpointidpayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [EndpointIdPayload](#endpointidpayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Endpoint deleted successfully | [EndpointDeleteResponse](#endpointdeleteresponse) | +| 200 | Endpoint deleted successfully | **application/json**: [EndpointDeleteResponse](#endpointdeleteresponse)
| | 403 | Admin privileges required | | -### /workspaces/current/endpoints/disable - -#### POST -##### Description - +### [POST] /workspaces/current/endpoints/disable Disable a plugin endpoint -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [EndpointIdPayload](#endpointidpayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [EndpointIdPayload](#endpointidpayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Endpoint disabled successfully | [EndpointDisableResponse](#endpointdisableresponse) | +| 200 | Endpoint disabled successfully | **application/json**: [EndpointDisableResponse](#endpointdisableresponse)
| | 403 | Admin privileges required | | -### /workspaces/current/endpoints/enable - -#### POST -##### Description - +### [POST] /workspaces/current/endpoints/enable Enable a plugin endpoint -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [EndpointIdPayload](#endpointidpayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [EndpointIdPayload](#endpointidpayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Endpoint enabled successfully | [EndpointEnableResponse](#endpointenableresponse) | +| 200 | Endpoint enabled successfully | **application/json**: [EndpointEnableResponse](#endpointenableresponse)
| | 403 | Admin privileges required | | -### /workspaces/current/endpoints/list - -#### GET -##### Description - +### [GET] /workspaces/current/endpoints/list List plugin endpoints with pagination -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [EndpointListQuery](#endpointlistquery) | +| page | query | | Yes | integer | +| page_size | query | | Yes | integer | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [EndpointListResponse](#endpointlistresponse) | - -### /workspaces/current/endpoints/list/plugin - -#### GET -##### Description +| 200 | Success | **application/json**: [EndpointListResponse](#endpointlistresponse)
| +### [GET] /workspaces/current/endpoints/list/plugin List endpoints for a specific plugin -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [EndpointListForPluginQuery](#endpointlistforpluginquery) | +| page | query | | Yes | integer | +| page_size | query | | Yes | integer | +| plugin_id | query | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [PluginEndpointListResponse](#pluginendpointlistresponse) | +| 200 | Success | **application/json**: [PluginEndpointListResponse](#pluginendpointlistresponse)
| -### /workspaces/current/endpoints/update +### ~~[POST] /workspaces/current/endpoints/update~~ -#### POST ***DEPRECATED*** -##### Description Deprecated legacy alias for updating a plugin endpoint. Use PATCH /workspaces/current/endpoints/{id} instead. -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [LegacyEndpointUpdatePayload](#legacyendpointupdatepayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [LegacyEndpointUpdatePayload](#legacyendpointupdatepayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Endpoint updated successfully | [EndpointUpdateResponse](#endpointupdateresponse) | +| 200 | Endpoint updated successfully | **application/json**: [EndpointUpdateResponse](#endpointupdateresponse)
| | 403 | Admin privileges required | | -### /workspaces/current/endpoints/{id} - -#### DELETE -##### Description - +### [DELETE] /workspaces/current/endpoints/{id} Delete a plugin endpoint -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | id | path | Endpoint ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Endpoint deleted successfully | [EndpointDeleteResponse](#endpointdeleteresponse) | +| 200 | Endpoint deleted successfully | **application/json**: [EndpointDeleteResponse](#endpointdeleteresponse)
| | 403 | Admin privileges required | | -#### PATCH -##### Description - +### [PATCH] /workspaces/current/endpoints/{id} Update a plugin endpoint -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [EndpointUpdatePayload](#endpointupdatepayload) | | id | path | Endpoint ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [EndpointUpdatePayload](#endpointupdatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Endpoint updated successfully | [EndpointUpdateResponse](#endpointupdateresponse) | +| 200 | Endpoint updated successfully | **application/json**: [EndpointUpdateResponse](#endpointupdateresponse)
| | 403 | Admin privileges required | | -### /workspaces/current/members - -#### GET -##### Responses +### [GET] /workspaces/current/members +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [AccountWithRoleList](#accountwithrolelist) | +| 200 | Success | **application/json**: [AccountWithRoleList](#accountwithrolelist)
| -### /workspaces/current/members/invite-email +### [POST] /workspaces/current/members/invite-email +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [MemberInvitePayload](#memberinvitepayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [MemberInvitePayload](#memberinvitepayload) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/members/owner-transfer-check +### [POST] /workspaces/current/members/owner-transfer-check +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [OwnerTransferCheckPayload](#ownertransfercheckpayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [OwnerTransferCheckPayload](#ownertransfercheckpayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [VerificationTokenResponse](#verificationtokenresponse) | +| 200 | Success | **application/json**: [VerificationTokenResponse](#verificationtokenresponse)
| -### /workspaces/current/members/send-owner-transfer-confirm-email +### [POST] /workspaces/current/members/send-owner-transfer-confirm-email +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [OwnerTransferEmailPayload](#ownertransferemailpayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [OwnerTransferEmailPayload](#ownertransferemailpayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultDataResponse](#simpleresultdataresponse) | +| 200 | Success | **application/json**: [SimpleResultDataResponse](#simpleresultdataresponse)
| -### /workspaces/current/members/{member_id} - -#### DELETE -##### Parameters +### [DELETE] /workspaces/current/members/{member_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | member_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/members/{member_id}/owner-transfer - -#### POST -##### Parameters +### [POST] /workspaces/current/members/{member_id}/owner-transfer +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | member_id | path | | Yes | string | -| payload | body | | Yes | [OwnerTransferPayload](#ownertransferpayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [OwnerTransferPayload](#ownertransferpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/members/{member_id}/update-role - -#### PUT -##### Parameters +### [PUT] /workspaces/current/members/{member_id}/update-role +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | member_id | path | | Yes | string | -| payload | body | | Yes | [MemberRoleUpdatePayload](#memberroleupdatepayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [MemberRoleUpdatePayload](#memberroleupdatepayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/model-providers - -#### GET -##### Parameters +### [GET] /workspaces/current/model-providers +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ParserModelList](#parsermodellist) | +| model_type | query | | No | | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/model-providers/{provider}/checkout-url - -#### GET -##### Parameters +### [GET] /workspaces/current/model-providers/{provider}/checkout-url +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/model-providers/{provider}/credentials - -#### DELETE -##### Parameters +### [DELETE] /workspaces/current/model-providers/{provider}/credentials +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -| payload | body | | Yes | [ParserCredentialDelete](#parsercredentialdelete) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserCredentialDelete](#parsercredentialdelete)
| + +#### Responses | Code | Description | | ---- | ----------- | | 204 | Credential deleted successfully | -#### GET -##### Parameters +### [GET] /workspaces/current/model-providers/{provider}/credentials +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | +| credential_id | query | | No | | | provider | path | | Yes | string | -| payload | body | | Yes | [ParserCredentialId](#parsercredentialid) | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -#### POST -##### Parameters +### [POST] /workspaces/current/model-providers/{provider}/credentials +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -| payload | body | | Yes | [ParserCredentialCreate](#parsercredentialcreate) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserCredentialCreate](#parsercredentialcreate)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -#### PUT -##### Parameters +### [PUT] /workspaces/current/model-providers/{provider}/credentials +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -| payload | body | | Yes | [ParserCredentialUpdate](#parsercredentialupdate) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserCredentialUpdate](#parsercredentialupdate)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/model-providers/{provider}/credentials/switch - -#### POST -##### Parameters +### [POST] /workspaces/current/model-providers/{provider}/credentials/switch +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -| payload | body | | Yes | [ParserCredentialSwitch](#parsercredentialswitch) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserCredentialSwitch](#parsercredentialswitch)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -### /workspaces/current/model-providers/{provider}/credentials/validate - -#### POST -##### Parameters +### [POST] /workspaces/current/model-providers/{provider}/credentials/validate +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -| payload | body | | Yes | [ParserCredentialValidate](#parsercredentialvalidate) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserCredentialValidate](#parsercredentialvalidate)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/model-providers/{provider}/models - -#### DELETE -##### Parameters +### [DELETE] /workspaces/current/model-providers/{provider}/models +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -| payload | body | | Yes | [ParserDeleteModels](#parserdeletemodels) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserDeleteModels](#parserdeletemodels)
| + +#### Responses | Code | Description | | ---- | ----------- | | 204 | Model deleted successfully | -#### GET -##### Parameters +### [GET] /workspaces/current/model-providers/{provider}/models +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -#### POST -##### Parameters +### [POST] /workspaces/current/model-providers/{provider}/models +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -| payload | body | | Yes | [ParserPostModels](#parserpostmodels) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserPostModels](#parserpostmodels)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/model-providers/{provider}/models/credentials - -#### DELETE -##### Parameters +### [DELETE] /workspaces/current/model-providers/{provider}/models/credentials +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -| payload | body | | Yes | [ParserDeleteCredential](#parserdeletecredential) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserDeleteCredential](#parserdeletecredential)
| + +#### Responses | Code | Description | | ---- | ----------- | | 204 | Credential deleted successfully | -#### GET -##### Parameters +### [GET] /workspaces/current/model-providers/{provider}/models/credentials +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | +| config_from | query | | No | | +| credential_id | query | | No | | +| model | query | | Yes | string | +| model_type | query | | Yes | [ModelType](#modeltype) | | provider | path | | Yes | string | -| payload | body | | Yes | [ParserGetCredentials](#parsergetcredentials) | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -#### POST -##### Parameters +### [POST] /workspaces/current/model-providers/{provider}/models/credentials +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -| payload | body | | Yes | [ParserCreateCredential](#parsercreatecredential) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserCreateCredential](#parsercreatecredential)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -#### PUT -##### Parameters +### [PUT] /workspaces/current/model-providers/{provider}/models/credentials +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -| payload | body | | Yes | [ParserUpdateCredential](#parserupdatecredential) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserUpdateCredential](#parserupdatecredential)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/model-providers/{provider}/models/credentials/switch - -#### POST -##### Parameters +### [POST] /workspaces/current/model-providers/{provider}/models/credentials/switch +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -| payload | body | | Yes | [ParserSwitch](#parserswitch) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserSwitch](#parserswitch)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -### /workspaces/current/model-providers/{provider}/models/credentials/validate - -#### POST -##### Parameters +### [POST] /workspaces/current/model-providers/{provider}/models/credentials/validate +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -| payload | body | | Yes | [ParserValidate](#parservalidate) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserValidate](#parservalidate)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/model-providers/{provider}/models/disable - -#### PATCH -##### Parameters +### [PATCH] /workspaces/current/model-providers/{provider}/models/disable +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -| payload | body | | Yes | [ParserDeleteModels](#parserdeletemodels) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserDeleteModels](#parserdeletemodels)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -### /workspaces/current/model-providers/{provider}/models/enable - -#### PATCH -##### Parameters +### [PATCH] /workspaces/current/model-providers/{provider}/models/enable +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -| payload | body | | Yes | [ParserDeleteModels](#parserdeletemodels) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserDeleteModels](#parserdeletemodels)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -### /workspaces/current/model-providers/{provider}/models/load-balancing-configs/credentials-validate - -#### POST -##### Parameters +### [POST] /workspaces/current/model-providers/{provider}/models/load-balancing-configs/credentials-validate +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -| payload | body | | Yes | [LoadBalancingCredentialPayload](#loadbalancingcredentialpayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [LoadBalancingCredentialPayload](#loadbalancingcredentialpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/model-providers/{provider}/models/load-balancing-configs/{config_id}/credentials-validate - -#### POST -##### Parameters +### [POST] /workspaces/current/model-providers/{provider}/models/load-balancing-configs/{config_id}/credentials-validate +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | config_id | path | | Yes | string | | provider | path | | Yes | string | -| payload | body | | Yes | [LoadBalancingCredentialPayload](#loadbalancingcredentialpayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [LoadBalancingCredentialPayload](#loadbalancingcredentialpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/model-providers/{provider}/models/parameter-rules - -#### GET -##### Parameters +### [GET] /workspaces/current/model-providers/{provider}/models/parameter-rules +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | +| model | query | | Yes | string | | provider | path | | Yes | string | -| payload | body | | Yes | [ParserParameter](#parserparameter) | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/model-providers/{provider}/preferred-provider-type - -#### POST -##### Parameters +### [POST] /workspaces/current/model-providers/{provider}/preferred-provider-type +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -| payload | body | | Yes | [ParserPreferredProviderType](#parserpreferredprovidertype) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserPreferredProviderType](#parserpreferredprovidertype)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -### /workspaces/current/models/model-types/{model_type} - -#### GET -##### Parameters +### [GET] /workspaces/current/models/model-types/{model_type} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | model_type | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/permission - -#### GET -##### Summary - -Get workspace permission settings - -##### Description +### [GET] /workspaces/current/permission +**Get workspace permission settings** Returns permission flags that control workspace features like member invitations and owner transfer. -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [WorkspacePermissionResponse](#workspacepermissionresponse) | +| 200 | Success | **application/json**: [WorkspacePermissionResponse](#workspacepermissionresponse)
| -### /workspaces/current/plugin/asset - -#### GET -##### Parameters +### [GET] /workspaces/current/plugin/asset +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ParserAsset](#parserasset) | +| file_name | query | | Yes | string | +| plugin_unique_identifier | query | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/plugin/debugging-key - -#### GET -##### Responses +### [GET] /workspaces/current/plugin/debugging-key +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [PluginDebuggingKeyResponse](#plugindebuggingkeyresponse) | +| 200 | Success | **application/json**: [PluginDebuggingKeyResponse](#plugindebuggingkeyresponse)
| -### /workspaces/current/plugin/fetch-manifest - -#### GET -##### Parameters +### [GET] /workspaces/current/plugin/fetch-manifest +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ParserPluginIdentifierQuery](#parserpluginidentifierquery) | +| plugin_unique_identifier | query | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/plugin/icon - -#### GET -##### Parameters +### [GET] /workspaces/current/plugin/icon +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ParserIcon](#parsericon) | +| filename | query | | Yes | string | +| tenant_id | query | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/plugin/install/github +### [POST] /workspaces/current/plugin/install/github +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserGithubInstall](#parsergithubinstall)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ParserGithubInstall](#parsergithubinstall) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/plugin/install/marketplace +### [POST] /workspaces/current/plugin/install/marketplace +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserPluginIdentifiers](#parserpluginidentifiers)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ParserPluginIdentifiers](#parserpluginidentifiers) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/plugin/install/pkg +### [POST] /workspaces/current/plugin/install/pkg +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserPluginIdentifiers](#parserpluginidentifiers)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ParserPluginIdentifiers](#parserpluginidentifiers) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/plugin/list - -#### GET -##### Parameters +### [GET] /workspaces/current/plugin/list +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ParserList](#parserlist) | +| page | query | Page number | No | integer,
**Default:** 1 | +| page_size | query | Page size (1-256) | No | integer,
**Default:** 256 | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/plugin/list/installations/ids +### [POST] /workspaces/current/plugin/list/installations/ids +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserLatest](#parserlatest)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ParserLatest](#parserlatest) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/plugin/list/latest-versions +### [POST] /workspaces/current/plugin/list/latest-versions +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserLatest](#parserlatest)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ParserLatest](#parserlatest) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/plugin/marketplace/pkg - -#### GET -##### Parameters +### [GET] /workspaces/current/plugin/marketplace/pkg +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ParserPluginIdentifierQuery](#parserpluginidentifierquery) | +| plugin_unique_identifier | query | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/plugin/parameters/dynamic-options - -#### GET -##### Parameters +### [GET] /workspaces/current/plugin/parameters/dynamic-options +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ParserDynamicOptions](#parserdynamicoptions) | +| action | query | | Yes | string | +| credential_id | query | | No | | +| parameter | query | | Yes | string | +| plugin_id | query | | Yes | string | +| provider | query | | Yes | string | +| provider_type | query | | Yes | string,
**Available values:** "tool", "trigger" | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/plugin/parameters/dynamic-options-with-credentials +### [POST] /workspaces/current/plugin/parameters/dynamic-options-with-credentials +**Fetch dynamic options using credentials directly (for edit mode)** -#### POST -##### Summary +#### Request Body -Fetch dynamic options using credentials directly (for edit mode) +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserDynamicOptionsWithCredentials](#parserdynamicoptionswithcredentials)
| -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ParserDynamicOptionsWithCredentials](#parserdynamicoptionswithcredentials) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/plugin/permission/change +### [POST] /workspaces/current/plugin/permission/change +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserPermissionChange](#parserpermissionchange)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ParserPermissionChange](#parserpermissionchange) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SuccessResponse](#successresponse) | +| 200 | Success | **application/json**: [SuccessResponse](#successresponse)
| -### /workspaces/current/plugin/permission/fetch - -#### GET -##### Responses +### [GET] /workspaces/current/plugin/permission/fetch +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/plugin/preferences/autoupgrade/exclude +### [POST] /workspaces/current/plugin/preferences/autoupgrade/exclude +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserExcludePlugin](#parserexcludeplugin)
| + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Success | + +### [POST] /workspaces/current/plugin/preferences/change +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserPreferencesChange](#parserpreferenceschange)
| + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Success | + +### [GET] /workspaces/current/plugin/preferences/fetch +#### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Success | + +### [GET] /workspaces/current/plugin/readme +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ParserExcludePlugin](#parserexcludeplugin) | +| language | query | | No | string,
**Default:** en-US | +| plugin_unique_identifier | query | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/plugin/preferences/change - -#### POST -##### Parameters +### [GET] /workspaces/current/plugin/tasks +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ParserPreferencesChange](#parserpreferenceschange) | +| page | query | Page number | No | integer,
**Default:** 1 | +| page_size | query | Page size (1-256) | No | integer,
**Default:** 256 | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/plugin/preferences/fetch - -#### GET -##### Responses - -| Code | Description | -| ---- | ----------- | -| 200 | Success | - -### /workspaces/current/plugin/readme - -#### GET -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ParserReadme](#parserreadme) | - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 200 | Success | - -### /workspaces/current/plugin/tasks - -#### GET -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ParserTasks](#parsertasks) | - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 200 | Success | - -### /workspaces/current/plugin/tasks/delete_all - -#### POST -##### Responses +### [POST] /workspaces/current/plugin/tasks/delete_all +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SuccessResponse](#successresponse) | +| 200 | Success | **application/json**: [SuccessResponse](#successresponse)
| -### /workspaces/current/plugin/tasks/{task_id} - -#### GET -##### Parameters +### [GET] /workspaces/current/plugin/tasks/{task_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | task_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/plugin/tasks/{task_id}/delete - -#### POST -##### Parameters +### [POST] /workspaces/current/plugin/tasks/{task_id}/delete +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | task_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SuccessResponse](#successresponse) | +| 200 | Success | **application/json**: [SuccessResponse](#successresponse)
| -### /workspaces/current/plugin/tasks/{task_id}/delete/{identifier} - -#### POST -##### Parameters +### [POST] /workspaces/current/plugin/tasks/{task_id}/delete/{identifier} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | identifier | path | | Yes | string | | task_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SuccessResponse](#successresponse) | +| 200 | Success | **application/json**: [SuccessResponse](#successresponse)
| -### /workspaces/current/plugin/uninstall +### [POST] /workspaces/current/plugin/uninstall +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserUninstall](#parseruninstall)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ParserUninstall](#parseruninstall) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SuccessResponse](#successresponse) | +| 200 | Success | **application/json**: [SuccessResponse](#successresponse)
| -### /workspaces/current/plugin/upgrade/github +### [POST] /workspaces/current/plugin/upgrade/github +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserGithubUpgrade](#parsergithubupgrade)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ParserGithubUpgrade](#parsergithubupgrade) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/plugin/upgrade/marketplace +### [POST] /workspaces/current/plugin/upgrade/marketplace +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserMarketplaceUpgrade](#parsermarketplaceupgrade)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ParserMarketplaceUpgrade](#parsermarketplaceupgrade) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/plugin/upload/bundle - -#### POST -##### Responses +### [POST] /workspaces/current/plugin/upload/bundle +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/plugin/upload/github +### [POST] /workspaces/current/plugin/upload/github +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ParserGithubUpload](#parsergithubupload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ParserGithubUpload](#parsergithubupload) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/plugin/upload/pkg - -#### POST -##### Responses +### [POST] /workspaces/current/plugin/upload/pkg +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-labels - -#### GET -##### Responses +### [GET] /workspaces/current/tool-labels +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-provider/api/add +### [POST] /workspaces/current/tool-provider/api/add +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ApiToolProviderAddPayload](#apitoolprovideraddpayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ApiToolProviderAddPayload](#apitoolprovideraddpayload) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-provider/api/delete +### [POST] /workspaces/current/tool-provider/api/delete +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ApiToolProviderDeletePayload](#apitoolproviderdeletepayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ApiToolProviderDeletePayload](#apitoolproviderdeletepayload) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-provider/api/get - -#### GET -##### Responses +### [GET] /workspaces/current/tool-provider/api/get +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-provider/api/remote - -#### GET -##### Responses +### [GET] /workspaces/current/tool-provider/api/remote +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-provider/api/schema +### [POST] /workspaces/current/tool-provider/api/schema +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ApiToolSchemaPayload](#apitoolschemapayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ApiToolSchemaPayload](#apitoolschemapayload) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-provider/api/test/pre +### [POST] /workspaces/current/tool-provider/api/test/pre +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ApiToolTestPayload](#apitooltestpayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ApiToolTestPayload](#apitooltestpayload) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-provider/api/tools - -#### GET -##### Responses +### [GET] /workspaces/current/tool-provider/api/tools +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-provider/api/update +### [POST] /workspaces/current/tool-provider/api/update +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ApiToolProviderUpdatePayload](#apitoolproviderupdatepayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ApiToolProviderUpdatePayload](#apitoolproviderupdatepayload) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-provider/builtin/{provider}/add - -#### POST -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| provider | path | | Yes | string | -| payload | body | | Yes | [BuiltinToolAddPayload](#builtintooladdpayload) | - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 200 | Success | - -### /workspaces/current/tool-provider/builtin/{provider}/credential/info - -#### GET -##### Parameters +### [POST] /workspaces/current/tool-provider/builtin/{provider}/add +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [BuiltinToolAddPayload](#builtintooladdpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-provider/builtin/{provider}/credential/schema/{credential_type} +### [GET] /workspaces/current/tool-provider/builtin/{provider}/credential/info +#### Parameters -#### GET -##### Parameters +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| provider | path | | Yes | string | + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Success | + +### [GET] /workspaces/current/tool-provider/builtin/{provider}/credential/schema/{credential_type} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | credential_type | path | | Yes | string | | provider | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-provider/builtin/{provider}/credentials - -#### GET -##### Parameters +### [GET] /workspaces/current/tool-provider/builtin/{provider}/credentials +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-provider/builtin/{provider}/default-credential - -#### POST -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| provider | path | | Yes | string | -| payload | body | | Yes | [BuiltinProviderDefaultCredentialPayload](#builtinproviderdefaultcredentialpayload) | - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 200 | Success | - -### /workspaces/current/tool-provider/builtin/{provider}/delete - -#### POST -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| provider | path | | Yes | string | -| payload | body | | Yes | [BuiltinToolCredentialDeletePayload](#builtintoolcredentialdeletepayload) | - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 200 | Success | - -### /workspaces/current/tool-provider/builtin/{provider}/icon - -#### GET -##### Parameters +### [POST] /workspaces/current/tool-provider/builtin/{provider}/default-credential +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [BuiltinProviderDefaultCredentialPayload](#builtinproviderdefaultcredentialpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-provider/builtin/{provider}/info - -#### GET -##### Parameters +### [POST] /workspaces/current/tool-provider/builtin/{provider}/delete +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [BuiltinToolCredentialDeletePayload](#builtintoolcredentialdeletepayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-provider/builtin/{provider}/oauth/client-schema - -#### GET -##### Parameters +### [GET] /workspaces/current/tool-provider/builtin/{provider}/icon +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-provider/builtin/{provider}/oauth/custom-client - -#### DELETE -##### Parameters +### [GET] /workspaces/current/tool-provider/builtin/{provider}/info +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -#### GET -##### Parameters +### [GET] /workspaces/current/tool-provider/builtin/{provider}/oauth/client-schema +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -#### POST -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| provider | path | | Yes | string | -| payload | body | | Yes | [ToolOAuthCustomClientPayload](#tooloauthcustomclientpayload) | - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 200 | Success | - -### /workspaces/current/tool-provider/builtin/{provider}/tools - -#### GET -##### Parameters +### [DELETE] /workspaces/current/tool-provider/builtin/{provider}/oauth/custom-client +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-provider/builtin/{provider}/update - -#### POST -##### Parameters +### [GET] /workspaces/current/tool-provider/builtin/{provider}/oauth/custom-client +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -| payload | body | | Yes | [BuiltinToolUpdatePayload](#builtintoolupdatepayload) | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-provider/mcp - -#### DELETE -##### Parameters +### [POST] /workspaces/current/tool-provider/builtin/{provider}/oauth/custom-client +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [MCPProviderDeletePayload](#mcpproviderdeletepayload) | +| provider | path | | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ToolOAuthCustomClientPayload](#tooloauthcustomclientpayload)
| + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Success | + +### [GET] /workspaces/current/tool-provider/builtin/{provider}/tools +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| provider | path | | Yes | string | + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Success | + +### [POST] /workspaces/current/tool-provider/builtin/{provider}/update +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| provider | path | | Yes | string | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [BuiltinToolUpdatePayload](#builtintoolupdatepayload)
| + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Success | + +### [DELETE] /workspaces/current/tool-provider/mcp +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [MCPProviderDeletePayload](#mcpproviderdeletepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -#### POST -##### Parameters +### [POST] /workspaces/current/tool-provider/mcp +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [MCPProviderCreatePayload](#mcpprovidercreatepayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [MCPProviderCreatePayload](#mcpprovidercreatepayload)
| -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -#### PUT -##### Parameters +### [PUT] /workspaces/current/tool-provider/mcp +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [MCPProviderUpdatePayload](#mcpproviderupdatepayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [MCPProviderUpdatePayload](#mcpproviderupdatepayload)
| -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-provider/mcp/auth +### [POST] /workspaces/current/tool-provider/mcp/auth +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [MCPAuthPayload](#mcpauthpayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [MCPAuthPayload](#mcpauthpayload) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-provider/mcp/tools/{provider_id} - -#### GET -##### Parameters +### [GET] /workspaces/current/tool-provider/mcp/tools/{provider_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-provider/mcp/update/{provider_id} - -#### GET -##### Parameters +### [GET] /workspaces/current/tool-provider/mcp/update/{provider_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-provider/workflow/create +### [POST] /workspaces/current/tool-provider/workflow/create +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [WorkflowToolCreatePayload](#workflowtoolcreatepayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkflowToolCreatePayload](#workflowtoolcreatepayload) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-provider/workflow/delete +### [POST] /workspaces/current/tool-provider/workflow/delete +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [WorkflowToolDeletePayload](#workflowtooldeletepayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkflowToolDeletePayload](#workflowtooldeletepayload) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-provider/workflow/get - -#### GET -##### Responses +### [GET] /workspaces/current/tool-provider/workflow/get +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-provider/workflow/tools - -#### GET -##### Responses +### [GET] /workspaces/current/tool-provider/workflow/tools +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-provider/workflow/update +### [POST] /workspaces/current/tool-provider/workflow/update +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [WorkflowToolUpdatePayload](#workflowtoolupdatepayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkflowToolUpdatePayload](#workflowtoolupdatepayload) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tool-providers - -#### GET -##### Responses +### [GET] /workspaces/current/tool-providers +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tools/api - -#### GET -##### Responses +### [GET] /workspaces/current/tools/api +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tools/builtin - -#### GET -##### Responses +### [GET] /workspaces/current/tools/builtin +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tools/mcp - -#### GET -##### Responses +### [GET] /workspaces/current/tools/mcp +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/tools/workflow - -#### GET -##### Responses +### [GET] /workspaces/current/tools/workflow +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/trigger-provider/{provider}/icon - -#### GET -##### Parameters +### [GET] /workspaces/current/trigger-provider/{provider}/icon +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/trigger-provider/{provider}/info +### [GET] /workspaces/current/trigger-provider/{provider}/info +**Get info for a trigger provider** -#### GET -##### Summary - -Get info for a trigger provider - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/trigger-provider/{provider}/oauth/client +### [DELETE] /workspaces/current/trigger-provider/{provider}/oauth/client +**Remove custom OAuth client configuration** -#### DELETE -##### Summary - -Remove custom OAuth client configuration - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -#### GET -##### Summary +### [GET] /workspaces/current/trigger-provider/{provider}/oauth/client +**Get OAuth client configuration for a provider** -Get OAuth client configuration for a provider - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -#### POST -##### Summary +### [POST] /workspaces/current/trigger-provider/{provider}/oauth/client +**Configure custom OAuth client for a provider** -Configure custom OAuth client for a provider - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -| payload | body | | Yes | [TriggerOAuthClientPayload](#triggeroauthclientpayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TriggerOAuthClientPayload](#triggeroauthclientpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/trigger-provider/{provider}/subscriptions/builder/build/{subscription_builder_id} +### [POST] /workspaces/current/trigger-provider/{provider}/subscriptions/builder/build/{subscription_builder_id} +**Build a subscription instance for a trigger provider** -#### POST -##### Summary - -Build a subscription instance for a trigger provider - -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| provider | path | | Yes | string | -| subscription_builder_id | path | | Yes | string | -| payload | body | | Yes | [TriggerSubscriptionBuilderUpdatePayload](#triggersubscriptionbuilderupdatepayload) | - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 200 | Success | - -### /workspaces/current/trigger-provider/{provider}/subscriptions/builder/create - -#### POST -##### Summary - -Add a new subscription instance for a trigger provider - -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| provider | path | | Yes | string | -| payload | body | | Yes | [TriggerSubscriptionBuilderCreatePayload](#triggersubscriptionbuildercreatepayload) | - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 200 | Success | - -### /workspaces/current/trigger-provider/{provider}/subscriptions/builder/logs/{subscription_builder_id} - -#### GET -##### Summary - -Get the request logs for a subscription instance for a trigger provider - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | | subscription_builder_id | path | | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TriggerSubscriptionBuilderUpdatePayload](#triggersubscriptionbuilderupdatepayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/trigger-provider/{provider}/subscriptions/builder/update/{subscription_builder_id} +### [POST] /workspaces/current/trigger-provider/{provider}/subscriptions/builder/create +**Add a new subscription instance for a trigger provider** -#### POST -##### Summary - -Update a subscription instance for a trigger provider - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -| subscription_builder_id | path | | Yes | string | -| payload | body | | Yes | [TriggerSubscriptionBuilderUpdatePayload](#triggersubscriptionbuilderupdatepayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TriggerSubscriptionBuilderCreatePayload](#triggersubscriptionbuildercreatepayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/trigger-provider/{provider}/subscriptions/builder/verify-and-update/{subscription_builder_id} +### [GET] /workspaces/current/trigger-provider/{provider}/subscriptions/builder/logs/{subscription_builder_id} +**Get the request logs for a subscription instance for a trigger provider** -#### POST -##### Summary - -Verify and update a subscription instance for a trigger provider - -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| provider | path | | Yes | string | -| subscription_builder_id | path | | Yes | string | -| payload | body | | Yes | [TriggerSubscriptionBuilderVerifyPayload](#triggersubscriptionbuilderverifypayload) | - -##### Responses - -| Code | Description | -| ---- | ----------- | -| 200 | Success | - -### /workspaces/current/trigger-provider/{provider}/subscriptions/builder/{subscription_builder_id} - -#### GET -##### Summary - -Get a subscription instance for a trigger provider - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | | subscription_builder_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/trigger-provider/{provider}/subscriptions/list +### [POST] /workspaces/current/trigger-provider/{provider}/subscriptions/builder/update/{subscription_builder_id} +**Update a subscription instance for a trigger provider** -#### GET -##### Summary +#### Parameters -List all trigger subscriptions for the current tenant's provider +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| provider | path | | Yes | string | +| subscription_builder_id | path | | Yes | string | -##### Parameters +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TriggerSubscriptionBuilderUpdatePayload](#triggersubscriptionbuilderupdatepayload)
| + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Success | + +### [POST] /workspaces/current/trigger-provider/{provider}/subscriptions/builder/verify-and-update/{subscription_builder_id} +**Verify and update a subscription instance for a trigger provider** + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| provider | path | | Yes | string | +| subscription_builder_id | path | | Yes | string | + +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TriggerSubscriptionBuilderVerifyPayload](#triggersubscriptionbuilderverifypayload)
| + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Success | + +### [GET] /workspaces/current/trigger-provider/{provider}/subscriptions/builder/{subscription_builder_id} +**Get a subscription instance for a trigger provider** + +#### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| provider | path | | Yes | string | +| subscription_builder_id | path | | Yes | string | + +#### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Success | + +### [GET] /workspaces/current/trigger-provider/{provider}/subscriptions/list +**List all trigger subscriptions for the current tenant's provider** + +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/trigger-provider/{provider}/subscriptions/oauth/authorize +### [GET] /workspaces/current/trigger-provider/{provider}/subscriptions/oauth/authorize +**Initiate OAuth authorization flow for a trigger provider** -#### GET -##### Summary - -Initiate OAuth authorization flow for a trigger provider - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/trigger-provider/{provider}/subscriptions/verify/{subscription_id} +### [POST] /workspaces/current/trigger-provider/{provider}/subscriptions/verify/{subscription_id} +**Verify credentials for an existing subscription (edit mode only)** -#### POST -##### Summary - -Verify credentials for an existing subscription (edit mode only) - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | provider | path | | Yes | string | | subscription_id | path | | Yes | string | -| payload | body | | Yes | [TriggerSubscriptionBuilderVerifyPayload](#triggersubscriptionbuilderverifypayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TriggerSubscriptionBuilderVerifyPayload](#triggersubscriptionbuilderverifypayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/trigger-provider/{subscription_id}/subscriptions/delete +### [POST] /workspaces/current/trigger-provider/{subscription_id}/subscriptions/delete +**Delete a subscription instance** -#### POST -##### Summary - -Delete a subscription instance - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | subscription_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| -### /workspaces/current/trigger-provider/{subscription_id}/subscriptions/update +### [POST] /workspaces/current/trigger-provider/{subscription_id}/subscriptions/update +**Update a subscription instance** -#### POST -##### Summary - -Update a subscription instance - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | subscription_id | path | | Yes | string | -| payload | body | | Yes | [TriggerSubscriptionBuilderUpdatePayload](#triggersubscriptionbuilderupdatepayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TriggerSubscriptionBuilderUpdatePayload](#triggersubscriptionbuilderupdatepayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/current/triggers +### [GET] /workspaces/current/triggers +**List all trigger providers for the current tenant** -#### GET -##### Summary - -List all trigger providers for the current tenant - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/custom-config +### [POST] /workspaces/custom-config +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [WorkspaceCustomConfigPayload](#workspacecustomconfigpayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkspaceCustomConfigPayload](#workspacecustomconfigpayload) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/custom-config/webapp-logo/upload - -#### POST -##### Responses +### [POST] /workspaces/custom-config/webapp-logo/upload +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/info +### [POST] /workspaces/info +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [WorkspaceInfoPayload](#workspaceinfopayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkspaceInfoPayload](#workspaceinfopayload) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/switch +### [POST] /workspaces/switch +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [SwitchWorkspacePayload](#switchworkspacepayload)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [SwitchWorkspacePayload](#switchworkspacepayload) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /workspaces/{tenant_id}/model-providers/{provider}/{icon_type}/{lang} - -#### GET -##### Parameters +### [GET] /workspaces/{tenant_id}/model-providers/{provider}/{icon_type}/{lang} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -11543,7 +10364,7 @@ List all trigger providers for the current tenant | provider | path | | Yes | string | | tenant_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -11553,21 +10374,17 @@ List all trigger providers for the current tenant ## default Default namespace -### /explore/banners +### [GET] /explore/banners +**Get banner list** -#### GET -##### Summary - -Get banner list - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | --- -### Models +### Schemas #### APIBasedExtensionListResponse @@ -11669,7 +10486,7 @@ Get banner list | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| interface_theme | string | *Enum:* `"dark"`, `"light"` | Yes | +| interface_theme | string,
**Available values:** "dark", "light" | *Enum:* `"dark"`, `"light"` | Yes | #### AccountNamePayload @@ -11792,7 +10609,7 @@ Get banner list | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | app_mode | string | Application mode | Yes | -| has_context | string | Whether has context | No | +| has_context | string,
**Default:** true | Whether has context | No | | model_mode | string | Model mode | Yes | | model_name | string | Model name | Yes | @@ -11848,7 +10665,7 @@ composer/publish validators and skipped by runtime request builders. | dangerous_acknowledged | boolean | | No | | dangerous_command | boolean | | No | | description | string | | No | -| enabled | boolean | | No | +| enabled | boolean,
**Default:** true | | No | | env | [AgentCliToolEnvConfig](#agentclitoolenvconfig) | | No | | id | string | | No | | install | string | | No | @@ -11931,7 +10748,7 @@ Risk marker for CLI tool bootstrap commands. | ---- | ---- | ----------- | -------- | | file_id | string | | No | | id | string | | No | -| kind | string | | No | +| kind | string,
**Default:** file | | No | | name | string | | No | | reference | string | | No | | remote_url | string | | No | @@ -11972,7 +10789,7 @@ Risk marker for CLI tool bootstrap commands. | description | string | | No | | file_id | string | | No | | id | string | | No | -| kind | string | | No | +| kind | string,
**Default:** skill | | No | | name | string | | No | | path | string | | No | @@ -12110,7 +10927,7 @@ Audit operation recorded for Agent Soul version/revision changes. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | description | string | | No | -| enabled | boolean | | No | +| enabled | boolean,
**Default:** true | | No | | name | string | | No | #### AgentIconType @@ -12166,8 +10983,8 @@ Supported icon storage formats for Agent roster entries. | ---- | ---- | ----------- | -------- | | app_id | string | Workflow app id for in-current-workflow markers | No | | keyword | string | | No | -| limit | integer | | No | -| page | integer | | No | +| limit | integer,
**Default:** 20 | | No | +| page | integer,
**Default:** 1 | | No | #### AgentInviteOptionsResponse @@ -12411,7 +11228,7 @@ Visibility and lifecycle scope of an Agent record. | model | [AgentSoulModelConfig](#agentsoulmodelconfig) | | No | | prompt | [AgentSoulPromptConfig](#agentsoulpromptconfig) | | No | | sandbox | [AgentSoulSandboxConfig](#agentsoulsandboxconfig) | | No | -| schema_version | integer | | No | +| schema_version | integer,
**Default:** 1 | | No | | skills_files | [AgentSoulSkillsFilesConfig](#agentsoulskillsfilesconfig) | | No | | tools | [AgentSoulToolsConfig](#agentsoultoolsconfig) | | No | @@ -12427,14 +11244,14 @@ new callers should send ``plugin_id`` + ``provider`` when available. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | credential_ref | [AgentSoulDifyToolCredentialRef](#agentsouldifytoolcredentialref) | | No | -| credential_type | string | *Enum:* `"api-key"`, `"oauth2"`, `"unauthorized"` | No | +| credential_type | string,
**Available values:** "api-key", "oauth2", "unauthorized",
**Default:** api-key | *Enum:* `"api-key"`, `"oauth2"`, `"unauthorized"` | No | | description | string | | No | -| enabled | boolean | | No | +| enabled | boolean,
**Default:** true | | No | | name | string | | No | | plugin_id | string | | No | | provider | string | | No | | provider_id | string | | No | -| provider_type | string | | No | +| provider_type | string,
**Default:** plugin | | No | | runtime_parameters | object | | No | | tool_name | string | | No | @@ -12450,7 +11267,7 @@ old Agent tool payloads can be read while new payloads stay explicit. | ---- | ---- | ----------- | -------- | | id | string | | No | | provider | string | | No | -| type | string | *Enum:* `"provider"`, `"tool"` | No | +| type | string,
**Available values:** "provider", "tool",
**Default:** tool | *Enum:* `"provider"`, `"tool"` | No | #### AgentSoulEnvConfig @@ -12664,8 +11481,8 @@ Soft lifecycle state for Agent records. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | keyword | string | Search keyword | No | -| limit | integer | Page size | No | -| page | integer | Page number | No | +| limit | integer,
**Default:** 20 | Page size | No | +| page | integer,
**Default:** 1 | Page number | No | #### AnnotationReplyPayload @@ -12679,7 +11496,7 @@ Soft lifecycle state for Agent records. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| action | string | *Enum:* `"disable"`, `"enable"` | Yes | +| action | string,
**Available values:** "disable", "enable" | *Enum:* `"disable"`, `"enable"` | Yes | #### AnnotationSettingUpdatePayload @@ -12897,10 +11714,10 @@ Enum class for api provider schema type. | ---- | ---- | ----------- | -------- | | creator_ids | [ string ] | Filter by creator account IDs | No | | is_created_by_me | boolean | Filter by creator | No | -| limit | integer | Page size (1-100) | No | -| mode | string | App mode filter
*Enum:* `"advanced-chat"`, `"agent"`, `"agent-chat"`, `"all"`, `"channel"`, `"chat"`, `"completion"`, `"workflow"` | No | +| limit | integer,
**Default:** 20 | Page size (1-100) | No | +| mode | string,
**Available values:** "advanced-chat", "agent", "agent-chat", "all", "channel", "chat", "completion", "workflow",
**Default:** all | App mode filter
*Enum:* `"advanced-chat"`, `"agent"`, `"agent-chat"`, `"all"`, `"channel"`, `"chat"`, `"completion"`, `"workflow"` | No | | name | string | Filter by app name | No | -| page | integer | Page number (1-99999) | No | +| page | integer,
**Default:** 1 | Page number (1-99999) | No | | tag_ids | [ string ] | Filter by tag IDs | No | #### AppMCPServerResponse @@ -13000,7 +11817,7 @@ AppMCPServer Status Enum | copyright | string | | No | | custom_disclaimer | string | | No | | customize_domain | string | | No | -| customize_token_strategy | string | *Enum:* `"allow"`, `"must"`, `"not_allow"` | No | +| customize_token_strategy | string | | No | | default_language | string | | No | | description | string | | No | | icon | string | | No | @@ -13152,12 +11969,12 @@ Button styles for user actions. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| annotation_status | string | Annotation status filter
*Enum:* `"all"`, `"annotated"`, `"not_annotated"` | No | +| annotation_status | string,
**Available values:** "all", "annotated", "not_annotated",
**Default:** all | Annotation status filter
*Enum:* `"all"`, `"annotated"`, `"not_annotated"` | No | | end | string | End date (YYYY-MM-DD HH:MM) | No | | keyword | string | Search keyword | No | -| limit | integer | Page size (1-100) | No | -| page | integer | Page number | No | -| sort_by | string | Sort field and direction
*Enum:* `"-created_at"`, `"-updated_at"`, `"created_at"`, `"updated_at"` | No | +| limit | integer,
**Default:** 20 | Page size (1-100) | No | +| page | integer,
**Default:** 1 | Page number | No | +| sort_by | string,
**Available values:** "-created_at", "-updated_at", "created_at", "updated_at",
**Default:** -updated_at | Sort field and direction
*Enum:* `"-created_at"`, `"-updated_at"`, `"created_at"`, `"updated_at"` | No | | start | string | Start date (YYYY-MM-DD HH:MM) | No | #### ChatMessagePayload @@ -13165,13 +11982,13 @@ Button styles for user actions. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | conversation_id | string | Conversation ID | No | -| files | [ ] | Uploaded files | No | +| files | [ object ] | Uploaded files | No | | inputs | object | | Yes | | model_config | object | | No | | parent_message_id | string | Parent message ID | No | | query | string | User query | Yes | -| response_mode | string | Response mode
*Enum:* `"blocking"`, `"streaming"` | No | -| retriever_from | string | Retriever source | No | +| response_mode | string,
**Available values:** "blocking", "streaming",
**Default:** blocking | Response mode
*Enum:* `"blocking"`, `"streaming"` | No | +| retriever_from | string,
**Default:** dev | Retriever source | No | #### ChatMessagesQuery @@ -13179,18 +11996,18 @@ Button styles for user actions. | ---- | ---- | ----------- | -------- | | conversation_id | string | Conversation ID | Yes | | first_id | string | First message ID for pagination | No | -| limit | integer | Number of messages to return (1-100) | No | +| limit | integer,
**Default:** 20 | Number of messages to return (1-100) | No | #### ChatRequest | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | conversation_id | string | | No | -| files | [ ] | | No | +| files | [ object ] | | No | | inputs | object | | Yes | | parent_message_id | string | | No | | query | string | | Yes | -| retriever_from | string | | No | +| retriever_from | string,
**Default:** explore_app | | No | #### CheckDependenciesResult @@ -13242,8 +12059,8 @@ Button styles for user actions. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | keyword | string | | No | -| limit | integer | | No | -| page | integer | | No | +| limit | integer,
**Default:** 20 | | No | +| page | integer,
**Default:** 1 | | No | #### ChildChunkListResponse @@ -13292,11 +12109,11 @@ Button styles for user actions. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| annotation_status | string | Annotation status filter
*Enum:* `"all"`, `"annotated"`, `"not_annotated"` | No | +| annotation_status | string,
**Available values:** "all", "annotated", "not_annotated",
**Default:** all | Annotation status filter
*Enum:* `"all"`, `"annotated"`, `"not_annotated"` | No | | end | string | End date (YYYY-MM-DD HH:MM) | No | | keyword | string | Search keyword | No | -| limit | integer | Page size (1-100) | No | -| page | integer | Page number | No | +| limit | integer,
**Default:** 20 | Page size (1-100) | No | +| page | integer,
**Default:** 1 | Page number | No | | start | string | Start date (YYYY-MM-DD HH:MM) | No | #### CompletionMessageExplorePayload @@ -13306,29 +12123,29 @@ Button styles for user actions. | files | [ object ] | | No | | inputs | object | | Yes | | query | string | | No | -| response_mode | string | *Enum:* `"blocking"`, `"streaming"` | No | -| retriever_from | string | | No | +| response_mode | string | | No | +| retriever_from | string,
**Default:** explore_app | | No | #### CompletionMessagePayload | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| files | [ ] | Uploaded files | No | +| files | [ object ] | Uploaded files | No | | inputs | object | | Yes | | model_config | object | | No | | query | string | Query text | No | -| response_mode | string | Response mode
*Enum:* `"blocking"`, `"streaming"` | No | -| retriever_from | string | Retriever source | No | +| response_mode | string,
**Available values:** "blocking", "streaming",
**Default:** blocking | Response mode
*Enum:* `"blocking"`, `"streaming"` | No | +| retriever_from | string,
**Default:** dev | Retriever source | No | #### CompletionRequest | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| files | [ ] | | No | +| files | [ object ] | | No | | inputs | object | | Yes | | query | string | | No | -| response_mode | string | *Enum:* `"blocking"`, `"streaming"` | No | -| retriever_from | string | | No | +| response_mode | string | | No | +| retriever_from | string,
**Default:** explore_app | | No | #### ComplianceDownloadQuery @@ -13341,7 +12158,7 @@ Button styles for user actions. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | agent_id | string | | No | -| binding_type | string | *Enum:* `"inline_agent"`, `"roster_agent"` | Yes | +| binding_type | string,
**Available values:** "inline_agent", "roster_agent" | *Enum:* `"inline_agent"`, `"roster_agent"` | Yes | | current_snapshot_id | string | | No | #### ComposerCandidateCapabilities @@ -13382,7 +12199,7 @@ Button styles for user actions. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| locked | boolean | | No | +| locked | boolean,
**Default:** true | | No | | unlocked_from_version_id | string | | No | #### ComposerValidationFindingsResponse @@ -13414,7 +12231,7 @@ Condition detail | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| comparison_operator | string | *Enum:* `"<"`, `"="`, `">"`, `"after"`, `"before"`, `"contains"`, `"empty"`, `"end with"`, `"in"`, `"is"`, `"is not"`, `"not contains"`, `"not empty"`, `"not in"`, `"start with"`, `"≠"`, `"≤"`, `"≥"` | Yes | +| comparison_operator | string,
**Available values:** "<", "=", ">", "after", "before", "contains", "empty", "end with", "in", "is", "is not", "not contains", "not empty", "not in", "start with", "≠", "≤", "≥" | *Enum:* `"<"`, `"="`, `">"`, `"after"`, `"before"`, `"contains"`, `"empty"`, `"end with"`, `"in"`, `"is"`, `"is not"`, `"not contains"`, `"not empty"`, `"not in"`, `"start with"`, `"≠"`, `"≤"`, `"≥"` | Yes | | name | string | | Yes | | value | string
[ string ]
integer
number | | No | @@ -13425,8 +12242,8 @@ Condition detail | ids | [ string ] | Filter by dataset IDs | No | | include_all | boolean | Include all datasets | No | | keyword | string | Search keyword | No | -| limit | integer | Number of items per page | No | -| page | integer | Page number | No | +| limit | integer,
**Default:** 20 | Number of items per page | No | +| page | integer,
**Default:** 1 | Page number | No | | tag_ids | [ string ] | Filter by tag IDs | No | #### ConsoleSegmentListResponse @@ -13500,7 +12317,7 @@ Condition detail | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | last_id | string | | No | -| limit | integer | | No | +| limit | integer,
**Default:** 20 | | No | | pinned | boolean | | No | #### ConversationMessageDetail @@ -13627,7 +12444,7 @@ Condition detail | icon | string | Icon | No | | icon_background | string | Icon background color | No | | icon_type | [IconType](#icontype) | Icon type | No | -| mode | string | App mode
*Enum:* `"advanced-chat"`, `"agent"`, `"agent-chat"`, `"chat"`, `"completion"`, `"workflow"` | Yes | +| mode | string,
**Available values:** "advanced-chat", "agent", "agent-chat", "chat", "completion", "workflow" | App mode
*Enum:* `"advanced-chat"`, `"agent"`, `"agent-chat"`, `"chat"`, `"completion"`, `"workflow"` | Yes | | name | string | App name | Yes | #### CreateSnippetPayload @@ -13641,7 +12458,7 @@ Payload for creating a new snippet. | icon_info | [IconInfo](#iconinfo) | | No | | input_fields | [ [InputFieldDefinition](#inputfielddefinition) ] | | No | | name | string | | Yes | -| type | string | *Enum:* `"group"`, `"node"` | No | +| type | string,
**Available values:** "group", "node",
**Default:** node | *Enum:* `"group"`, `"node"` | No | #### CredentialType @@ -13727,7 +12544,7 @@ Payload for creating a new snippet. | indexing_technique | string | | No | | name | string | | Yes | | permission | [PermissionEnum](#permissionenum) | | No | -| provider | string | | No | +| provider | string,
**Default:** vendor | | No | #### DatasetDetail @@ -14270,7 +13087,7 @@ code can call ``output.failure_strategy.on_failure`` without None-guards. | file | [DeclaredOutputFileConfig](#declaredoutputfileconfig) | | No | | id | string | | No | | name | string | | Yes | -| required | boolean | | No | +| required | boolean,
**Default:** true | | No | | type | [DeclaredOutputType](#declaredoutputtype) | | Yes | #### DeclaredOutputFailureStrategy @@ -14522,7 +13339,7 @@ Request payload for bulk downloading documents as a zip archive. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | keywords | string | | Yes | -| limit | integer | | No | +| limit | integer,
**Default:** 20 | | No | | page | integer | | No | #### EducationAutocompleteResponse @@ -14693,8 +13510,8 @@ Request payload for bulk downloading documents as a zip archive. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | keyword | string | Search keyword | No | -| limit | integer | Number of items per page | No | -| page | integer | Page number | No | +| limit | integer,
**Default:** 20 | Number of items per page | No | +| page | integer,
**Default:** 1 | Page number | No | #### ExternalDatasetCreatePayload @@ -14748,13 +13565,13 @@ Request payload for bulk downloading documents as a zip archive. | billing | [BillingModel](#billingmodel) | | Yes | | can_replace_logo | boolean | | Yes | | dataset_operator_enabled | boolean | | Yes | -| docs_processing | string | | Yes | +| docs_processing | string,
**Default:** standard | | Yes | | documents_upload_quota | [LimitationModel](#limitationmodel) | | Yes | | education | [EducationModel](#educationmodel) | | Yes | | human_input_email_delivery_enabled | boolean | | Yes | -| is_allow_transfer_workspace | boolean | | Yes | +| is_allow_transfer_workspace | boolean,
**Default:** true | | Yes | | knowledge_pipeline | [KnowledgePipeline](#knowledgepipeline) | | Yes | -| knowledge_rate_limit | integer | | Yes | +| knowledge_rate_limit | integer,
**Default:** 10 | | Yes | | members | [LimitationModel](#limitationmodel) | | Yes | | model_load_balancing_enabled | boolean | | Yes | | next_credit_reset_date | integer | | Yes | @@ -14778,10 +13595,10 @@ Request payload for bulk downloading documents as a zip archive. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | end_date | string | End date (YYYY-MM-DD) | No | -| format | string | Export format
*Enum:* `"csv"`, `"json"` | No | -| from_source | string | Filter by feedback source
*Enum:* `"admin"`, `"user"` | No | +| format | string,
**Available values:** "csv", "json",
**Default:** csv | Export format
*Enum:* `"csv"`, `"json"` | No | +| from_source | string | Filter by feedback source | No | | has_comment | boolean | Only include feedback with comments | No | -| rating | string | Filter by rating
*Enum:* `"dislike"`, `"like"` | No | +| rating | string | Filter by rating | No | | start_date | string | Start date (YYYY-MM-DD) | No | #### FeedbackStat @@ -15093,7 +13910,7 @@ Icon information model. | ---- | ---- | ----------- | -------- | | icon | string | | No | | icon_background | string | | No | -| icon_type | string | *Enum:* `"emoji"`, `"image"` | No | +| icon_type | string | | No | | icon_url | string | | No | #### IconType @@ -15116,7 +13933,7 @@ How Dify forwards the end-user's identity to an MCP server. | ---- | ---- | ----------- | -------- | | app_id | string | | No | | app_mode | string | | No | -| current_dsl_version | string | | No | +| current_dsl_version | string,
**Default:** 0.6.0 | | No | | error | string | | No | | id | string | | Yes | | imported_dsl_version | string | | No | @@ -15134,7 +13951,7 @@ Query parameter for including secret variables in export. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| include_secret | string | Whether to include secret variables | No | +| include_secret | string,
**Default:** false | Whether to include secret variables | No | #### IndexingEstimate @@ -15149,8 +13966,8 @@ Query parameter for including secret variables in export. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | dataset_id | string | | No | -| doc_form | string | | No | -| doc_language | string | | No | +| doc_form | string,
**Default:** text_model | | No | +| doc_language | string,
**Default:** English | | No | | indexing_technique | string | | Yes | | info_list | object | | Yes | | process_rule | object | | Yes | @@ -15182,7 +13999,7 @@ Query parameter for including secret variables in export. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| data_source_type | string | *Enum:* `"notion_import"`, `"upload_file"`, `"website_crawl"` | Yes | +| data_source_type | string,
**Available values:** "notion_import", "upload_file", "website_crawl" | *Enum:* `"notion_import"`, `"upload_file"`, `"website_crawl"` | Yes | | file_info_list | [FileInfo](#fileinfo) | | No | | notion_info_list | [ [NotionInfo](#notioninfo) ] | | No | | website_info_list | [WebsiteInfo](#websiteinfo) | | No | @@ -15272,7 +14089,7 @@ Input field definition for snippet parameters. | flow_id | string | Workflow/Flow ID | Yes | | ideal_output | string | Expected ideal output | No | | instruction | string | Instruction for generation | Yes | -| language | string | Programming language (javascript/python) | No | +| language | string,
**Default:** javascript | Programming language (javascript/python) | No | | model_config | [ModelConfig](#modelconfig) | Model configuration | Yes | | node_id | string | Node ID for workflow context | No | @@ -15305,12 +14122,12 @@ Input field definition for snippet parameters. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | data_source | [DataSource](#datasource) | | No | -| doc_form | string | | No | -| doc_language | string | | No | -| duplicate | boolean | | No | +| doc_form | string,
**Default:** text_model | | No | +| doc_language | string,
**Default:** English | | No | +| duplicate | boolean,
**Default:** true | | No | | embedding_model | string | | No | | embedding_model_provider | string | | No | -| indexing_technique | string | *Enum:* `"economy"`, `"high_quality"` | Yes | +| indexing_technique | string,
**Available values:** "economy", "high_quality" | *Enum:* `"economy"`, `"high_quality"` | Yes | | is_multimodal | boolean | | No | | name | string | | No | | original_document_id | string | | No | @@ -15547,7 +14364,7 @@ Enum class for large language model mode. | ---- | ---- | ----------- | -------- | | content | string | | No | | message_id | string | Message ID | Yes | -| rating | string | *Enum:* `"dislike"`, `"like"` | No | +| rating | string | | No | #### MessageFile @@ -15577,14 +14394,14 @@ Enum class for large language model mode. | ---- | ---- | ----------- | -------- | | conversation_id | string | Conversation UUID | Yes | | first_id | string | First message ID for pagination | No | -| limit | integer | Number of messages to return (1-100) | No | +| limit | integer,
**Default:** 20 | Number of messages to return (1-100) | No | #### MetadataArgs | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | name | string | | Yes | -| type | string | *Enum:* `"number"`, `"string"`, `"time"` | Yes | +| type | string,
**Available values:** "number", "string", "time" | *Enum:* `"number"`, `"string"`, `"time"` | Yes | #### MetadataDetail @@ -15601,7 +14418,7 @@ Metadata Filtering Condition. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | conditions | [ [Condition](#condition) ] | | No | -| logical_operator | string | *Enum:* `"and"`, `"or"` | No | +| logical_operator | string | | No | #### MetadataOperationData @@ -15666,7 +14483,7 @@ Enum class for model type. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| response_mode | string | *Enum:* `"blocking"`, `"streaming"` | Yes | +| response_mode | string,
**Available values:** "blocking", "streaming" | *Enum:* `"blocking"`, `"streaming"` | Yes | #### NewAppResponse @@ -15704,11 +14521,11 @@ Lifecycle status of a single declared output within a run. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| node_completed_at | dateTime | | No | +| node_completed_at | string | | No | | node_display_name | string | | Yes | | node_id | string | | Yes | | node_kind | string | | Yes | -| node_started_at | dateTime | | No | +| node_started_at | string | | No | | node_status | [NodeStatus](#nodestatus) | | Yes | | outputs | [ [NodeOutputView](#nodeoutputview) ] | | No | @@ -15736,8 +14553,8 @@ Coarse node-level status used by Inspector to pick a banner. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| doc_form | string | | No | -| doc_language | string | | No | +| doc_form | string,
**Default:** text_model | | No | +| doc_language | string,
**Default:** English | | No | | notion_info_list | [ object ] | | Yes | | process_rule | object | | Yes | @@ -15965,7 +14782,7 @@ Form input definition. | parameter | string | | Yes | | plugin_id | string | | Yes | | provider | string | | Yes | -| provider_type | string | *Enum:* `"tool"`, `"trigger"` | Yes | +| provider_type | string,
**Available values:** "tool", "trigger" | *Enum:* `"tool"`, `"trigger"` | Yes | #### ParserDynamicOptionsWithCredentials @@ -16050,8 +14867,8 @@ Form input definition. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| page | integer | Page number | No | -| page_size | integer | Page size (1-256) | No | +| page | integer,
**Default:** 1 | Page number | No | +| page_size | integer,
**Default:** 256 | Page size (1-256) | No | #### ParserMarketplaceUpgrade @@ -16118,13 +14935,13 @@ Form input definition. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| preferred_provider_type | string | *Enum:* `"custom"`, `"system"` | Yes | +| preferred_provider_type | string,
**Available values:** "custom", "system" | *Enum:* `"custom"`, `"system"` | Yes | #### ParserReadme | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| language | string | | No | +| language | string,
**Default:** en-US | | No | | plugin_unique_identifier | string | | Yes | #### ParserSwitch @@ -16139,8 +14956,8 @@ Form input definition. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| page | integer | Page number | No | -| page_size | integer | Page size (1-256) | No | +| page | integer,
**Default:** 1 | Page number | No | +| page_size | integer,
**Default:** 256 | Page size (1-256) | No | #### ParserUninstall @@ -16198,7 +15015,7 @@ Shared permission levels for resources (datasets, credentials, etc.) | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| type | string | Template source: built-in or customized | No | +| type | string,
**Default:** built-in | Template source: built-in or customized | No | #### PipelineTemplateDetailResponse @@ -16230,8 +15047,8 @@ Shared permission levels for resources (datasets, credentials, etc.) | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| language | string | Template language | No | -| type | string | Template source: built-in or customized | No | +| language | string,
**Default:** en-US | Template language | No | +| type | string,
**Default:** built-in | Template source: built-in or customized | No | #### PipelineTemplateListResponse @@ -16363,7 +15180,7 @@ Payload for publishing snippet workflow. | inputs | object | | Yes | | is_preview | boolean | | No | | original_document_id | string | | No | -| response_mode | string | *Enum:* `"blocking"`, `"streaming"` | No | +| response_mode | string,
**Available values:** "blocking", "streaming",
**Default:** streaming | *Enum:* `"blocking"`, `"streaming"` | No | | start_node_id | string | | Yes | #### QAPreviewDetail @@ -16378,7 +15195,7 @@ Payload for publishing snippet workflow. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | limit | integer | | Yes | -| reset_date | integer | | Yes | +| reset_date | integer,
**Default:** -1 | | Yes | | usage | integer | | Yes | #### RagPipelineDatasetImportPayload @@ -16423,7 +15240,7 @@ Payload for publishing snippet workflow. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| type | string | | No | +| type | string,
**Default:** all | | No | #### RagPipelineWorkflowPublishResponse @@ -16586,14 +15403,14 @@ Payload for publishing snippet workflow. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | keyword | string | | No | -| limit | integer | | No | -| page | integer | | No | +| limit | integer,
**Default:** 20 | | No | +| page | integer,
**Default:** 1 | | No | #### Rule | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| parent_mode | string | *Enum:* `"full-doc"`, `"paragraph"` | No | +| parent_mode | string | | No | | pre_processing_rules | [ [PreProcessingRule](#preprocessingrule) ] | | No | | segmentation | [Segmentation](#segmentation) | | No | | subchunk_segmentation | [Segmentation](#segmentation) | | No | @@ -16602,7 +15419,7 @@ Payload for publishing snippet workflow. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| code_language | string | Programming language for code generation | No | +| code_language | string,
**Default:** javascript | Programming language for code generation | No | | instruction | string | Rule generation instruction | Yes | | model_config | [ModelConfig](#modelconfig) | Model configuration | Yes | | no_variable | boolean | Whether to exclude variables | No | @@ -16629,7 +15446,7 @@ Payload for publishing snippet workflow. | mtime | integer | | No | | name | string | | Yes | | size | integer | | No | -| type | string | *Enum:* `"dir"`, `"file"`, `"other"`, `"symlink"` | Yes | +| type | string,
**Available values:** "dir", "file", "other", "symlink" | *Enum:* `"dir"`, `"file"`, `"other"`, `"symlink"` | Yes | #### SandboxListResponse @@ -16654,7 +15471,7 @@ Payload for publishing snippet workflow. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | reference | string | | Yes | -| transfer_method | string | | No | +| transfer_method | string,
**Default:** tool_file | | No | #### SandboxUploadResponse @@ -16674,7 +15491,7 @@ Payload for publishing snippet workflow. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | last_id | string | | No | -| limit | integer | | No | +| limit | integer,
**Default:** 20 | | No | #### SegmentAttachmentResponse @@ -16720,11 +15537,11 @@ Payload for publishing snippet workflow. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| enabled | string | | No | +| enabled | string,
**Default:** all | | No | | hit_count_gte | integer | | No | | keyword | string | | No | -| limit | integer | | No | -| page | integer | | No | +| limit | integer,
**Default:** 20 | | No | +| page | integer,
**Default:** 1 | | No | | status | [ string ] | | No | #### SegmentResponse @@ -16776,7 +15593,8 @@ Payload for publishing snippet workflow. | ---- | ---- | ----------- | -------- | | chunk_overlap | integer | | No | | max_tokens | integer | | Yes | -| separator | string | | No | +| separator | string,
**Default:** + | | No | #### SelectInputConfig @@ -16984,8 +15802,8 @@ Query parameters for listing snippets. | creators | [ string ] | Filter by creator account IDs | No | | is_published | boolean | Filter by published status | No | | keyword | string | | No | -| limit | integer | | No | -| page | integer | | No | +| limit | integer,
**Default:** 20 | | No | +| page | integer,
**Default:** 1 | | No | | tag_ids | [ string ] | Filter by tag IDs | No | #### SnippetLoopNodeRunPayload @@ -17012,8 +15830,8 @@ Query parameters for listing snippet published workflows. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| limit | integer | | No | -| page | integer | | No | +| limit | integer,
**Default:** 10 | | No | +| page | integer,
**Default:** 1 | | No | #### SnippetWorkflowResponse @@ -17081,14 +15899,14 @@ Default configuration for form inputs. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | interval | string | | Yes | -| plan | string | | Yes | +| plan | string,
**Default:** sandbox | | Yes | #### SubscriptionQuery | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| interval | string | Billing interval
*Enum:* `"month"`, `"year"` | Yes | -| plan | string | Subscription plan
*Enum:* `"professional"`, `"team"` | Yes | +| interval | string,
**Available values:** "month", "year" | Billing interval
*Enum:* `"month"`, `"year"` | Yes | +| plan | string,
**Available values:** "professional", "team" | Subscription plan
*Enum:* `"professional"`, `"team"` | Yes | #### SuccessResponse @@ -17131,11 +15949,11 @@ Default configuration for form inputs. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | branding | [BrandingModel](#brandingmodel) | | Yes | -| enable_change_email | boolean | | Yes | -| enable_collaboration_mode | boolean | | Yes | +| enable_change_email | boolean,
**Default:** true | | Yes | +| enable_collaboration_mode | boolean,
**Default:** true | | Yes | | enable_creators_platform | boolean | | Yes | | enable_email_code_login | boolean | | Yes | -| enable_email_password_login | boolean | | Yes | +| enable_email_password_login | boolean,
**Default:** true | | Yes | | enable_explore_banner | boolean | | Yes | | enable_marketplace | boolean | | Yes | | enable_social_oauth_login | boolean | | Yes | @@ -17144,7 +15962,7 @@ Default configuration for form inputs. | is_allow_register | boolean | | Yes | | is_email_setup | boolean | | Yes | | license | [LicenseModel](#licensemodel) | | Yes | -| max_plugin_package_size | integer | | Yes | +| max_plugin_package_size | integer,
**Default:** 15728640 | | Yes | | plugin_installation_permission | [PluginInstallationPermissionModel](#plugininstallationpermissionmodel) | | Yes | | plugin_manager | [PluginManagerModel](#pluginmanagermodel) | | Yes | | sso_enforced_for_signin | boolean | | Yes | @@ -17187,7 +16005,7 @@ Default configuration for form inputs. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | keyword | string | Search keyword | No | -| type | string | Tag type filter
*Enum:* `""`, `"app"`, `"knowledge"`, `"snippet"` | No | +| type | string,
**Available values:** "", "app", "knowledge", "snippet" | Tag type filter
*Enum:* `""`, `"app"`, `"knowledge"`, `"snippet"` | No | #### TagResponse @@ -17486,7 +16304,7 @@ Tag type | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| credential_type | string | | No | +| credential_type | string,
**Default:** unauthorized | | No | #### TriggerSubscriptionBuilderUpdatePayload @@ -17627,7 +16445,7 @@ in form definiton, or a variable while the workflow is running. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| created_at | dateTime | | No | +| created_at | string | | No | | id | string | | Yes | | node_id | string | | Yes | | webhook_debug_url | string | | Yes | @@ -17639,21 +16457,21 @@ in form definiton, or a variable while the workflow is running. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | options | object | | Yes | -| provider | string | *Enum:* `"firecrawl"`, `"jinareader"`, `"watercrawl"` | Yes | +| provider | string,
**Available values:** "firecrawl", "jinareader", "watercrawl" | *Enum:* `"firecrawl"`, `"jinareader"`, `"watercrawl"` | Yes | | url | string | | Yes | #### WebsiteCrawlStatusQuery | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| provider | string | *Enum:* `"firecrawl"`, `"jinareader"`, `"watercrawl"` | Yes | +| provider | string,
**Available values:** "firecrawl", "jinareader", "watercrawl" | *Enum:* `"firecrawl"`, `"jinareader"`, `"watercrawl"` | Yes | #### WebsiteInfo | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | job_id | string | | Yes | -| only_main_content | boolean | | No | +| only_main_content | boolean,
**Default:** true | | No | | provider | string | | Yes | | urls | [ string ] | | Yes | @@ -17669,7 +16487,7 @@ in form definiton, or a variable while the workflow is running. | ---- | ---- | ----------- | -------- | | keyword_setting | [WeightKeywordSetting](#weightkeywordsetting) | | No | | vector_setting | [WeightVectorSetting](#weightvectorsetting) | | No | -| weight_type | string | *Enum:* `"customized"`, `"keyword_first"`, `"semantic_first"` | No | +| weight_type | string | | No | #### WeightVectorSetting @@ -17740,14 +16558,14 @@ How a workflow node is bound to an Agent. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| created_at__after | dateTime | Filter logs created after this timestamp | No | -| created_at__before | dateTime | Filter logs created before this timestamp | No | +| created_at__after | string | Filter logs created after this timestamp | No | +| created_at__before | string | Filter logs created before this timestamp | No | | created_by_account | string | Filter by account | No | | created_by_end_user_session_id | string | Filter by end user session ID | No | | detail | boolean | Whether to return detailed logs | No | | keyword | string | Search keyword for filtering logs | No | -| limit | integer | Number of items per page (1-100) | No | -| page | integer | Page number (1-99999) | No | +| limit | integer,
**Default:** 20 | Number of items per page (1-100) | No | +| page | integer,
**Default:** 1 | Page number (1-99999) | No | | status | [WorkflowExecutionStatus](#workflowexecutionstatus) | Execution status filter (succeeded, failed, stopped, partial-succeeded) | No | #### WorkflowArchivedLogPaginationResponse @@ -17966,8 +16784,8 @@ How a workflow node is bound to an Agent. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| limit | integer | Items per page | No | -| page | integer | Page number | No | +| limit | integer,
**Default:** 20 | Items per page | No | +| page | integer,
**Default:** 1 | Page number | No | #### WorkflowDraftVariableListWithoutValue @@ -18039,16 +16857,16 @@ can reuse its existing handler. | current_graph | object | Existing draft graph to refine (cmd+k `/refine`); omit for create-from-scratch | No | | ideal_output | string | Optional sample output for grounding | No | | instruction | string | Natural-language workflow description | Yes | -| mode | string | Target app mode for the generated graph
*Enum:* `"advanced-chat"`, `"workflow"` | Yes | +| mode | string,
**Available values:** "advanced-chat", "workflow" | Target app mode for the generated graph
*Enum:* `"advanced-chat"`, `"workflow"` | Yes | | model_config | [ModelConfig](#modelconfig) | Model configuration | Yes | #### WorkflowListQuery | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| limit | integer | | No | +| limit | integer,
**Default:** 10 | | No | | named_only | boolean | | No | -| page | integer | | No | +| page | integer,
**Default:** 1 | | No | | user_id | string | | No | #### WorkflowNodeJobConfig @@ -18060,7 +16878,7 @@ can reuse its existing handler. | metadata | [WorkflowNodeJobMetadata](#workflownodejobmetadata) | | No | | mode | [WorkflowNodeJobMode](#workflownodejobmode) | | No | | previous_node_output_refs | [ [WorkflowPreviousNodeOutputRef](#workflowpreviousnodeoutputref) ] | | No | -| schema_version | integer | | No | +| schema_version | integer,
**Default:** 1 | | No | | workflow_prompt | string | | No | #### WorkflowNodeJobMetadata @@ -18137,10 +16955,10 @@ can reuse its existing handler. | name | string | | No | | node_id | string | | No | | output | string | | No | -| selector | [ ] | | No | -| value_selector | [ ] | | No | +| selector | [ string
integer
number
boolean ] | | No | +| value_selector | [ string
integer
number
boolean ] | | No | | variable | string | | No | -| variable_selector | [ ] | | No | +| variable_selector | [ string
integer
number
boolean ] | | No | #### WorkflowResponse @@ -18166,9 +16984,9 @@ can reuse its existing handler. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| status | string | Workflow run status filter
*Enum:* `"failed"`, `"partial-succeeded"`, `"running"`, `"stopped"`, `"succeeded"` | No | +| status | string | Workflow run status filter | No | | time_range | string | Filter by time range (optional): e.g., 7d (7 days), 4h (4 hours), 30m (30 minutes), 30s (30 seconds). Filters by created_at field. | No | -| triggered_from | string | Filter by trigger source: debugging or app-run. Default: debugging
*Enum:* `"app-run"`, `"debugging"` | No | +| triggered_from | string | Filter by trigger source: debugging or app-run. Default: debugging | No | #### WorkflowRunCountResponse @@ -18257,9 +17075,9 @@ can reuse its existing handler. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | last_id | string | Last run ID for pagination | No | -| limit | integer | Number of items per page (1-100) | No | -| status | string | Workflow run status filter
*Enum:* `"failed"`, `"partial-succeeded"`, `"running"`, `"stopped"`, `"succeeded"` | No | -| triggered_from | string | Filter by trigger source: debugging or app-run. Default: debugging
*Enum:* `"app-run"`, `"debugging"` | No | +| limit | integer,
**Default:** 20 | Number of items per page (1-100) | No | +| status | string | Workflow run status filter | No | +| triggered_from | string | Filter by trigger source: debugging or app-run. Default: debugging | No | #### WorkflowRunNodeExecutionListResponse @@ -18316,13 +17134,13 @@ Query parameters for workflow runs. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | last_id | string | | No | -| limit | integer | | No | +| limit | integer,
**Default:** 20 | | No | #### WorkflowRunRequest | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| files | [ ] | | No | +| files | [ object ] | | No | | inputs | object | | Yes | #### WorkflowRunSnapshotView @@ -18392,7 +17210,7 @@ Workflow tool configuration | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| created_at | dateTime | | No | +| created_at | string | | No | | icon | string | | Yes | | id | string | | Yes | | node_id | string | | Yes | @@ -18400,7 +17218,7 @@ Workflow tool configuration | status | string | | Yes | | title | string | | Yes | | trigger_type | string | | Yes | -| updated_at | dateTime | | No | +| updated_at | string | | No | #### WorkflowUpdatePayload @@ -18433,8 +17251,8 @@ Workflow tool configuration | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| limit | integer | | No | -| page | integer | | No | +| limit | integer,
**Default:** 20 | | No | +| page | integer,
**Default:** 1 | | No | #### WorkspacePermissionResponse @@ -18488,7 +17306,7 @@ Workflow tool configuration | model_provider_name | string | | No | | summary_prompt | string | | No | -## FastOpenAPI Preview (OpenAPI 3.0) +## FastOpenAPI Preview (OpenAPI 3.1) ### Dify API (FastOpenAPI PoC) FastOpenAPI proof of concept for Dify API @@ -18620,7 +17438,7 @@ FastOpenAPI proof of concept for Dify API | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | email | string | Admin email address | Yes | -| language | | Admin language | No | +| language | string | Admin language | No | | name | string | Admin name (max 30 characters) | Yes | | password | string | Admin password | Yes | @@ -18634,7 +17452,7 @@ FastOpenAPI proof of concept for Dify API | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| setup_at | | Setup completion time (ISO format) | No | +| setup_at | string | Setup completion time (ISO format) | No | | step | string,
**Available values:** "finished", "not_started" | Setup step status
*Enum:* `"finished"`, `"not_started"` | Yes | ###### VersionFeatures diff --git a/api/openapi/markdown/openapi-swagger.md b/api/openapi/markdown/openapi-openapi.md similarity index 66% rename from api/openapi/markdown/openapi-swagger.md rename to api/openapi/markdown/openapi-openapi.md index 451be85a1c3..f0394f6cc56 100644 --- a/api/openapi/markdown/openapi-swagger.md +++ b/api/openapi/markdown/openapi-openapi.md @@ -3,523 +3,485 @@ User-scoped programmatic API (bearer auth) ## Version: 1.0 -### Security -**Bearer** - -| apiKey | *API Key* | -| ------ | --------- | -| Description | Type: Bearer {your-api-key} | -| In | header | -| Name | Authorization | +### Available authorizations +#### Bearer (API Key Authentication) +Type: Bearer {your-api-key} +**Name:** Authorization +**In:** header --- ## openapi User-scoped operations -### /_health - -#### GET -##### Responses +### [GET] /_health +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Health check | [HealthResponse](#healthresponse) | -| default | Error | [ErrorBody](#errorbody) | +| 200 | Health check | **application/json**: [HealthResponse](#healthresponse)
| +| default | Error | **application/json**: [ErrorBody](#errorbody)
| -### /_version - -#### GET -##### Responses +### [GET] /_version +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Server version | [ServerVersionResponse](#serverversionresponse) | -| default | Error | [ErrorBody](#errorbody) | +| 200 | Server version | **application/json**: [ServerVersionResponse](#serverversionresponse)
| +| default | Error | **application/json**: [ErrorBody](#errorbody)
| -### /account - -#### GET -##### Responses +### [GET] /account +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Account info | [AccountResponse](#accountresponse) | -| default | Error | [ErrorBody](#errorbody) | +| 200 | Account info | **application/json**: [AccountResponse](#accountresponse)
| +| default | Error | **application/json**: [ErrorBody](#errorbody)
| -### /account/sessions - -#### GET -##### Parameters +### [GET] /account/sessions +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| limit | query | | No | integer | -| page | query | | No | integer | +| limit | query | | No | integer,
**Default:** 100 | +| page | query | | No | integer,
**Default:** 1 | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Session list | [SessionListResponse](#sessionlistresponse) | -| 422 | Validation error | [ErrorBody](#errorbody) | -| default | Error | [ErrorBody](#errorbody) | +| 200 | Session list | **application/json**: [SessionListResponse](#sessionlistresponse)
| +| 422 | Validation error | **application/json**: [ErrorBody](#errorbody)
| +| default | Error | **application/json**: [ErrorBody](#errorbody)
| -### /account/sessions/self - -#### DELETE -##### Responses +### [DELETE] /account/sessions/self +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Session revoked | [RevokeResponse](#revokeresponse) | -| default | Error | [ErrorBody](#errorbody) | +| 200 | Session revoked | **application/json**: [RevokeResponse](#revokeresponse)
| +| default | Error | **application/json**: [ErrorBody](#errorbody)
| -### /account/sessions/{session_id} - -#### DELETE -##### Parameters +### [DELETE] /account/sessions/{session_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | session_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Session revoked | [RevokeResponse](#revokeresponse) | -| default | Error | [ErrorBody](#errorbody) | +| 200 | Session revoked | **application/json**: [RevokeResponse](#revokeresponse)
| +| default | Error | **application/json**: [ErrorBody](#errorbody)
| -### /apps - -#### GET -##### Parameters +### [GET] /apps +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| limit | query | | No | integer | +| limit | query | | No | integer,
**Default:** 20 | | mode | query | | No | string | | name | query | | No | string | -| page | query | | No | integer | +| page | query | | No | integer,
**Default:** 1 | | tag | query | | No | string | | workspace_id | query | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | App list | [AppListResponse](#applistresponse) | -| 422 | Validation error | [ErrorBody](#errorbody) | -| default | Error | [ErrorBody](#errorbody) | +| 200 | App list | **application/json**: [AppListResponse](#applistresponse)
| +| 422 | Validation error | **application/json**: [ErrorBody](#errorbody)
| +| default | Error | **application/json**: [ErrorBody](#errorbody)
| -### /apps/{app_id}/check-dependencies - -#### GET -##### Parameters +### [GET] /apps/{app_id}/check-dependencies +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Dependencies checked | [CheckDependenciesResult](#checkdependenciesresult) | -| default | Error | [ErrorBody](#errorbody) | +| 200 | Dependencies checked | **application/json**: [CheckDependenciesResult](#checkdependenciesresult)
| +| default | Error | **application/json**: [ErrorBody](#errorbody)
| -### /apps/{app_id}/describe - -#### GET -##### Parameters +### [GET] /apps/{app_id}/describe +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | | fields | query | | No | string | +| app_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | App description | [AppDescribeResponse](#appdescriberesponse) | -| 422 | Validation error | [ErrorBody](#errorbody) | -| default | Error | [ErrorBody](#errorbody) | +| 200 | App description | **application/json**: [AppDescribeResponse](#appdescriberesponse)
| +| 422 | Validation error | **application/json**: [ErrorBody](#errorbody)
| +| default | Error | **application/json**: [ErrorBody](#errorbody)
| -### /apps/{app_id}/export - -#### GET -##### Parameters +### [GET] /apps/{app_id}/export +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | | Yes | string | | include_secret | query | Include encrypted secret values in the exported DSL | No | boolean | | workflow_id | query | Export a specific workflow version instead of the current draft | No | string | +| app_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Export successful | [AppDslExportResponse](#appdslexportresponse) | -| 422 | Validation error | [ErrorBody](#errorbody) | -| default | Error | [ErrorBody](#errorbody) | - -### /apps/{app_id}/files/upload - -#### POST -##### Description +| 200 | Export successful | **application/json**: [AppDslExportResponse](#appdslexportresponse)
| +| 422 | Validation error | **application/json**: [ErrorBody](#errorbody)
| +| default | Error | **application/json**: [ErrorBody](#errorbody)
| +### [POST] /apps/{app_id}/files/upload Upload a file to use as an input variable when running the app -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | File uploaded successfully | [FileResponse](#fileresponse) | +| 201 | File uploaded successfully | **application/json**: [FileResponse](#fileresponse)
| | 400 | Bad request — no file or filename missing | | | 401 | Unauthorized — invalid or expired bearer token | | | 413 | File too large | | | 415 | Unsupported file type or blocked extension | | -| default | Error | [ErrorBody](#errorbody) | +| default | Error | **application/json**: [ErrorBody](#errorbody)
| -### /apps/{app_id}/form/human_input/{form_token} - -#### GET -##### Parameters +### [GET] /apps/{app_id}/form/human_input/{form_token} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | | form_token | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Form definition | -#### POST -##### Parameters +### [POST] /apps/{app_id}/form/human_input/{form_token} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | | form_token | path | | Yes | string | -| payload | body | | Yes | [HumanInputFormSubmitPayload](#humaninputformsubmitpayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [HumanInputFormSubmitPayload](#humaninputformsubmitpayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Form submitted | [FormSubmitResponse](#formsubmitresponse) | -| 422 | Validation error | [ErrorBody](#errorbody) | -| default | Error | [ErrorBody](#errorbody) | +| 200 | Form submitted | **application/json**: [FormSubmitResponse](#formsubmitresponse)
| +| 422 | Validation error | **application/json**: [ErrorBody](#errorbody)
| +| default | Error | **application/json**: [ErrorBody](#errorbody)
| -### /apps/{app_id}/run - -#### POST -##### Parameters +### [POST] /apps/{app_id}/run +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | -| payload | body | | Yes | [AppRunRequest](#apprunrequest) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AppRunRequest](#apprunrequest)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | | 200 | Run result (SSE stream) | | -| 422 | Validation error | [ErrorBody](#errorbody) | +| 422 | Validation error | **application/json**: [ErrorBody](#errorbody)
| -### /apps/{app_id}/tasks/{task_id}/events - -#### GET -##### Parameters +### [GET] /apps/{app_id}/tasks/{task_id}/events +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | | task_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | SSE event stream | -### /apps/{app_id}/tasks/{task_id}/stop - -#### POST -##### Parameters +### [POST] /apps/{app_id}/tasks/{task_id}/stop +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | | task_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Task stopped | [TaskStopResponse](#taskstopresponse) | -| default | Error | [ErrorBody](#errorbody) | +| 200 | Task stopped | **application/json**: [TaskStopResponse](#taskstopresponse)
| +| default | Error | **application/json**: [ErrorBody](#errorbody)
| -### /oauth/device/approve +### [POST] /oauth/device/approve +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DeviceMutateRequest](#devicemutaterequest)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [DeviceMutateRequest](#devicemutaterequest) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Approved | [DeviceMutateResponse](#devicemutateresponse) | +| 200 | Approved | **application/json**: [DeviceMutateResponse](#devicemutateresponse)
| -### /oauth/device/code +### [POST] /oauth/device/code +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DeviceCodeRequest](#devicecoderequest)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [DeviceCodeRequest](#devicecoderequest) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Device code created | [DeviceCodeResponse](#devicecoderesponse) | +| 200 | Device code created | **application/json**: [DeviceCodeResponse](#devicecoderesponse)
| -### /oauth/device/deny +### [POST] /oauth/device/deny +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DeviceMutateRequest](#devicemutaterequest)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [DeviceMutateRequest](#devicemutaterequest) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Denied | [DeviceMutateResponse](#devicemutateresponse) | +| 200 | Denied | **application/json**: [DeviceMutateResponse](#devicemutateresponse)
| -### /oauth/device/lookup - -#### GET -##### Parameters +### [GET] /oauth/device/lookup +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | user_code | query | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Device lookup result | [DeviceLookupResponse](#devicelookupresponse) | +| 200 | Device lookup result | **application/json**: [DeviceLookupResponse](#devicelookupresponse)
| -### /oauth/device/token +### [POST] /oauth/device/token +#### Request Body -#### POST -##### Parameters +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DevicePollRequest](#devicepollrequest)
| -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [DevicePollRequest](#devicepollrequest) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /permitted-external-apps - -#### GET -##### Parameters +### [GET] /permitted-external-apps +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| limit | query | | No | integer | +| limit | query | | No | integer,
**Default:** 20 | | mode | query | | No | string | | name | query | | No | string | -| page | query | | No | integer | +| page | query | | No | integer,
**Default:** 1 | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Permitted external apps list | [PermittedExternalAppsListResponse](#permittedexternalappslistresponse) | -| 422 | Validation error | [ErrorBody](#errorbody) | -| default | Error | [ErrorBody](#errorbody) | +| 200 | Permitted external apps list | **application/json**: [PermittedExternalAppsListResponse](#permittedexternalappslistresponse)
| +| 422 | Validation error | **application/json**: [ErrorBody](#errorbody)
| +| default | Error | **application/json**: [ErrorBody](#errorbody)
| -### /workspaces - -#### GET -##### Responses +### [GET] /workspaces +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workspace list | [WorkspaceListResponse](#workspacelistresponse) | -| default | Error | [ErrorBody](#errorbody) | +| 200 | Workspace list | **application/json**: [WorkspaceListResponse](#workspacelistresponse)
| +| default | Error | **application/json**: [ErrorBody](#errorbody)
| -### /workspaces/{workspace_id} - -#### GET -##### Parameters +### [GET] /workspaces/{workspace_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | workspace_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workspace detail | [WorkspaceDetailResponse](#workspacedetailresponse) | -| default | Error | [ErrorBody](#errorbody) | +| 200 | Workspace detail | **application/json**: [WorkspaceDetailResponse](#workspacedetailresponse)
| +| default | Error | **application/json**: [ErrorBody](#errorbody)
| -### /workspaces/{workspace_id}/apps/imports - -#### POST -##### Parameters +### [POST] /workspaces/{workspace_id}/apps/imports +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | workspace_id | path | | Yes | string | -| payload | body | | Yes | [AppDslImportPayload](#appdslimportpayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AppDslImportPayload](#appdslimportpayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Import completed | [Import](#import) | -| 202 | Import pending confirmation | [Import](#import) | -| 400 | Import failed | [Import](#import) | -| 422 | Validation error | [ErrorBody](#errorbody) | -| default | Error | [ErrorBody](#errorbody) | +| 200 | Import completed | **application/json**: [Import](#import)
| +| 202 | Import pending confirmation | **application/json**: [Import](#import)
| +| 400 | Import failed | **application/json**: [Import](#import)
| +| 422 | Validation error | **application/json**: [ErrorBody](#errorbody)
| +| default | Error | **application/json**: [ErrorBody](#errorbody)
| -### /workspaces/{workspace_id}/apps/imports/{import_id}/confirm - -#### POST -##### Parameters +### [POST] /workspaces/{workspace_id}/apps/imports/{import_id}/confirm +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | import_id | path | | Yes | string | | workspace_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Import confirmed | [Import](#import) | -| 400 | Import failed | [Import](#import) | -| default | Error | [ErrorBody](#errorbody) | +| 200 | Import confirmed | **application/json**: [Import](#import)
| +| 400 | Import failed | **application/json**: [Import](#import)
| +| default | Error | **application/json**: [ErrorBody](#errorbody)
| -### /workspaces/{workspace_id}/members +### [GET] /workspaces/{workspace_id}/members +#### Parameters -#### GET -##### Parameters +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| limit | query | | No | integer,
**Default:** 20 | +| page | query | | No | integer,
**Default:** 1 | +| workspace_id | path | | Yes | string | + +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Member list | **application/json**: [MemberListResponse](#memberlistresponse)
| +| 422 | Validation error | **application/json**: [ErrorBody](#errorbody)
| +| default | Error | **application/json**: [ErrorBody](#errorbody)
| + +### [POST] /workspaces/{workspace_id}/members +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | workspace_id | path | | Yes | string | -| limit | query | | No | integer | -| page | query | | No | integer | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [MemberInvitePayload](#memberinvitepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Member list | [MemberListResponse](#memberlistresponse) | -| 422 | Validation error | [ErrorBody](#errorbody) | -| default | Error | [ErrorBody](#errorbody) | +| 201 | Member invited | **application/json**: [MemberInviteResponse](#memberinviteresponse)
| +| 422 | Validation error | **application/json**: [ErrorBody](#errorbody)
| +| default | Error | **application/json**: [ErrorBody](#errorbody)
| -#### POST -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| workspace_id | path | | Yes | string | -| payload | body | | Yes | [MemberInvitePayload](#memberinvitepayload) | - -##### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 201 | Member invited | [MemberInviteResponse](#memberinviteresponse) | -| 422 | Validation error | [ErrorBody](#errorbody) | -| default | Error | [ErrorBody](#errorbody) | - -### /workspaces/{workspace_id}/members/{member_id} - -#### DELETE -##### Parameters +### [DELETE] /workspaces/{workspace_id}/members/{member_id} +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | member_id | path | | Yes | string | | workspace_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Member removed | [MemberActionResponse](#memberactionresponse) | -| default | Error | [ErrorBody](#errorbody) | +| 200 | Member removed | **application/json**: [MemberActionResponse](#memberactionresponse)
| +| default | Error | **application/json**: [ErrorBody](#errorbody)
| -### /workspaces/{workspace_id}/members/{member_id}/role - -#### PUT -##### Parameters +### [PUT] /workspaces/{workspace_id}/members/{member_id}/role +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | member_id | path | | Yes | string | | workspace_id | path | | Yes | string | -| payload | body | | Yes | [MemberRoleUpdatePayload](#memberroleupdatepayload) | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [MemberRoleUpdatePayload](#memberroleupdatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Role updated | [MemberActionResponse](#memberactionresponse) | -| 422 | Validation error | [ErrorBody](#errorbody) | -| default | Error | [ErrorBody](#errorbody) | +| 200 | Role updated | **application/json**: [MemberActionResponse](#memberactionresponse)
| +| 422 | Validation error | **application/json**: [ErrorBody](#errorbody)
| +| default | Error | **application/json**: [ErrorBody](#errorbody)
| -### /workspaces/{workspace_id}/switch - -#### POST -##### Parameters +### [POST] /workspaces/{workspace_id}/switch +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | workspace_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workspace detail | [WorkspaceDetailResponse](#workspacedetailresponse) | -| default | Error | [ErrorBody](#errorbody) | +| 200 | Workspace detail | **application/json**: [WorkspaceDetailResponse](#workspacedetailresponse)
| +| default | Error | **application/json**: [ErrorBody](#errorbody)
| --- -### Models +### Schemas #### AccountPayload @@ -538,7 +500,7 @@ Upload a file to use as an input variable when running the app | subject_email | string | | No | | subject_issuer | string | | No | | subject_type | string | | Yes | -| workspaces | [ [WorkspacePayload](#workspacepayload) ] | | No | +| workspaces | [ [WorkspacePayload](#workspacepayload) ],
**Default:** | | No | #### AppDescribeInfo @@ -551,7 +513,7 @@ Upload a file to use as an input variable when running the app | mode | string | | Yes | | name | string | | Yes | | service_api_enabled | boolean | | Yes | -| tags | [ [TagItem](#tagitem) ] | | No | +| tags | [ [TagItem](#tagitem) ],
**Default:** | | No | | updated_at | string | | No | #### AppDescribeQuery @@ -600,7 +562,7 @@ Request body for POST /workspaces//apps/imports. | icon | string | | No | | icon_background | string | | No | | icon_type | string | | No | -| mode | string | Import mode: yaml-content or yaml-url
*Enum:* `"yaml-content"`, `"yaml-url"` | Yes | +| mode | string,
**Available values:** "yaml-content", "yaml-url" | Import mode: yaml-content or yaml-url
*Enum:* `"yaml-content"`, `"yaml-url"` | Yes | | name | string | Override the app name from the DSL | No | | yaml_content | string | Inline YAML DSL string (required when mode is yaml-content) | No | | yaml_url | string | Remote URL to fetch YAML from (required when mode is yaml-url) | No | @@ -614,7 +576,7 @@ Request body for POST /workspaces//apps/imports. | id | string | | Yes | | mode | string | | Yes | | name | string | | Yes | -| tags | [ [TagItem](#tagitem) ] | | No | +| tags | [ [TagItem](#tagitem) ],
**Default:** | | No | #### AppListQuery @@ -622,10 +584,10 @@ mode is a closed enum. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| limit | integer | | No | +| limit | integer,
**Default:** 20 | | No | | mode | [AppMode](#appmode) | | No | | name | string | | No | -| page | integer | | No | +| page | integer,
**Default:** 1 | | No | | tag | string | | No | | workspace_id | string | | Yes | @@ -648,7 +610,7 @@ mode is a closed enum. | id | string | | Yes | | mode | [AppMode](#appmode) | | Yes | | name | string | | Yes | -| tags | [ [TagItem](#tagitem) ] | | No | +| tags | [ [TagItem](#tagitem) ],
**Default:** | | No | | updated_at | string | | No | | workspace_id | string | | No | | workspace_name | string | | No | @@ -663,7 +625,7 @@ mode is a closed enum. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| auto_generate_name | boolean | | No | +| auto_generate_name | boolean,
**Default:** true | | No | | conversation_id | string | | No | | files | [ object ] | | No | | inputs | object | | Yes | @@ -745,7 +707,7 @@ future server adds a code. Formatter tests pin emitted values to the enum. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| loc | [ ] | | No | +| loc | [ ],
**Default:** | | No | | msg | string | | Yes | | type | string | | Yes | @@ -808,7 +770,7 @@ Liveness payload for `GET /openapi/v1/_health` — no auth required. | ---- | ---- | ----------- | -------- | | app_id | string | | No | | app_mode | string | | No | -| current_dsl_version | string | | No | +| current_dsl_version | string,
**Default:** 0.6.0 | | No | | error | string | | No | | id | string | | Yes | | imported_dsl_version | string | | No | @@ -837,14 +799,14 @@ Liveness payload for `GET /openapi/v1/_health` — no auth required. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| result | string | | No | +| result | string,
**Default:** success | | No | #### MemberInvitePayload | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | email | string | | Yes | -| role | string | *Enum:* `"admin"`, `"normal"` | Yes | +| role | string,
**Available values:** "admin", "normal" | *Enum:* `"admin"`, `"normal"` | Yes | #### MemberInviteResponse @@ -853,7 +815,7 @@ Liveness payload for `GET /openapi/v1/_health` — no auth required. | email | string | | Yes | | invite_url | string | | Yes | | member_id | string | | Yes | -| result | string | | No | +| result | string,
**Default:** success | | No | | role | string | | Yes | | tenant_id | string | | Yes | @@ -863,8 +825,8 @@ Strict (extra='forbid'). | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| limit | integer | | No | -| page | integer | | No | +| limit | integer,
**Default:** 20 | | No | +| page | integer,
**Default:** 1 | | No | #### MemberListResponse @@ -891,13 +853,13 @@ Strict (extra='forbid'). | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| role | string | *Enum:* `"admin"`, `"normal"` | Yes | +| role | string,
**Available values:** "admin", "normal" | *Enum:* `"admin"`, `"normal"` | Yes | #### MessageMetadata | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| retriever_resources | [ object ] | | No | +| retriever_resources | [ object ],
**Default:** | | No | | usage | [UsageInfo](#usageinfo) | | No | #### OpenApiErrorCode @@ -919,10 +881,10 @@ Strict (extra='forbid'). | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| limit | integer | | No | +| limit | integer,
**Default:** 20 | | No | | mode | [AppMode](#appmode) | | No | | name | string | | No | -| page | integer | | No | +| page | integer,
**Default:** 1 | | No | #### PermittedExternalAppsListResponse @@ -954,7 +916,7 @@ Meta endpoint payload for `GET /openapi/v1/_version` — no auth required. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| edition | string | *Enum:* `"CLOUD"`, `"SELF_HOSTED"` | Yes | +| edition | string,
**Available values:** "CLOUD", "SELF_HOSTED" | *Enum:* `"CLOUD"`, `"SELF_HOSTED"` | Yes | | version | string | | Yes | #### SessionListQuery @@ -963,8 +925,8 @@ Pagination for GET /account/sessions. Strict (extra='forbid'). | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| limit | integer | | No | -| page | integer | | No | +| limit | integer,
**Default:** 100 | | No | +| page | integer,
**Default:** 1 | | No | #### SessionListResponse diff --git a/api/openapi/markdown/service-swagger.md b/api/openapi/markdown/service-openapi.md similarity index 74% rename from api/openapi/markdown/service-swagger.md rename to api/openapi/markdown/service-openapi.md index f32e998bd1c..47350fa8479 100644 --- a/api/openapi/markdown/service-swagger.md +++ b/api/openapi/markdown/service-openapi.md @@ -3,97 +3,76 @@ API for application services ## Version: 1.0 -### Security -**Bearer** - -| apiKey | *API Key* | -| ------ | --------- | -| Description | Type: Bearer {your-api-key} | -| In | header | -| Name | Authorization | +### Available authorizations +#### Bearer (API Key Authentication) +Type: Bearer {your-api-key} +**Name:** Authorization +**In:** header --- ## service_api Service operations -### / - -#### GET -##### Responses +### [GET] / +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [IndexInfoResponse](#indexinforesponse) | +| 200 | Success | **application/json**: [IndexInfoResponse](#indexinforesponse)
| -### /app/feedbacks - -#### GET -##### Summary - -Get all feedbacks for the application - -##### Description +### [GET] /app/feedbacks +**Get all feedbacks for the application** Get all feedbacks for the application Returns paginated list of all feedback submitted for messages in this app. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [FeedbackListQuery](#feedbacklistquery) | +| limit | query | Number of feedbacks per page | No | integer,
**Default:** 20 | +| page | query | Page number | No | integer,
**Default:** 1 | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Feedbacks retrieved successfully | | 401 | Unauthorized - invalid API token | -### /apps/annotation-reply/{action} +### [POST] /apps/annotation-reply/{action} +**Enable or disable annotation reply feature** -#### POST -##### Summary - -Enable or disable annotation reply feature - -##### Description - -Enable or disable annotation reply feature - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AnnotationReplyActionPayload](#annotationreplyactionpayload) | | action | path | Action to perform: 'enable' or 'disable' | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AnnotationReplyActionPayload](#annotationreplyactionpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | | 200 | Action completed successfully | | 401 | Unauthorized - invalid API token | -### /apps/annotation-reply/{action}/status/{job_id} +### [GET] /apps/annotation-reply/{action}/status/{job_id} +**Get the status of an annotation reply action job** -#### GET -##### Summary - -Get the status of an annotation reply action job - -##### Description - -Get the status of an annotation reply action job - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | action | path | Action type | Yes | string | | job_id | path | Job ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -101,72 +80,50 @@ Get the status of an annotation reply action job | 401 | Unauthorized - invalid API token | | 404 | Job not found | -### /apps/annotations +### [GET] /apps/annotations +**List annotations for the application** -#### GET -##### Summary - -List annotations for the application - -##### Description - -List annotations for the application - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | keyword | query | Keyword to search annotations | No | string | -| limit | query | Number of annotations per page | No | integer | -| page | query | Page number | No | integer | +| limit | query | Number of annotations per page | No | integer,
**Default:** 20 | +| page | query | Page number | No | integer,
**Default:** 1 | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Annotations retrieved successfully | [AnnotationList](#annotationlist) | +| 200 | Annotations retrieved successfully | **application/json**: [AnnotationList](#annotationlist)
| | 401 | Unauthorized - invalid API token | | -#### POST -##### Summary +### [POST] /apps/annotations +**Create a new annotation** -Create a new annotation +#### Request Body -##### Description +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AnnotationCreatePayload](#annotationcreatepayload)
| -Create a new annotation - -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AnnotationCreatePayload](#annotationcreatepayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | Annotation created successfully | [Annotation](#annotation) | +| 201 | Annotation created successfully | **application/json**: [Annotation](#annotation)
| | 401 | Unauthorized - invalid API token | | -### /apps/annotations/{annotation_id} +### [DELETE] /apps/annotations/{annotation_id} +**Delete an annotation** -#### DELETE -##### Summary - -Delete an annotation - -##### Description - -Delete an annotation - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | annotation_id | path | Annotation ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -175,44 +132,37 @@ Delete an annotation | 403 | Forbidden - insufficient permissions | | 404 | Annotation not found | -#### PUT -##### Summary +### [PUT] /apps/annotations/{annotation_id} +**Update an existing annotation** -Update an existing annotation - -##### Description - -Update an existing annotation - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [AnnotationCreatePayload](#annotationcreatepayload) | | annotation_id | path | Annotation ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [AnnotationCreatePayload](#annotationcreatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Annotation updated successfully | [Annotation](#annotation) | +| 200 | Annotation updated successfully | **application/json**: [Annotation](#annotation)
| | 401 | Unauthorized - invalid API token | | | 403 | Forbidden - insufficient permissions | | | 404 | Annotation not found | | -### /audio-to-text - -#### POST -##### Summary - -Convert audio to text using speech-to-text - -##### Description +### [POST] /audio-to-text +**Convert audio to text using speech-to-text** Convert audio to text using speech-to-text Accepts an audio file upload and returns the transcribed text. -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -223,26 +173,20 @@ Accepts an audio file upload and returns the transcribed text. | 415 | Unsupported audio type | | 500 | Internal server error | -### /chat-messages - -#### POST -##### Summary - -Send a message in a chat conversation - -##### Description +### [POST] /chat-messages +**Send a message in a chat conversation** Send a message in a chat conversation This endpoint handles chat messages for chat, agent chat, and advanced chat applications. Supports conversation management and both blocking and streaming response modes. -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ChatRequestPayload](#chatrequestpayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ChatRequestPayload](#chatrequestpayload)
| -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -253,51 +197,37 @@ Supports conversation management and both blocking and streaming response modes. | 429 | Rate limit exceeded | | 500 | Internal server error | -### /chat-messages/{task_id}/stop +### [POST] /chat-messages/{task_id}/stop +**Stop a running chat message generation** -#### POST -##### Summary - -Stop a running chat message generation - -##### Description - -Stop a running chat message generation - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | task_id | path | The ID of the task to stop | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Task stopped successfully | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Task stopped successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Task not found | | -### /completion-messages - -#### POST -##### Summary - -Create a completion for the given prompt - -##### Description +### [POST] /completion-messages +**Create a completion for the given prompt** Create a completion for the given prompt This endpoint generates a completion based on the provided inputs and query. Supports both blocking and streaming response modes. -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [CompletionRequestPayload](#completionrequestpayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [CompletionRequestPayload](#completionrequestpayload)
| -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -307,50 +237,38 @@ Supports both blocking and streaming response modes. | 404 | Conversation not found | | 500 | Internal server error | -### /completion-messages/{task_id}/stop +### [POST] /completion-messages/{task_id}/stop +**Stop a running completion task** -#### POST -##### Summary - -Stop a running completion task - -##### Description - -Stop a running completion task - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | task_id | path | The ID of the task to stop | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Task stopped successfully | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Task stopped successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Task not found | | -### /conversations - -#### GET -##### Summary - -List all conversations for the current user - -##### Description +### [GET] /conversations +**List all conversations for the current user** List all conversations for the current user Supports pagination using last_id and limit parameters. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ConversationListQuery](#conversationlistquery) | +| last_id | query | Last conversation ID for pagination | No | | +| limit | query | Number of conversations to return | No | integer,
**Default:** 20 | +| sort_by | query | Sort order for conversations | No | string,
**Available values:** "-created_at", "-updated_at", "created_at", "updated_at",
**Default:** -updated_at | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -358,24 +276,16 @@ Supports pagination using last_id and limit parameters. | 401 | Unauthorized - invalid API token | | 404 | Last conversation not found | -### /conversations/{c_id} +### [DELETE] /conversations/{c_id} +**Delete a specific conversation** -#### DELETE -##### Summary - -Delete a specific conversation - -##### Description - -Delete a specific conversation - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | c_id | path | Conversation ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -383,25 +293,22 @@ Delete a specific conversation | 401 | Unauthorized - invalid API token | | 404 | Conversation not found | -### /conversations/{c_id}/name +### [POST] /conversations/{c_id}/name +**Rename a conversation or auto-generate a name** -#### POST -##### Summary - -Rename a conversation or auto-generate a name - -##### Description - -Rename a conversation or auto-generate a name - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ConversationRenamePayload](#conversationrenamepayload) | | c_id | path | Conversation ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ConversationRenamePayload](#conversationrenamepayload)
| + +#### Responses | Code | Description | | ---- | ----------- | @@ -409,127 +316,106 @@ Rename a conversation or auto-generate a name | 401 | Unauthorized - invalid API token | | 404 | Conversation not found | -### /conversations/{c_id}/variables - -#### GET -##### Summary - -List all variables for a conversation - -##### Description +### [GET] /conversations/{c_id}/variables +**List all variables for a conversation** List all variables for a conversation Conversational variables are only available for chat applications. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ConversationVariablesQuery](#conversationvariablesquery) | | c_id | path | Conversation ID | Yes | string | +| last_id | query | Last variable ID for pagination | No | | +| limit | query | Number of variables to return | No | integer,
**Default:** 20 | +| variable_name | query | Filter variables by name | No | | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Variables retrieved successfully | [ConversationVariableInfiniteScrollPaginationResponse](#conversationvariableinfinitescrollpaginationresponse) | +| 200 | Variables retrieved successfully | **application/json**: [ConversationVariableInfiniteScrollPaginationResponse](#conversationvariableinfinitescrollpaginationresponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Conversation not found | | -### /conversations/{c_id}/variables/{variable_id} - -#### PUT -##### Summary - -Update a conversation variable's value - -##### Description +### [PUT] /conversations/{c_id}/variables/{variable_id} +**Update a conversation variable's value** Update a conversation variable's value Allows updating the value of a specific conversation variable. The value must match the variable's expected type. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ConversationVariableUpdatePayload](#conversationvariableupdatepayload) | | c_id | path | Conversation ID | Yes | string | | variable_id | path | Variable ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ConversationVariableUpdatePayload](#conversationvariableupdatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Variable updated successfully | [ConversationVariableResponse](#conversationvariableresponse) | +| 200 | Variable updated successfully | **application/json**: [ConversationVariableResponse](#conversationvariableresponse)
| | 400 | Bad request - type mismatch | | | 401 | Unauthorized - invalid API token | | | 404 | Conversation or variable not found | | -### /datasets - -#### GET -##### Summary - -Resource for getting datasets - -##### Description +### [GET] /datasets +**Resource for getting datasets** List all datasets -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | include_all | query | Include all datasets | No | boolean | | keyword | query | Search keyword | No | string | -| limit | query | Number of items per page | No | integer | -| page | query | Page number | No | integer | +| limit | query | Number of items per page | No | integer,
**Default:** 20 | +| page | query | Page number | No | integer,
**Default:** 1 | | tag_ids | query | Filter by tag IDs | No | [ string ] | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Datasets retrieved successfully | [DatasetListResponse](#datasetlistresponse) | +| 200 | Datasets retrieved successfully | **application/json**: [DatasetListResponse](#datasetlistresponse)
| | 401 | Unauthorized - invalid API token | | -#### POST -##### Summary - -Resource for creating datasets - -##### Description +### [POST] /datasets +**Resource for creating datasets** Create a new dataset -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [DatasetCreatePayload](#datasetcreatepayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DatasetCreatePayload](#datasetcreatepayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Dataset created successfully | [DatasetDetailResponse](#datasetdetailresponse) | +| 200 | Dataset created successfully | **application/json**: [DatasetDetailResponse](#datasetdetailresponse)
| | 400 | Bad request - invalid parameters | | | 401 | Unauthorized - invalid API token | | -### /datasets/pipeline/file-upload - -#### POST -##### Summary - -Upload a file for use in conversations - -##### Description +### [POST] /datasets/pipeline/file-upload +**Upload a file for use in conversations** Upload a file to a knowledgebase pipeline Accepts a single file upload via multipart/form-data. -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -539,24 +425,16 @@ Accepts a single file upload via multipart/form-data. | 413 | File too large | | 415 | Unsupported file type | -### /datasets/tags +### [DELETE] /datasets/tags +**Delete a knowledge type tag** -#### DELETE -##### Summary +#### Request Body -Delete a knowledge type tag +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TagDeletePayload](#tagdeletepayload)
| -##### Description - -Delete a knowledge type tag - -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [TagDeletePayload](#tagdeletepayload) | - -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -564,78 +442,60 @@ Delete a knowledge type tag | 401 | Unauthorized - invalid API token | | 403 | Forbidden - insufficient permissions | -#### GET -##### Summary +### [GET] /datasets/tags +**Get all knowledge type tags** -Get all knowledge type tags - -##### Description - -Get all knowledge type tags - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Tags retrieved successfully | [KnowledgeTagListResponse](#knowledgetaglistresponse) | +| 200 | Tags retrieved successfully | **application/json**: [KnowledgeTagListResponse](#knowledgetaglistresponse)
| | 401 | Unauthorized - invalid API token | | -#### PATCH -##### Description - +### [PATCH] /datasets/tags Update a knowledge type tag -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [TagUpdatePayload](#tagupdatepayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TagUpdatePayload](#tagupdatepayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Tag updated successfully | [KnowledgeTagResponse](#knowledgetagresponse) | +| 200 | Tag updated successfully | **application/json**: [KnowledgeTagResponse](#knowledgetagresponse)
| | 401 | Unauthorized - invalid API token | | | 403 | Forbidden - insufficient permissions | | -#### POST -##### Summary +### [POST] /datasets/tags +**Add a knowledge type tag** -Add a knowledge type tag +#### Request Body -##### Description +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TagCreatePayload](#tagcreatepayload)
| -Add a knowledge type tag - -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [TagCreatePayload](#tagcreatepayload) | - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Tag created successfully | [KnowledgeTagResponse](#knowledgetagresponse) | +| 200 | Tag created successfully | **application/json**: [KnowledgeTagResponse](#knowledgetagresponse)
| | 401 | Unauthorized - invalid API token | | | 403 | Forbidden - insufficient permissions | | -### /datasets/tags/binding - -#### POST -##### Description - +### [POST] /datasets/tags/binding Bind tags to a dataset -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [TagBindingPayload](#tagbindingpayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TagBindingPayload](#tagbindingpayload)
| -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -643,20 +503,16 @@ Bind tags to a dataset | 401 | Unauthorized - invalid API token | | 403 | Forbidden - insufficient permissions | -### /datasets/tags/unbinding - -#### POST -##### Description - +### [POST] /datasets/tags/unbinding Unbind tags from a dataset -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [TagUnbindingPayload](#tagunbindingpayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TagUnbindingPayload](#tagunbindingpayload)
| -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -664,14 +520,8 @@ Unbind tags from a dataset | 401 | Unauthorized - invalid API token | | 403 | Forbidden - insufficient permissions | -### /datasets/{dataset_id} - -#### DELETE -##### Summary - -Deletes a dataset given its ID - -##### Description +### [DELETE] /datasets/{dataset_id} +**Deletes a dataset given its ID** Delete a dataset Args: @@ -686,13 +536,13 @@ Returns: Raises: NotFound: If the dataset with the given ID does not exist. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -701,178 +551,180 @@ Raises: | 404 | Dataset not found | | 409 | Conflict - dataset is in use | -#### GET -##### Description - +### [GET] /datasets/{dataset_id} Get a specific dataset by ID -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Dataset retrieved successfully | [DatasetDetailWithPartialMembersResponse](#datasetdetailwithpartialmembersresponse) | +| 200 | Dataset retrieved successfully | **application/json**: [DatasetDetailWithPartialMembersResponse](#datasetdetailwithpartialmembersresponse)
| | 401 | Unauthorized - invalid API token | | | 403 | Forbidden - insufficient permissions | | | 404 | Dataset not found | | -#### PATCH -##### Description - +### [PATCH] /datasets/{dataset_id} Update an existing dataset -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [DatasetUpdatePayload](#datasetupdatepayload) | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DatasetUpdatePayload](#datasetupdatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Dataset updated successfully | [DatasetDetailWithPartialMembersResponse](#datasetdetailwithpartialmembersresponse) | +| 200 | Dataset updated successfully | **application/json**: [DatasetDetailWithPartialMembersResponse](#datasetdetailwithpartialmembersresponse)
| | 401 | Unauthorized - invalid API token | | | 403 | Forbidden - insufficient permissions | | | 404 | Dataset not found | | -### /datasets/{dataset_id}/document/create-by-file - -#### POST -##### Description - +### [POST] /datasets/{dataset_id}/document/create-by-file Create a new document by uploading a file -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| data | formData | Optional JSON string with document creation settings. | No | string | -| file | formData | Document file to upload. | Yes | file | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **multipart/form-data**: { **"data"**: string, **"file"**: binary }
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Document created successfully | [DocumentAndBatchResponse](#documentandbatchresponse) | +| 200 | Document created successfully | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| | 400 | Bad request - invalid file or parameters | | | 401 | Unauthorized - invalid API token | | -### /datasets/{dataset_id}/document/create-by-text - -#### POST -##### Description - +### [POST] /datasets/{dataset_id}/document/create-by-text Create a new document by providing text content -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [DocumentTextCreatePayload](#documenttextcreatepayload) | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DocumentTextCreatePayload](#documenttextcreatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Document created successfully | [DocumentAndBatchResponse](#documentandbatchresponse) | +| 200 | Document created successfully | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| | 400 | Bad request - invalid parameters | | | 401 | Unauthorized - invalid API token | | -### /datasets/{dataset_id}/document/create_by_file - -#### POST -##### Description - +### [POST] /datasets/{dataset_id}/document/create_by_file Create a new document by uploading a file -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| data | formData | Optional JSON string with document creation settings. | No | string | -| file | formData | Document file to upload. | Yes | file | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **multipart/form-data**: { **"data"**: string, **"file"**: binary }
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Document created successfully | [DocumentAndBatchResponse](#documentandbatchresponse) | +| 200 | Document created successfully | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| | 400 | Bad request - invalid file or parameters | | | 401 | Unauthorized - invalid API token | | -### /datasets/{dataset_id}/document/create_by_text +### ~~[POST] /datasets/{dataset_id}/document/create_by_text~~ -#### POST ***DEPRECATED*** -##### Description Deprecated legacy alias for creating a new document by providing text content. Use /datasets/{dataset_id}/document/create-by-text instead. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [DocumentTextCreatePayload](#documenttextcreatepayload) | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DocumentTextCreatePayload](#documenttextcreatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Document created successfully | [DocumentAndBatchResponse](#documentandbatchresponse) | +| 200 | Document created successfully | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| | 400 | Bad request - invalid parameters | | | 401 | Unauthorized - invalid API token | | -### /datasets/{dataset_id}/documents - -#### GET -##### Description - +### [GET] /datasets/{dataset_id}/documents List all documents in a dataset -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | Dataset ID | Yes | string | | keyword | query | Search keyword | No | string | -| limit | query | Number of items per page | No | integer | -| page | query | Page number | No | integer | +| limit | query | Number of items per page | No | integer,
**Default:** 20 | +| page | query | Page number | No | integer,
**Default:** 1 | | status | query | Document status filter | No | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Documents retrieved successfully | [DocumentListResponse](#documentlistresponse) | +| 200 | Documents retrieved successfully | **application/json**: [DocumentListResponse](#documentlistresponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Dataset not found | | -### /datasets/{dataset_id}/documents/download-zip - -#### POST -##### Description - +### [POST] /datasets/{dataset_id}/documents/download-zip Download selected uploaded documents as a single ZIP archive -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [DocumentBatchDownloadZipPayload](#documentbatchdownloadzippayload) | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DocumentBatchDownloadZipPayload](#documentbatchdownloadzippayload)
| + +#### Responses | Code | Description | | ---- | ----------- | @@ -881,40 +733,31 @@ Download selected uploaded documents as a single ZIP archive | 403 | Forbidden - insufficient permissions | | 404 | Document or dataset not found | -### /datasets/{dataset_id}/documents/metadata +### [POST] /datasets/{dataset_id}/documents/metadata +**Update metadata for multiple documents** -#### POST -##### Summary - -Update metadata for multiple documents - -##### Description - -Update metadata for multiple documents - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [MetadataOperationData](#metadataoperationdata) | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [MetadataOperationData](#metadataoperationdata)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Documents metadata updated successfully | [DatasetMetadataActionResponse](#datasetmetadataactionresponse) | +| 200 | Documents metadata updated successfully | **application/json**: [DatasetMetadataActionResponse](#datasetmetadataactionresponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Dataset not found | | -### /datasets/{dataset_id}/documents/status/{action} - -#### PATCH -##### Summary - -Batch update document status - -##### Description +### [PATCH] /datasets/{dataset_id}/documents/status/{action} +**Batch update document status** Batch update document status Args: @@ -931,64 +774,54 @@ Raises: Forbidden: If the user does not have permission. InvalidActionError: If the action is invalid or cannot be performed. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | action | path | Action to perform: 'enable', 'disable', 'archive', or 'un_archive' | Yes | string | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Document status updated successfully | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Document status updated successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| | 400 | Bad request - invalid action | | | 401 | Unauthorized - invalid API token | | | 403 | Forbidden - insufficient permissions | | | 404 | Dataset not found | | -### /datasets/{dataset_id}/documents/{batch}/indexing-status - -#### GET -##### Description - +### [GET] /datasets/{dataset_id}/documents/{batch}/indexing-status Get indexing status for documents in a batch -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | batch | path | Batch ID | Yes | string | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Indexing status retrieved successfully | [DocumentStatusListResponse](#documentstatuslistresponse) | +| 200 | Indexing status retrieved successfully | **application/json**: [DocumentStatusListResponse](#documentstatuslistresponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Dataset or documents not found | | -### /datasets/{dataset_id}/documents/{document_id} - -#### DELETE -##### Summary - -Delete document - -##### Description +### [DELETE] /datasets/{dataset_id}/documents/{document_id} +**Delete document** Delete a document -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | Dataset ID | Yes | string | | document_id | path | Document ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -997,19 +830,17 @@ Delete a document | 403 | Forbidden - document is archived | | 404 | Document not found | -#### GET -##### Description - +### [GET] /datasets/{dataset_id}/documents/{document_id} Get a specific document by ID -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | Dataset ID | Yes | string | | document_id | path | Document ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -1018,107 +849,100 @@ Get a specific document by ID | 403 | Forbidden - insufficient permissions | | 404 | Document not found | -#### PATCH -##### Description - +### [PATCH] /datasets/{dataset_id}/documents/{document_id} Update an existing document by uploading a file -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| data | formData | Optional JSON string with document update settings. | No | string | -| file | formData | Replacement document file. | No | file | | dataset_id | path | Dataset ID | Yes | string | | document_id | path | Document ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| No | **multipart/form-data**: { **"data"**: string, **"file"**: binary }
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Document updated successfully | [DocumentAndBatchResponse](#documentandbatchresponse) | +| 200 | Document updated successfully | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Document not found | | -### /datasets/{dataset_id}/documents/{document_id}/download - -#### GET -##### Description - +### [GET] /datasets/{dataset_id}/documents/{document_id}/download Get a signed download URL for a document's original uploaded file -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | Dataset ID | Yes | string | | document_id | path | Document ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Download URL generated successfully | [UrlResponse](#urlresponse) | +| 200 | Download URL generated successfully | **application/json**: [UrlResponse](#urlresponse)
| | 401 | Unauthorized - invalid API token | | | 403 | Forbidden - insufficient permissions | | | 404 | Document or upload file not found | | -### /datasets/{dataset_id}/documents/{document_id}/segments - -#### GET -##### Description - +### [GET] /datasets/{dataset_id}/documents/{document_id}/segments List segments in a document -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | Dataset ID | Yes | string | | document_id | path | Document ID | Yes | string | | keyword | query | | No | string | -| limit | query | | No | integer | -| page | query | | No | integer | +| limit | query | | No | integer,
**Default:** 20 | +| page | query | | No | integer,
**Default:** 1 | | status | query | | No | [ string ] | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Segments retrieved successfully | [SegmentListResponse](#segmentlistresponse) | +| 200 | Segments retrieved successfully | **application/json**: [SegmentListResponse](#segmentlistresponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Dataset or document not found | | -#### POST -##### Description - +### [POST] /datasets/{dataset_id}/documents/{document_id}/segments Create segments in a document -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [SegmentCreatePayload](#segmentcreatepayload) | | dataset_id | path | Dataset ID | Yes | string | | document_id | path | Document ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [SegmentCreatePayload](#segmentcreatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Segments created successfully | [SegmentCreateListResponse](#segmentcreatelistresponse) | +| 200 | Segments created successfully | **application/json**: [SegmentCreateListResponse](#segmentcreatelistresponse)
| | 400 | Bad request - segments data is missing | | | 401 | Unauthorized - invalid API token | | | 404 | Dataset or document not found | | -### /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id} - -#### DELETE -##### Description - +### [DELETE] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id} Delete a specific segment -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -1126,7 +950,7 @@ Delete a specific segment | document_id | path | Document ID | Yes | string | | segment_id | path | Segment ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -1134,12 +958,10 @@ Delete a specific segment | 401 | Unauthorized - invalid API token | | 404 | Dataset, document, or segment not found | -#### GET -##### Description - +### [GET] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id} Get a specific segment by ID -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -1147,44 +969,43 @@ Get a specific segment by ID | document_id | path | Document ID | Yes | string | | segment_id | path | Segment ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Segment retrieved successfully | [SegmentDetailResponse](#segmentdetailresponse) | +| 200 | Segment retrieved successfully | **application/json**: [SegmentDetailResponse](#segmentdetailresponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Dataset, document, or segment not found | | -#### POST -##### Description - +### [POST] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id} Update a specific segment -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [SegmentUpdatePayload](#segmentupdatepayload) | | dataset_id | path | Dataset ID | Yes | string | | document_id | path | Document ID | Yes | string | | segment_id | path | Segment ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [SegmentUpdatePayload](#segmentupdatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Segment updated successfully | [SegmentDetailResponse](#segmentdetailresponse) | +| 200 | Segment updated successfully | **application/json**: [SegmentDetailResponse](#segmentdetailresponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Dataset, document, or segment not found | | -### /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks - -#### GET -##### Description - +### [GET] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks List child chunks for a segment -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -1192,47 +1013,46 @@ List child chunks for a segment | document_id | path | Document ID | Yes | string | | segment_id | path | Parent segment ID | Yes | string | | keyword | query | | No | string | -| limit | query | | No | integer | -| page | query | | No | integer | +| limit | query | | No | integer,
**Default:** 20 | +| page | query | | No | integer,
**Default:** 1 | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Child chunks retrieved successfully | [ChildChunkListResponse](#childchunklistresponse) | +| 200 | Child chunks retrieved successfully | **application/json**: [ChildChunkListResponse](#childchunklistresponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Dataset, document, or segment not found | | -#### POST -##### Description - +### [POST] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks Create a new child chunk for a segment -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ChildChunkCreatePayload](#childchunkcreatepayload) | | dataset_id | path | Dataset ID | Yes | string | | document_id | path | Document ID | Yes | string | | segment_id | path | Parent segment ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ChildChunkCreatePayload](#childchunkcreatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Child chunk created successfully | [ChildChunkDetailResponse](#childchunkdetailresponse) | +| 200 | Child chunk created successfully | **application/json**: [ChildChunkDetailResponse](#childchunkdetailresponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Dataset, document, or segment not found | | -### /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id} - -#### DELETE -##### Description - +### [DELETE] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id} Delete a specific child chunk -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -1241,7 +1061,7 @@ Delete a specific child chunk | document_id | path | Document ID | Yes | string | | segment_id | path | Parent segment ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -1249,271 +1069,248 @@ Delete a specific child chunk | 401 | Unauthorized - invalid API token | | 404 | Dataset, document, segment, or child chunk not found | -#### PATCH -##### Description - +### [PATCH] /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id} Update a specific child chunk -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ChildChunkUpdatePayload](#childchunkupdatepayload) | | child_chunk_id | path | Child chunk ID | Yes | string | | dataset_id | path | Dataset ID | Yes | string | | document_id | path | Document ID | Yes | string | | segment_id | path | Parent segment ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ChildChunkUpdatePayload](#childchunkupdatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Child chunk updated successfully | [ChildChunkDetailResponse](#childchunkdetailresponse) | +| 200 | Child chunk updated successfully | **application/json**: [ChildChunkDetailResponse](#childchunkdetailresponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Dataset, document, segment, or child chunk not found | | -### /datasets/{dataset_id}/documents/{document_id}/update-by-file +### ~~[POST] /datasets/{dataset_id}/documents/{document_id}/update-by-file~~ -#### POST ***DEPRECATED*** -##### Description Deprecated legacy alias for updating an existing document by uploading a file. Use PATCH /datasets/{dataset_id}/documents/{document_id} instead. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| data | formData | Optional JSON string with document update settings. | No | string | -| file | formData | Replacement document file. | No | file | | dataset_id | path | Dataset ID | Yes | string | | document_id | path | Document ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| No | **multipart/form-data**: { **"data"**: string, **"file"**: binary }
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Document updated successfully | [DocumentAndBatchResponse](#documentandbatchresponse) | +| 200 | Document updated successfully | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Document not found | | -### /datasets/{dataset_id}/documents/{document_id}/update-by-text - -#### POST -##### Description - +### [POST] /datasets/{dataset_id}/documents/{document_id}/update-by-text Update an existing document by providing text content -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [DocumentTextUpdate](#documenttextupdate) | | dataset_id | path | Dataset ID | Yes | string | | document_id | path | Document ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DocumentTextUpdate](#documenttextupdate)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Document updated successfully | [DocumentAndBatchResponse](#documentandbatchresponse) | +| 200 | Document updated successfully | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Document not found | | -### /datasets/{dataset_id}/documents/{document_id}/update_by_file +### ~~[POST] /datasets/{dataset_id}/documents/{document_id}/update_by_file~~ -#### POST ***DEPRECATED*** -##### Description Deprecated legacy alias for updating an existing document by uploading a file. Use PATCH /datasets/{dataset_id}/documents/{document_id} instead. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| data | formData | Optional JSON string with document update settings. | No | string | -| file | formData | Replacement document file. | No | file | | dataset_id | path | Dataset ID | Yes | string | | document_id | path | Document ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| No | **multipart/form-data**: { **"data"**: string, **"file"**: binary }
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Document updated successfully | [DocumentAndBatchResponse](#documentandbatchresponse) | +| 200 | Document updated successfully | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Document not found | | -### /datasets/{dataset_id}/documents/{document_id}/update_by_text +### ~~[POST] /datasets/{dataset_id}/documents/{document_id}/update_by_text~~ -#### POST ***DEPRECATED*** -##### Description Deprecated legacy alias for updating an existing document by providing text content. Use /datasets/{dataset_id}/documents/{document_id}/update-by-text instead. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [DocumentTextUpdate](#documenttextupdate) | | dataset_id | path | Dataset ID | Yes | string | | document_id | path | Document ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [DocumentTextUpdate](#documenttextupdate)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Document updated successfully | [DocumentAndBatchResponse](#documentandbatchresponse) | +| 200 | Document updated successfully | **application/json**: [DocumentAndBatchResponse](#documentandbatchresponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Document not found | | -### /datasets/{dataset_id}/hit-testing - -#### POST -##### Summary - -Perform hit testing on a dataset - -##### Description +### [POST] /datasets/{dataset_id}/hit-testing +**Perform hit testing on a dataset** Perform hit testing on a dataset Tests retrieval performance for the specified dataset. -##### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [HitTestingPayload](#hittestingpayload) | -| dataset_id | path | Dataset ID | Yes | string | - -##### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Hit testing results | [HitTestingResponse](#hittestingresponse) | -| 401 | Unauthorized - invalid API token | | -| 404 | Dataset not found | | - -### /datasets/{dataset_id}/metadata - -#### GET -##### Summary - -Get all metadata for a dataset - -##### Description - -Get all metadata for a dataset - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [HitTestingPayload](#hittestingpayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Metadata retrieved successfully | [DatasetMetadataListResponse](#datasetmetadatalistresponse) | +| 200 | Hit testing results | **application/json**: [HitTestingResponse](#hittestingresponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Dataset not found | | -#### POST -##### Summary +### [GET] /datasets/{dataset_id}/metadata +**Get all metadata for a dataset** -Create metadata for a dataset - -##### Description - -Create metadata for a dataset - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [MetadataArgs](#metadataargs) | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | Metadata created successfully | [DatasetMetadataResponse](#datasetmetadataresponse) | +| 200 | Metadata retrieved successfully | **application/json**: [DatasetMetadataListResponse](#datasetmetadatalistresponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Dataset not found | | -### /datasets/{dataset_id}/metadata/built-in +### [POST] /datasets/{dataset_id}/metadata +**Create metadata for a dataset** -#### GET -##### Summary +#### Parameters -Get all built-in metadata fields +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ------ | +| dataset_id | path | Dataset ID | Yes | string | -##### Description +#### Request Body -Get all built-in metadata fields +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [MetadataArgs](#metadataargs)
| -##### Parameters +#### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Metadata created successfully | **application/json**: [DatasetMetadataResponse](#datasetmetadataresponse)
| +| 401 | Unauthorized - invalid API token | | +| 404 | Dataset not found | | + +### [GET] /datasets/{dataset_id}/metadata/built-in +**Get all built-in metadata fields** + +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Built-in fields retrieved successfully | [DatasetMetadataBuiltInFieldsResponse](#datasetmetadatabuiltinfieldsresponse) | +| 200 | Built-in fields retrieved successfully | **application/json**: [DatasetMetadataBuiltInFieldsResponse](#datasetmetadatabuiltinfieldsresponse)
| | 401 | Unauthorized - invalid API token | | -### /datasets/{dataset_id}/metadata/built-in/{action} +### [POST] /datasets/{dataset_id}/metadata/built-in/{action} +**Enable or disable built-in metadata field** -#### POST -##### Summary - -Enable or disable built-in metadata field - -##### Description - -Enable or disable built-in metadata field - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | action | path | Action to perform: 'enable' or 'disable' | Yes | string | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Action completed successfully | [DatasetMetadataActionResponse](#datasetmetadataactionresponse) | +| 200 | Action completed successfully | **application/json**: [DatasetMetadataActionResponse](#datasetmetadataactionresponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Dataset not found | | -### /datasets/{dataset_id}/metadata/{metadata_id} +### [DELETE] /datasets/{dataset_id}/metadata/{metadata_id} +**Delete metadata** -#### DELETE -##### Summary - -Delete metadata - -##### Description - -Delete metadata - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | Dataset ID | Yes | string | | metadata_id | path | Metadata ID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -1521,226 +1318,182 @@ Delete metadata | 401 | Unauthorized - invalid API token | | 404 | Dataset or metadata not found | -#### PATCH -##### Summary +### [PATCH] /datasets/{dataset_id}/metadata/{metadata_id} +**Update metadata name** -Update metadata name - -##### Description - -Update metadata name - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [MetadataUpdatePayload](#metadataupdatepayload) | | dataset_id | path | Dataset ID | Yes | string | | metadata_id | path | Metadata ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [MetadataUpdatePayload](#metadataupdatepayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Metadata updated successfully | [DatasetMetadataResponse](#datasetmetadataresponse) | +| 200 | Metadata updated successfully | **application/json**: [DatasetMetadataResponse](#datasetmetadataresponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Dataset or metadata not found | | -### /datasets/{dataset_id}/pipeline/datasource-plugins - -#### GET -##### Summary - -Resource for getting datasource plugins - -##### Description +### [GET] /datasets/{dataset_id}/pipeline/datasource-plugins +**Resource for getting datasource plugins** List all datasource plugins for a rag pipeline -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| dataset_id | path | | Yes | string | | is_published | query | Whether to get published or draft datasource plugins (true for published, false for draft, default: true) | No | string | +| dataset_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Datasource plugins retrieved successfully | | 401 | Unauthorized - invalid API token | -### /datasets/{dataset_id}/pipeline/datasource/nodes/{node_id}/run - -#### POST -##### Summary - -Resource for getting datasource plugins - -##### Description +### [POST] /datasets/{dataset_id}/pipeline/datasource/nodes/{node_id}/run +**Resource for getting datasource plugins** Run a datasource node for a rag pipeline -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string | | node_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Datasource node run successfully | | 401 | Unauthorized - invalid API token | -### /datasets/{dataset_id}/pipeline/run - -#### POST -##### Summary - -Resource for running a rag pipeline - -##### Description +### [POST] /datasets/{dataset_id}/pipeline/run +**Resource for running a rag pipeline** Run a datasource node for a rag pipeline -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Pipeline run successfully | | 401 | Unauthorized - invalid API token | -### /datasets/{dataset_id}/retrieve - -#### POST -##### Summary - -Perform hit testing on a dataset - -##### Description +### [POST] /datasets/{dataset_id}/retrieve +**Perform hit testing on a dataset** Perform hit testing on a dataset Tests retrieval performance for the specified dataset. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [HitTestingPayload](#hittestingpayload) | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [HitTestingPayload](#hittestingpayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Hit testing results | [HitTestingResponse](#hittestingresponse) | +| 200 | Hit testing results | **application/json**: [HitTestingResponse](#hittestingresponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Dataset not found | | -### /datasets/{dataset_id}/tags - -#### GET -##### Summary - -Get all knowledge type tags - -##### Description +### [GET] /datasets/{dataset_id}/tags +**Get all knowledge type tags** Get tags bound to a specific dataset -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | dataset_id | path | Dataset ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Tags retrieved successfully | [DatasetBoundTagListResponse](#datasetboundtaglistresponse) | +| 200 | Tags retrieved successfully | **application/json**: [DatasetBoundTagListResponse](#datasetboundtaglistresponse)
| | 401 | Unauthorized - invalid API token | | -### /end-users/{end_user_id} - -#### GET -##### Summary - -Get end user detail - -##### Description +### [GET] /end-users/{end_user_id} +**Get end user detail** Get an end user by ID This endpoint is scoped to the current app token's tenant/app to prevent cross-tenant/app access when an end-user ID is known. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | end_user_id | path | End user ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | End user retrieved successfully | [EndUserDetail](#enduserdetail) | +| 200 | End user retrieved successfully | **application/json**: [EndUserDetail](#enduserdetail)
| | 401 | Unauthorized - invalid API token | | | 404 | End user not found | | -### /files/upload - -#### POST -##### Summary - -Upload a file for use in conversations - -##### Description +### [POST] /files/upload +**Upload a file for use in conversations** Upload a file for use in conversations Accepts a single file upload via multipart/form-data. -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | File uploaded successfully | [FileResponse](#fileresponse) | +| 201 | File uploaded successfully | **application/json**: [FileResponse](#fileresponse)
| | 400 | Bad request - no file or invalid file | | | 401 | Unauthorized - invalid API token | | | 413 | File too large | | | 415 | Unsupported file type | | -### /files/{file_id}/preview - -#### GET -##### Summary - -Preview/Download a file that was uploaded via Service API - -##### Description +### [GET] /files/{file_id}/preview +**Preview/Download a file that was uploaded via Service API** Preview or download a file uploaded via Service API Provides secure file preview/download functionality. Files can only be accessed if they belong to messages within the requesting app's context. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [FilePreviewQuery](#filepreviewquery) | | file_id | path | UUID of the file to preview | Yes | string | +| as_attachment | query | Download as attachment | No | boolean | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -1749,20 +1502,16 @@ Files can only be accessed if they belong to messages within the requesting app' | 403 | Forbidden - file access denied | | 404 | File not found | -### /form/human_input/{form_token} - -#### GET -##### Description - +### [GET] /form/human_input/{form_token} Get a paused human input form by token -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | form_token | path | Human input form token | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -1771,19 +1520,22 @@ Get a paused human input form by token | 404 | Form not found | | 412 | Form already submitted or expired | -#### POST -##### Description - +### [POST] /form/human_input/{form_token} Submit a paused human input form by token -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [HumanInputFormSubmitPayload](#humaninputformsubmitpayload) | | form_token | path | Human input form token | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [HumanInputFormSubmitPayload](#humaninputformsubmitpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | @@ -1793,45 +1545,35 @@ Submit a paused human input form by token | 404 | Form not found | | 412 | Form already submitted or expired | -### /info - -#### GET -##### Summary - -Get app information - -##### Description +### [GET] /info +**Get app information** Get basic application information Returns basic information about the application including name, description, tags, and mode. -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Application info retrieved successfully | [AppInfoResponse](#appinforesponse) | +| 200 | Application info retrieved successfully | **application/json**: [AppInfoResponse](#appinforesponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Application not found | | -### /messages - -#### GET -##### Summary - -List messages in a conversation - -##### Description +### [GET] /messages +**List messages in a conversation** List messages in a conversation Retrieves messages with pagination support using first_id. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [MessageListQuery](#messagelistquery) | +| conversation_id | query | Conversation UUID | Yes | string | +| first_id | query | First message ID for pagination | No | | +| limit | query | Number of messages to return (1-100) | No | integer,
**Default:** 20 | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -1839,74 +1581,61 @@ Retrieves messages with pagination support using first_id. | 401 | Unauthorized - invalid API token | | 404 | Conversation or first message not found | -### /messages/{message_id}/feedbacks - -#### POST -##### Summary - -Submit feedback for a message - -##### Description +### [POST] /messages/{message_id}/feedbacks +**Submit feedback for a message** Submit feedback for a message Allows users to rate messages as like/dislike and provide optional feedback content. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [MessageFeedbackPayload](#messagefeedbackpayload) | | message_id | path | Message ID | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [MessageFeedbackPayload](#messagefeedbackpayload)
| + +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Feedback submitted successfully | [ResultResponse](#resultresponse) | +| 200 | Feedback submitted successfully | **application/json**: [ResultResponse](#resultresponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Message not found | | -### /messages/{message_id}/suggested - -#### GET -##### Summary - -Get suggested follow-up questions for a message - -##### Description +### [GET] /messages/{message_id}/suggested +**Get suggested follow-up questions for a message** Get suggested follow-up questions for a message Returns AI-generated follow-up questions based on the message content. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | message_id | path | Message ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Suggested questions retrieved successfully | [SimpleResultStringListResponse](#simpleresultstringlistresponse) | +| 200 | Suggested questions retrieved successfully | **application/json**: [SimpleResultStringListResponse](#simpleresultstringlistresponse)
| | 400 | Suggested questions feature is disabled | | | 401 | Unauthorized - invalid API token | | | 404 | Message not found | | | 500 | Internal server error | | -### /meta - -#### GET -##### Summary - -Get app metadata - -##### Description +### [GET] /meta +**Get app metadata** Get application metadata Returns metadata about the application including configuration and settings. -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -1914,19 +1643,13 @@ Returns metadata about the application including configuration and settings. | 401 | Unauthorized - invalid API token | | 404 | Application not found | -### /parameters - -#### GET -##### Summary - -Retrieve app parameters - -##### Description +### [GET] /parameters +**Retrieve app parameters** Retrieve application input parameters and configuration Returns the input form parameters and configuration for the application. -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -1934,45 +1657,33 @@ Returns the input form parameters and configuration for the application. | 401 | Unauthorized - invalid API token | | 404 | Application not found | -### /site - -#### GET -##### Summary - -Retrieve app site info - -##### Description +### [GET] /site +**Retrieve app site info** Get application site configuration Returns the site configuration for the application including theme, icons, and text. -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Site configuration retrieved successfully | [Site](#site) | +| 200 | Site configuration retrieved successfully | **application/json**: [Site](#site)
| | 401 | Unauthorized - invalid API token | | | 403 | Forbidden - site not found or tenant archived | | -### /text-to-audio - -#### POST -##### Summary - -Convert text to audio using text-to-speech - -##### Description +### [POST] /text-to-audio +**Convert text to audio using text-to-speech** Convert text to audio using text-to-speech Converts the provided text to audio using the specified voice. -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [TextToAudioPayload](#texttoaudiopayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TextToAudioPayload](#texttoaudiopayload)
| -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -1981,14 +1692,10 @@ Converts the provided text to audio using the specified voice. | 401 | Unauthorized - invalid API token | | 500 | Internal server error | -### /workflow/{task_id}/events - -#### GET -##### Description - +### [GET] /workflow/{task_id}/events Get workflow execution events stream after resume -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -1997,7 +1704,7 @@ Get workflow execution events stream after resume | include_state_snapshot | query | Whether to replay from persisted state snapshot, specify `"true"` to include a status snapshot of executed nodes | No | string | | user | query | End user identifier (query param) | No | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -2005,51 +1712,46 @@ Get workflow execution events stream after resume | 401 | Unauthorized - invalid API token | | 404 | Workflow run not found | -### /workflows/logs - -#### GET -##### Summary - -Get workflow app logs - -##### Description +### [GET] /workflows/logs +**Get workflow app logs** Get workflow execution logs Returns paginated workflow execution logs with filtering options. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkflowLogQuery](#workflowlogquery) | +| created_at__after | query | | No | | +| created_at__before | query | | No | | +| created_by_account | query | | No | | +| created_by_end_user_session_id | query | | No | | +| keyword | query | | No | | +| limit | query | | No | integer,
**Default:** 20 | +| page | query | | No | integer,
**Default:** 1 | +| status | query | | No | | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Logs retrieved successfully | [WorkflowAppLogPaginationResponse](#workflowapplogpaginationresponse) | +| 200 | Logs retrieved successfully | **application/json**: [WorkflowAppLogPaginationResponse](#workflowapplogpaginationresponse)
| | 401 | Unauthorized - invalid API token | | -### /workflows/run - -#### POST -##### Summary - -Execute a workflow - -##### Description +### [POST] /workflows/run +**Execute a workflow** Execute a workflow Runs a workflow with the provided inputs and returns the results. Supports both blocking and streaming response modes. -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkflowRunPayload](#workflowrunpayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [WorkflowRunPayload](#workflowrunpayload)
| -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -2060,77 +1762,62 @@ Supports both blocking and streaming response modes. | 429 | Rate limit exceeded | | 500 | Internal server error | -### /workflows/run/{workflow_run_id} - -#### GET -##### Summary - -Get a workflow task running detail - -##### Description +### [GET] /workflows/run/{workflow_run_id} +**Get a workflow task running detail** Get workflow run details Returns detailed information about a specific workflow run. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | workflow_run_id | path | Workflow run ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Workflow run details retrieved successfully | [WorkflowRunResponse](#workflowrunresponse) | +| 200 | Workflow run details retrieved successfully | **application/json**: [WorkflowRunResponse](#workflowrunresponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Workflow run not found | | -### /workflows/tasks/{task_id}/stop +### [POST] /workflows/tasks/{task_id}/stop +**Stop a running workflow task** -#### POST -##### Summary - -Stop a running workflow task - -##### Description - -Stop a running workflow task - -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | task_id | path | Task ID to stop | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Task stopped successfully | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Task stopped successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| | 401 | Unauthorized - invalid API token | | | 404 | Task not found | | -### /workflows/{workflow_id}/run - -#### POST -##### Summary - -Run specific workflow by ID - -##### Description +### [POST] /workflows/{workflow_id}/run +**Run specific workflow by ID** Execute a specific workflow by ID Executes a specific workflow version identified by its ID. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkflowRunPayload](#workflowrunpayload) | | workflow_id | path | Workflow ID to execute | Yes | string | -##### Responses +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [WorkflowRunPayload](#workflowrunpayload)
| + +#### Responses | Code | Description | | ---- | ----------- | @@ -2141,25 +1828,19 @@ Executes a specific workflow version identified by its ID. | 429 | Rate limit exceeded | | 500 | Internal server error | -### /workspaces/current/models/model-types/{model_type} - -#### GET -##### Summary - -Get available models by model type - -##### Description +### [GET] /workspaces/current/models/model-types/{model_type} +**Get available models by model type** Get available models by model type Returns a list of available models for the specified model type. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | model_type | path | Type of model to retrieve | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -2167,7 +1848,7 @@ Returns a list of available models for the specified model type. | 401 | Unauthorized - invalid API token | --- -### Models +### Schemas #### Annotation @@ -2201,8 +1882,8 @@ Returns a list of available models for the specified model type. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | keyword | string | Keyword to search annotations | No | -| limit | integer | Number of annotations per page | No | -| page | integer | Page number | No | +| limit | integer,
**Default:** 20 | Number of annotations per page | No | +| page | integer,
**Default:** 1 | Page number | No | #### AnnotationReplyActionPayload @@ -2226,13 +1907,13 @@ Returns a list of available models for the specified model type. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| auto_generate_name | boolean | Auto generate conversation name | No | +| auto_generate_name | boolean,
**Default:** true | Auto generate conversation name | No | | conversation_id | string | Conversation UUID | No | | files | [ object ] | | No | | inputs | object | | Yes | | query | string | | Yes | -| response_mode | string | *Enum:* `"blocking"`, `"streaming"` | No | -| retriever_from | string | | No | +| response_mode | string | | No | +| retriever_from | string,
**Default:** dev | | No | | trace_session_id | string | Trace session ID for observability grouping | No | | workflow_id | string | Workflow ID for advanced chat | No | @@ -2253,8 +1934,8 @@ Returns a list of available models for the specified model type. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | keyword | string | | No | -| limit | integer | | No | -| page | integer | | No | +| limit | integer,
**Default:** 20 | | No | +| page | integer,
**Default:** 1 | | No | #### ChildChunkListResponse @@ -2292,8 +1973,8 @@ Returns a list of available models for the specified model type. | files | [ object ] | | No | | inputs | object | | Yes | | query | string | | No | -| response_mode | string | *Enum:* `"blocking"`, `"streaming"` | No | -| retriever_from | string | | No | +| response_mode | string | | No | +| retriever_from | string,
**Default:** dev | | No | | trace_session_id | string | Trace session ID for observability grouping | No | #### Condition @@ -2302,7 +1983,7 @@ Condition detail | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| comparison_operator | string | *Enum:* `"<"`, `"="`, `">"`, `"after"`, `"before"`, `"contains"`, `"empty"`, `"end with"`, `"in"`, `"is"`, `"is not"`, `"not contains"`, `"not empty"`, `"not in"`, `"start with"`, `"≠"`, `"≤"`, `"≥"` | Yes | +| comparison_operator | string,
**Available values:** "<", "=", ">", "after", "before", "contains", "empty", "end with", "in", "is", "is not", "not contains", "not empty", "not in", "start with", "≠", "≤", "≥" | *Enum:* `"<"`, `"="`, `">"`, `"after"`, `"before"`, `"contains"`, `"empty"`, `"end with"`, `"in"`, `"is"`, `"is not"`, `"not contains"`, `"not empty"`, `"not in"`, `"start with"`, `"≠"`, `"≤"`, `"≥"` | Yes | | name | string | | Yes | | value | string
[ string ]
integer
number | | No | @@ -2311,8 +1992,8 @@ Condition detail | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | last_id | string | Last conversation ID for pagination | No | -| limit | integer | Number of conversations to return | No | -| sort_by | string | Sort order for conversations
*Enum:* `"-created_at"`, `"-updated_at"`, `"created_at"`, `"updated_at"` | No | +| limit | integer,
**Default:** 20 | Number of conversations to return | No | +| sort_by | string,
**Available values:** "-created_at", "-updated_at", "created_at", "updated_at",
**Default:** -updated_at | Sort order for conversations
*Enum:* `"-created_at"`, `"-updated_at"`, `"created_at"`, `"updated_at"` | No | #### ConversationRenamePayload @@ -2352,7 +2033,7 @@ Condition detail | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | last_id | string | Last variable ID for pagination | No | -| limit | integer | Number of variables to return | No | +| limit | integer,
**Default:** 20 | Number of variables to return | No | | variable_name | string | Filter variables by name | No | #### DatasetBoundTagListResponse @@ -2378,10 +2059,10 @@ Condition detail | embedding_model_provider | string | | No | | external_knowledge_api_id | string | | No | | external_knowledge_id | string | | No | -| indexing_technique | string | *Enum:* `"economy"`, `"high_quality"` | No | +| indexing_technique | string | | No | | name | string | | Yes | | permission | [PermissionEnum](#permissionenum) | | No | -| provider | string | | No | +| provider | string,
**Default:** vendor | | No | | retrieval_model | [RetrievalModel](#retrievalmodel) | | No | | summary_index_setting | object | | No | @@ -2512,8 +2193,8 @@ Condition detail | ---- | ---- | ----------- | -------- | | include_all | boolean | Include all datasets | No | | keyword | string | Search keyword | No | -| limit | integer | Number of items per page | No | -| page | integer | Page number | No | +| limit | integer,
**Default:** 20 | Number of items per page | No | +| page | integer,
**Default:** 1 | Page number | No | | tag_ids | [ string ] | Filter by tag IDs | No | #### DatasetListResponse @@ -2616,7 +2297,7 @@ Condition detail | external_knowledge_api_id | string | | No | | external_knowledge_id | string | | No | | external_retrieval_model | object | | No | -| indexing_technique | string | *Enum:* `"economy"`, `"high_quality"` | No | +| indexing_technique | string | | No | | name | string | | No | | partial_member_list | [ object ] | | No | | permission | [PermissionEnum](#permissionenum) | | No | @@ -2667,8 +2348,8 @@ Request payload for bulk downloading documents as a zip archive. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | keyword | string | Search keyword | No | -| limit | integer | Number of items per page | No | -| page | integer | Page number | No | +| limit | integer,
**Default:** 20 | Number of items per page | No | +| page | integer,
**Default:** 1 | Page number | No | | status | string | Document status filter | No | #### DocumentListResponse @@ -2754,8 +2435,8 @@ Request payload for bulk downloading documents as a zip archive. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| doc_form | string | | No | -| doc_language | string | | No | +| doc_form | string,
**Default:** text_model | | No | +| doc_language | string,
**Default:** English | | No | | embedding_model | string | | No | | embedding_model_provider | string | | No | | indexing_technique | string | | No | @@ -2769,8 +2450,8 @@ Request payload for bulk downloading documents as a zip archive. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| doc_form | string | | No | -| doc_language | string | | No | +| doc_form | string,
**Default:** text_model | | No | +| doc_language | string,
**Default:** English | | No | | name | string | | No | | process_rule | [ProcessRule](#processrule) | | No | | retrieval_model | [RetrievalModel](#retrievalmodel) | | No | @@ -2801,8 +2482,8 @@ Note: The SQLAlchemy model defines an `is_anonymous` property for Flask-Login se | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| limit | integer | Number of feedbacks per page | No | -| page | integer | Page number | No | +| limit | integer,
**Default:** 20 | Number of feedbacks per page | No | +| page | integer,
**Default:** 1 | Page number | No | #### FilePreviewQuery @@ -2962,7 +2643,7 @@ Note: The SQLAlchemy model defines an `is_anonymous` property for Flask-Login se | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | content | string | | No | -| rating | string | *Enum:* `"dislike"`, `"like"` | No | +| rating | string | | No | #### MessageListQuery @@ -2970,14 +2651,14 @@ Note: The SQLAlchemy model defines an `is_anonymous` property for Flask-Login se | ---- | ---- | ----------- | -------- | | conversation_id | string | Conversation UUID | Yes | | first_id | string | First message ID for pagination | No | -| limit | integer | Number of messages to return (1-100) | No | +| limit | integer,
**Default:** 20 | Number of messages to return (1-100) | No | #### MetadataArgs | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | name | string | | Yes | -| type | string | *Enum:* `"number"`, `"string"`, `"time"` | Yes | +| type | string,
**Available values:** "number", "string", "time" | *Enum:* `"number"`, `"string"`, `"time"` | Yes | #### MetadataDetail @@ -2994,7 +2675,7 @@ Metadata Filtering Condition. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | conditions | [ [Condition](#condition) ] | | No | -| logical_operator | string | *Enum:* `"and"`, `"or"` | No | +| logical_operator | string | | No | #### MetadataOperationData @@ -3088,7 +2769,7 @@ Dataset Process Rule Mode | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| parent_mode | string | *Enum:* `"full-doc"`, `"paragraph"` | No | +| parent_mode | string | | No | | pre_processing_rules | [ [PreProcessingRule](#preprocessingrule) ] | | No | | segmentation | [Segmentation](#segmentation) | | No | | subchunk_segmentation | [Segmentation](#segmentation) | | No | @@ -3138,8 +2819,8 @@ Dataset Process Rule Mode | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | keyword | string | | No | -| limit | integer | | No | -| page | integer | | No | +| limit | integer,
**Default:** 20 | | No | +| page | integer,
**Default:** 1 | | No | | status | [ string ] | | No | #### SegmentListResponse @@ -3209,7 +2890,8 @@ Dataset Process Rule Mode | ---- | ---- | ----------- | -------- | | chunk_overlap | integer | | No | | max_tokens | integer | | Yes | -| separator | string | | No | +| separator | string,
**Default:** + | | No | #### SimpleAccount @@ -3323,7 +3005,7 @@ Accept the legacy single-tag Service API payload while exposing a normalized tag | ---- | ---- | ----------- | -------- | | keyword_setting | [WeightKeywordSetting](#weightkeywordsetting) | | No | | vector_setting | [WeightVectorSetting](#weightvectorsetting) | | No | -| weight_type | string | *Enum:* `"customized"`, `"keyword_first"`, `"semantic_first"` | No | +| weight_type | string | | No | #### WeightVectorSetting @@ -3365,9 +3047,9 @@ Accept the legacy single-tag Service API payload while exposing a normalized tag | created_by_account | string | | No | | created_by_end_user_session_id | string | | No | | keyword | string | | No | -| limit | integer | | No | -| page | integer | | No | -| status | string | *Enum:* `"failed"`, `"stopped"`, `"succeeded"` | No | +| limit | integer,
**Default:** 20 | | No | +| page | integer,
**Default:** 1 | | No | +| status | string | | No | #### WorkflowRunForLogResponse @@ -3391,7 +3073,7 @@ Accept the legacy single-tag Service API payload while exposing a normalized tag | ---- | ---- | ----------- | -------- | | files | [ object ] | | No | | inputs | object | | Yes | -| response_mode | string | *Enum:* `"blocking"`, `"streaming"` | No | +| response_mode | string | | No | | trace_session_id | string | Trace session ID for observability grouping | No | #### WorkflowRunResponse diff --git a/api/openapi/markdown/web-swagger.md b/api/openapi/markdown/web-openapi.md similarity index 73% rename from api/openapi/markdown/web-swagger.md rename to api/openapi/markdown/web-openapi.md index d24dc48ee69..5903d56b193 100644 --- a/api/openapi/markdown/web-swagger.md +++ b/api/openapi/markdown/web-openapi.md @@ -3,31 +3,22 @@ Public APIs for web applications including file uploads, chat interactions, and ## Version: 1.0 -### Security -**Bearer** - -| apiKey | *API Key* | -| ------ | --------- | -| Description | Type: Bearer {your-api-key} | -| In | header | -| Name | Authorization | +### Available authorizations +#### Bearer (API Key Authentication) +Type: Bearer {your-api-key} +**Name:** Authorization +**In:** header --- ## web Web application API operations -### /audio-to-text - -#### POST -##### Summary - -Convert audio to text - -##### Description +### [POST] /audio-to-text +**Convert audio to text** Convert audio file to text using speech-to-text service. -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -39,20 +30,16 @@ Convert audio file to text using speech-to-text service. | 415 | Unsupported audio type | | 500 | Internal Server Error | -### /chat-messages - -#### POST -##### Description - +### [POST] /chat-messages Create a chat message for conversational applications. -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ChatMessagePayload](#chatmessagepayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ChatMessagePayload](#chatmessagepayload)
| -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -63,44 +50,36 @@ Create a chat message for conversational applications. | 404 | App Not Found | | 500 | Internal Server Error | -### /chat-messages/{task_id}/stop - -#### POST -##### Description - +### [POST] /chat-messages/{task_id}/stop Stop a running chat message task. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | task_id | path | Task ID to stop | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| | 400 | Bad Request | | | 401 | Unauthorized | | | 403 | Forbidden | | | 404 | Task Not Found | | | 500 | Internal Server Error | | -### /completion-messages - -#### POST -##### Description - +### [POST] /completion-messages Create a completion message for text generation applications. -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [CompletionMessagePayload](#completionmessagepayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [CompletionMessagePayload](#completionmessagepayload)
| -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -111,47 +90,39 @@ Create a completion message for text generation applications. | 404 | App Not Found | | 500 | Internal Server Error | -### /completion-messages/{task_id}/stop - -#### POST -##### Description - +### [POST] /completion-messages/{task_id}/stop Stop a running completion message task. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | task_id | path | Task ID to stop | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| | 400 | Bad Request | | | 401 | Unauthorized | | | 403 | Forbidden | | | 404 | Task Not Found | | | 500 | Internal Server Error | | -### /conversations - -#### GET -##### Description - +### [GET] /conversations Retrieve paginated list of conversations for a chat application. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | last_id | query | Last conversation ID for pagination | No | string | -| limit | query | Number of conversations to return (1-100) | No | integer | -| pinned | query | Filter by pinned status | No | string | -| sort_by | query | Sort order | No | string | +| limit | query | Number of conversations to return (1-100) | No | integer,
**Default:** 20 | +| pinned | query | Filter by pinned status | No | string,
**Available values:** "false", "true" | +| sort_by | query | Sort order | No | string,
**Available values:** "-created_at", "-updated_at", "created_at", "updated_at",
**Default:** -updated_at | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -162,20 +133,16 @@ Retrieve paginated list of conversations for a chat application. | 404 | App Not Found or Not a Chat App | | 500 | Internal Server Error | -### /conversations/{c_id} - -#### DELETE -##### Description - +### [DELETE] /conversations/{c_id} Delete a specific conversation. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | c_id | path | Conversation UUID | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -186,14 +153,10 @@ Delete a specific conversation. | 404 | Conversation Not Found or Not a Chat App | | 500 | Internal Server Error | -### /conversations/{c_id}/name - -#### POST -##### Description - +### [POST] /conversations/{c_id}/name Rename a specific conversation with a custom name or auto-generate one. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | @@ -201,7 +164,7 @@ Rename a specific conversation with a custom name or auto-generate one. | auto_generate | query | Auto-generate conversation name | No | boolean | | name | query | New conversation name | No | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -212,105 +175,83 @@ Rename a specific conversation with a custom name or auto-generate one. | 404 | Conversation Not Found or Not a Chat App | | 500 | Internal Server Error | -### /conversations/{c_id}/pin - -#### PATCH -##### Description - +### [PATCH] /conversations/{c_id}/pin Pin a specific conversation to keep it at the top of the list. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | c_id | path | Conversation UUID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Conversation pinned successfully | [ResultResponse](#resultresponse) | +| 200 | Conversation pinned successfully | **application/json**: [ResultResponse](#resultresponse)
| | 400 | Bad Request | | | 401 | Unauthorized | | | 403 | Forbidden | | | 404 | Conversation Not Found or Not a Chat App | | | 500 | Internal Server Error | | -### /conversations/{c_id}/unpin - -#### PATCH -##### Description - +### [PATCH] /conversations/{c_id}/unpin Unpin a specific conversation to remove it from the top of the list. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | c_id | path | Conversation UUID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Conversation unpinned successfully | [ResultResponse](#resultresponse) | +| 200 | Conversation unpinned successfully | **application/json**: [ResultResponse](#resultresponse)
| | 400 | Bad Request | | | 401 | Unauthorized | | | 403 | Forbidden | | | 404 | Conversation Not Found or Not a Chat App | | | 500 | Internal Server Error | | -### /email-code-login - -#### POST -##### Description - +### [POST] /email-code-login Send email verification code for login -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [EmailCodeLoginSendPayload](#emailcodeloginsendpayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [EmailCodeLoginSendPayload](#emailcodeloginsendpayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Email code sent successfully | [SimpleResultDataResponse](#simpleresultdataresponse) | +| 200 | Email code sent successfully | **application/json**: [SimpleResultDataResponse](#simpleresultdataresponse)
| | 400 | Bad request - invalid email format | | | 404 | Account not found | | -### /email-code-login/validity - -#### POST -##### Description - +### [POST] /email-code-login/validity Verify email code and complete login -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [EmailCodeLoginVerifyPayload](#emailcodeloginverifypayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [EmailCodeLoginVerifyPayload](#emailcodeloginverifypayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Email code verified and login successful | [AccessTokenResultResponse](#accesstokenresultresponse) | +| 200 | Email code verified and login successful | **application/json**: [AccessTokenResultResponse](#accesstokenresultresponse)
| | 400 | Bad request - invalid code or token | | | 401 | Invalid token or expired code | | | 404 | Account not found | | -### /files/upload - -#### POST -##### Summary - -Upload a file for use in web applications - -##### Description +### [POST] /files/upload +**Upload a file for use in web applications** Upload a file for use in web applications Accepts file uploads for use within web applications, supporting @@ -335,109 +276,87 @@ Raises: FileTooLargeError: File exceeds size limit UnsupportedFileTypeError: File type not supported -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | File uploaded successfully | [FileResponse](#fileresponse) | +| 201 | File uploaded successfully | **application/json**: [FileResponse](#fileresponse)
| | 400 | Bad request - invalid file or parameters | | | 413 | File too large | | | 415 | Unsupported file type | | -### /forgot-password - -#### POST -##### Description - +### [POST] /forgot-password Send password reset email -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ForgotPasswordSendPayload](#forgotpasswordsendpayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ForgotPasswordSendPayload](#forgotpasswordsendpayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Password reset email sent successfully | [SimpleResultDataResponse](#simpleresultdataresponse) | +| 200 | Password reset email sent successfully | **application/json**: [SimpleResultDataResponse](#simpleresultdataresponse)
| | 400 | Bad request - invalid email format | | | 404 | Account not found | | | 429 | Too many requests - rate limit exceeded | | -### /forgot-password/resets - -#### POST -##### Description - +### [POST] /forgot-password/resets Reset user password with verification token -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ForgotPasswordResetPayload](#forgotpasswordresetpayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ForgotPasswordResetPayload](#forgotpasswordresetpayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Password reset successfully | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Password reset successfully | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| | 400 | Bad request - invalid parameters or password mismatch | | | 401 | Invalid or expired token | | | 404 | Account not found | | -### /forgot-password/validity - -#### POST -##### Description - +### [POST] /forgot-password/validity Verify password reset token validity -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [ForgotPasswordCheckPayload](#forgotpasswordcheckpayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [ForgotPasswordCheckPayload](#forgotpasswordcheckpayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Token is valid | [VerificationTokenResponse](#verificationtokenresponse) | +| 200 | Token is valid | **application/json**: [VerificationTokenResponse](#verificationtokenresponse)
| | 400 | Bad request - invalid token format | | | 401 | Invalid or expired token | | -### /form/human_input/{form_token} - -#### GET -##### Summary - -Get human input form definition by token - -##### Description +### [GET] /form/human_input/{form_token} +**Get human input form definition by token** GET /api/form/human_input/ -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | form_token | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -#### POST -##### Summary - -Submit human input form by token - -##### Description +### [POST] /form/human_input/{form_token} +**Submit human input form by token** POST /api/form/human_input/ @@ -449,124 +368,96 @@ Request body: "action": "Approve" } -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | form_token | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /form/human_input/{form_token}/upload-token - -#### POST -##### Summary - -Issue an upload token for a human input form - -##### Description +### [POST] /form/human_input/{form_token}/upload-token +**Issue an upload token for a human input form** POST /api/form/human_input//upload-token -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | form_token | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | -### /human-input-forms/files +### [POST] /human-input-forms/files +**Upload one local file or remote URL file for a HITL human input form** -#### POST -##### Summary - -Upload one local file or remote URL file for a HITL human input form - -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | File uploaded successfully | [FileResponse](#fileresponse) | +| 201 | File uploaded successfully | **application/json**: [FileResponse](#fileresponse)
| -### /login - -#### POST -##### Summary - -Authenticate user and login - -##### Description +### [POST] /login +**Authenticate user and login** Authenticate user for web application access -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [LoginPayload](#loginpayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [LoginPayload](#loginpayload)
| -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Authentication successful | [AccessTokenResultResponse](#accesstokenresultresponse) | +| 200 | Authentication successful | **application/json**: [AccessTokenResultResponse](#accesstokenresultresponse)
| | 400 | Bad request - invalid email or password format | | | 401 | Authentication failed - email or password mismatch | | | 403 | Account banned or login disabled | | | 404 | Account not found | | -### /login/status - -#### GET -##### Description - +### [GET] /login/status Check login status -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Login status | [LoginStatusResponse](#loginstatusresponse) | +| 200 | Login status | **application/json**: [LoginStatusResponse](#loginstatusresponse)
| | 401 | Login status | | -### /logout - -#### POST -##### Description - +### [POST] /logout Logout user from web application -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Logout successful | [SimpleResultResponse](#simpleresultresponse) | - -### /messages - -#### GET -##### Description +| 200 | Logout successful | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| +### [GET] /messages Retrieve paginated list of messages from a conversation in a chat application. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | conversation_id | query | Conversation UUID | Yes | string | | first_id | query | First message ID for pagination | No | string | -| limit | query | Number of messages to return (1-100) | No | integer | +| limit | query | Number of messages to return (1-100) | No | integer,
**Default:** 20 | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -577,47 +468,39 @@ Retrieve paginated list of messages from a conversation in a chat application. | 404 | Conversation Not Found or Not a Chat App | | 500 | Internal Server Error | -### /messages/{message_id}/feedbacks - -#### POST -##### Description - +### [POST] /messages/{message_id}/feedbacks Submit feedback (like/dislike) for a specific message. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | message_id | path | Message UUID | Yes | string | | content | query | Feedback content | No | string | -| rating | query | Feedback rating | No | string | +| rating | query | Feedback rating | No | string,
**Available values:** "dislike", "like" | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Feedback submitted successfully | [ResultResponse](#resultresponse) | +| 200 | Feedback submitted successfully | **application/json**: [ResultResponse](#resultresponse)
| | 400 | Bad Request | | | 401 | Unauthorized | | | 403 | Forbidden | | | 404 | Message Not Found | | | 500 | Internal Server Error | | -### /messages/{message_id}/more-like-this - -#### GET -##### Description - +### [GET] /messages/{message_id}/more-like-this Generate a new completion similar to an existing message (completion apps only). -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | +| response_mode | query | Response mode | Yes | string,
**Available values:** "blocking", "streaming" | | message_id | path | | Yes | string | -| payload | body | | Yes | [MessageMoreLikeThisQuery](#messagemorelikethisquery) | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -628,42 +511,32 @@ Generate a new completion similar to an existing message (completion apps only). | 404 | Message Not Found | | 500 | Internal Server Error | -### /messages/{message_id}/suggested-questions - -#### GET -##### Description - +### [GET] /messages/{message_id}/suggested-questions Get suggested follow-up questions after a message (chat apps only). -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | message_id | path | Message UUID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SuggestedQuestionsResponse](#suggestedquestionsresponse) | +| 200 | Success | **application/json**: [SuggestedQuestionsResponse](#suggestedquestionsresponse)
| | 400 | Bad Request - Not a chat app or feature disabled | | | 401 | Unauthorized | | | 403 | Forbidden | | | 404 | Message Not Found or Conversation Not Found | | | 500 | Internal Server Error | | -### /meta - -#### GET -##### Summary - -Get app meta - -##### Description +### [GET] /meta +**Get app meta** Retrieve the metadata for a specific app. -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -674,18 +547,12 @@ Retrieve the metadata for a specific app. | 404 | App Not Found | | 500 | Internal Server Error | -### /parameters - -#### GET -##### Summary - -Retrieve app parameters - -##### Description +### [GET] /parameters +**Retrieve app parameters** Retrieve the parameters for a specific app. -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -696,14 +563,10 @@ Retrieve the parameters for a specific app. | 404 | App Not Found | | 500 | Internal Server Error | -### /passport - -#### GET -##### Description - +### [GET] /passport Get authentication passport for web application access -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -711,14 +574,8 @@ Get authentication passport for web application access | 401 | Unauthorized - missing app code or invalid authentication | | 404 | Application or user not found | -### /remote-files/upload - -#### POST -##### Summary - -Upload a file from a remote URL - -##### Description +### [POST] /remote-files/upload +**Upload a file from a remote URL** Upload a file from a remote URL Downloads a file from the provided remote URL and uploads it @@ -740,24 +597,18 @@ Raises: FileTooLargeError: File exceeds size limit UnsupportedFileTypeError: File type not supported -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | Remote file uploaded successfully | [FileWithSignedUrl](#filewithsignedurl) | +| 201 | Remote file uploaded successfully | **application/json**: [FileWithSignedUrl](#filewithsignedurl)
| | 400 | Bad request - invalid URL or parameters | | | 413 | File too large | | | 415 | Unsupported file type | | | 500 | Failed to fetch remote file | | -### /remote-files/{url} - -#### GET -##### Summary - -Get information about a remote file - -##### Description +### [GET] /remote-files/{url} +**Get information about a remote file** Get information about a remote file Retrieves basic information about a file located at a remote URL, @@ -774,36 +625,32 @@ Returns: Raises: HTTPException: If the remote file cannot be accessed -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | url | path | | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Remote file information retrieved successfully | [RemoteFileInfo](#remotefileinfo) | +| 200 | Remote file information retrieved successfully | **application/json**: [RemoteFileInfo](#remotefileinfo)
| | 400 | Bad request - invalid URL | | | 404 | Remote file not found | | | 500 | Failed to fetch remote file | | -### /saved-messages - -#### GET -##### Description - +### [GET] /saved-messages Retrieve paginated list of saved messages for a completion application. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | last_id | query | Last message ID for pagination | No | string | -| limit | query | Number of messages to return (1-100) | No | integer | +| limit | query | Number of messages to return (1-100) | No | integer,
**Default:** 20 | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -814,42 +661,36 @@ Retrieve paginated list of saved messages for a completion application. | 404 | App Not Found | | 500 | Internal Server Error | -#### POST -##### Description - +### [POST] /saved-messages Save a specific message for later reference. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | message_id | query | Message UUID to save | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Message saved successfully | [ResultResponse](#resultresponse) | +| 200 | Message saved successfully | **application/json**: [ResultResponse](#resultresponse)
| | 400 | Bad Request - Not a completion app | | | 401 | Unauthorized | | | 403 | Forbidden | | | 404 | Message Not Found | | | 500 | Internal Server Error | | -### /saved-messages/{message_id} - -#### DELETE -##### Description - +### [DELETE] /saved-messages/{message_id} Remove a message from saved messages. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | message_id | path | Message UUID to delete | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -860,18 +701,12 @@ Remove a message from saved messages. | 404 | Message Not Found | | 500 | Internal Server Error | -### /site - -#### GET -##### Summary - -Retrieve app site info - -##### Description +### [GET] /site +**Retrieve app site info** Retrieve app site information and configuration. -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -882,14 +717,8 @@ Retrieve app site information and configuration. | 404 | App Not Found | | 500 | Internal Server Error | -### /system-features - -#### GET -##### Summary - -Get system feature flags and configuration - -##### Description +### [GET] /system-features +**Get system feature flags and configuration** Get system feature flags and configuration Returns the current system feature flags and configuration @@ -908,31 +737,25 @@ Authentication would create circular dependency (can't authenticate without weba Only non-sensitive configuration data should be returned by this endpoint. -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | System features retrieved successfully | [SystemFeatureModel](#systemfeaturemodel) | +| 200 | System features retrieved successfully | **application/json**: [SystemFeatureModel](#systemfeaturemodel)
| | 500 | Internal server error | | -### /text-to-audio - -#### POST -##### Summary - -Convert text to audio - -##### Description +### [POST] /text-to-audio +**Convert text to audio** Convert text to audio using text-to-speech service. -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [TextToAudioPayload](#texttoaudiopayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [TextToAudioPayload](#texttoaudiopayload)
| -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -942,68 +765,54 @@ Convert text to audio using text-to-speech service. | 403 | Forbidden | | 500 | Internal Server Error | -### /webapp/access-mode - -#### GET -##### Description - +### [GET] /webapp/access-mode Retrieve the access mode for a web application (public or restricted). -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | appCode | query | Application code | No | string | | appId | query | Application ID | No | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [AccessModeResponse](#accessmoderesponse) | +| 200 | Success | **application/json**: [AccessModeResponse](#accessmoderesponse)
| | 400 | Bad Request | | | 500 | Internal Server Error | | -### /webapp/permission - -#### GET -##### Description - +### [GET] /webapp/permission Check if user has permission to access a web application. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | appId | query | Application ID | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [BooleanResultResponse](#booleanresultresponse) | +| 200 | Success | **application/json**: [BooleanResultResponse](#booleanresultresponse)
| | 400 | Bad Request | | | 401 | Unauthorized | | | 500 | Internal Server Error | | -### /workflows/run - -#### POST -##### Summary - -Run workflow - -##### Description +### [POST] /workflows/run +**Run workflow** Execute a workflow with provided inputs and files. -##### Parameters +#### Request Body -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| payload | body | | Yes | [WorkflowRunPayload](#workflowrunpayload) | +| Required | Schema | +| -------- | ------ | +| Yes | **application/json**: [WorkflowRunPayload](#workflowrunpayload)
| -##### Responses +#### Responses | Code | Description | | ---- | ----------- | @@ -1014,28 +823,22 @@ Execute a workflow with provided inputs and files. | 404 | App Not Found | | 500 | Internal Server Error | -### /workflows/tasks/{task_id}/stop - -#### POST -##### Summary - -Stop workflow task - -##### Description +### [POST] /workflows/tasks/{task_id}/stop +**Stop workflow task** Stop a running workflow task. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | task_id | path | Task ID to stop | Yes | string | -##### Responses +#### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | [SimpleResultResponse](#simpleresultresponse) | +| 200 | Success | **application/json**: [SimpleResultResponse](#simpleresultresponse)
| | 400 | Bad Request | | | 401 | Unauthorized | | | 403 | Forbidden | | @@ -1046,33 +849,27 @@ Stop a running workflow task. ## default Default namespace -### /workflow/{task_id}/events - -#### GET -##### Summary - -Get workflow execution events stream after resume - -##### Description +### [GET] /workflow/{task_id}/events +**Get workflow execution events stream after resume** GET /api/workflow//events Returns Server-Sent Events stream. -##### Parameters +#### Parameters | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | task_id | path | | Yes | string | -##### Responses +#### Responses | Code | Description | | ---- | ----------- | | 200 | Success | --- -### Models +### Schemas #### AccessModeResponse @@ -1125,8 +922,8 @@ Returns Server-Sent Events stream. | inputs | object | Input variables for the chat | Yes | | parent_message_id | string | Parent message ID | No | | query | string | User query/message | Yes | -| response_mode | string | Response mode: blocking or streaming
*Enum:* `"blocking"`, `"streaming"` | No | -| retriever_from | string | Source of retriever | No | +| response_mode | string | Response mode: blocking or streaming | No | +| retriever_from | string,
**Default:** web_app | Source of retriever | No | #### CompletionMessagePayload @@ -1135,17 +932,17 @@ Returns Server-Sent Events stream. | files | [ object ] | Files to be processed | No | | inputs | object | Input variables for the completion | Yes | | query | string | Query text for completion | No | -| response_mode | string | Response mode: blocking or streaming
*Enum:* `"blocking"`, `"streaming"` | No | -| retriever_from | string | Source of retriever | No | +| response_mode | string | Response mode: blocking or streaming | No | +| retriever_from | string,
**Default:** web_app | Source of retriever | No | #### ConversationListQuery | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | last_id | string | | No | -| limit | integer | | No | +| limit | integer,
**Default:** 20 | | No | | pinned | boolean | | No | -| sort_by | string | *Enum:* `"-created_at"`, `"-updated_at"`, `"created_at"`, `"updated_at"` | No | +| sort_by | string,
**Available values:** "-created_at", "-updated_at", "created_at", "updated_at",
**Default:** -updated_at | *Enum:* `"-created_at"`, `"-updated_at"`, `"created_at"`, `"updated_at"` | No | #### ConversationRenamePayload @@ -1231,7 +1028,7 @@ Parsed multipart form fields for HITL uploads. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| url | string (uri) | Remote file URL | No | +| url | string | Remote file URL | No | #### HumanInputUploadTokenResponse @@ -1285,7 +1082,7 @@ Parsed multipart form fields for HITL uploads. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | content | string | | No | -| rating | string | *Enum:* `"dislike"`, `"like"` | No | +| rating | string | | No | #### MessageListQuery @@ -1293,13 +1090,13 @@ Parsed multipart form fields for HITL uploads. | ---- | ---- | ----------- | -------- | | conversation_id | string | Conversation UUID | Yes | | first_id | string | First message ID for pagination | No | -| limit | integer | Number of messages to return (1-100) | No | +| limit | integer,
**Default:** 20 | Number of messages to return (1-100) | No | #### MessageMoreLikeThisQuery | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| response_mode | string | Response mode
*Enum:* `"blocking"`, `"streaming"` | Yes | +| response_mode | string,
**Available values:** "blocking", "streaming" | Response mode
*Enum:* `"blocking"`, `"streaming"` | Yes | #### PluginInstallationPermissionModel @@ -1350,7 +1147,7 @@ Parsed multipart form fields for HITL uploads. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | last_id | string | | No | -| limit | integer | | No | +| limit | integer,
**Default:** 20 | | No | #### SimpleResultDataResponse @@ -1376,11 +1173,11 @@ Parsed multipart form fields for HITL uploads. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | branding | [BrandingModel](#brandingmodel) | | Yes | -| enable_change_email | boolean | | Yes | -| enable_collaboration_mode | boolean | | Yes | +| enable_change_email | boolean,
**Default:** true | | Yes | +| enable_collaboration_mode | boolean,
**Default:** true | | Yes | | enable_creators_platform | boolean | | Yes | | enable_email_code_login | boolean | | Yes | -| enable_email_password_login | boolean | | Yes | +| enable_email_password_login | boolean,
**Default:** true | | Yes | | enable_explore_banner | boolean | | Yes | | enable_marketplace | boolean | | Yes | | enable_social_oauth_login | boolean | | Yes | @@ -1389,7 +1186,7 @@ Parsed multipart form fields for HITL uploads. | is_allow_register | boolean | | Yes | | is_email_setup | boolean | | Yes | | license | [LicenseModel](#licensemodel) | | Yes | -| max_plugin_package_size | integer | | Yes | +| max_plugin_package_size | integer,
**Default:** 15728640 | | Yes | | plugin_installation_permission | [PluginInstallationPermissionModel](#plugininstallationpermissionmodel) | | Yes | | plugin_manager | [PluginManagerModel](#pluginmanagermodel) | | Yes | | sso_enforced_for_signin | boolean | | Yes | diff --git a/api/pyproject.toml b/api/pyproject.toml index 14f16a8ee59..8e4ebe4112e 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -60,6 +60,7 @@ exclude = ["providers/vdb/__pycache__", "providers/trace/__pycache__"] [tool.uv.sources] dify-agent = { path = "../dify-agent", editable = true } +flask-restx = { git = "https://github.com/asukaminato0721/flask-restx", rev = "27758e26f8f740d7525d5039c51a9e524b6e2b68" } dify-vdb-alibabacloud-mysql = { workspace = true } dify-vdb-analyticdb = { workspace = true } dify-vdb-baidu = { workspace = true } diff --git a/api/tests/unit_tests/commands/test_generate_swagger_markdown_docs.py b/api/tests/unit_tests/commands/test_generate_swagger_markdown_docs.py index 4da03b2a883..8444af741fb 100644 --- a/api/tests/unit_tests/commands/test_generate_swagger_markdown_docs.py +++ b/api/tests/unit_tests/commands/test_generate_swagger_markdown_docs.py @@ -22,7 +22,7 @@ def _load_generate_swagger_markdown_docs_module(): def test_generate_markdown_docs_keeps_split_docs_and_merges_fastopenapi_into_console(tmp_path, monkeypatch): module = _load_generate_swagger_markdown_docs_module() - swagger_dir = tmp_path / "openapi" + openapi_dir = tmp_path / "openapi" markdown_dir = tmp_path / "markdown" stale_combined_doc = markdown_dir / "api-reference.md" markdown_dir.mkdir() @@ -50,23 +50,23 @@ def test_generate_markdown_docs_keeps_split_docs_and_merges_fastopenapi_into_con monkeypatch.setattr(module, "generate_fastopenapi_specs", write_fastopenapi_specs) monkeypatch.setattr(module, "_convert_spec_to_markdown", convert_spec_to_markdown) - written_paths = module.generate_markdown_docs(swagger_dir, markdown_dir) + written_paths = module.generate_markdown_docs(openapi_dir, markdown_dir) assert [path.name for path in written_paths] == [ - "console-swagger.md", - "web-swagger.md", - "service-swagger.md", - "openapi-swagger.md", + "console-openapi.md", + "web-openapi.md", + "service-openapi.md", + "openapi-openapi.md", ] assert not stale_combined_doc.exists() - assert not list(swagger_dir.glob("*.json")) + assert not list(openapi_dir.glob("*.json")) - console_markdown = (markdown_dir / "console-swagger.md").read_text(encoding="utf-8") - assert "## FastOpenAPI Preview (OpenAPI 3.0)" in console_markdown + console_markdown = (markdown_dir / "console-openapi.md").read_text(encoding="utf-8") + assert "## FastOpenAPI Preview (OpenAPI 3.1)" in console_markdown assert "### fastopenapi-console-openapi" in console_markdown assert "#### Routes" in console_markdown - assert "FastOpenAPI Preview" not in (markdown_dir / "web-swagger.md").read_text(encoding="utf-8") - assert "FastOpenAPI Preview" not in (markdown_dir / "service-swagger.md").read_text(encoding="utf-8") + assert "FastOpenAPI Preview" not in (markdown_dir / "web-openapi.md").read_text(encoding="utf-8") + assert "FastOpenAPI Preview" not in (markdown_dir / "service-openapi.md").read_text(encoding="utf-8") def test_generate_markdown_docs_only_removes_generated_specs_from_separate_swagger_dir(tmp_path, monkeypatch): @@ -107,39 +107,41 @@ def test_generate_markdown_docs_only_removes_generated_specs_from_separate_swagg def test_patch_union_schema_markdown_fills_converter_blank_schema_types(tmp_path): module = _load_generate_swagger_markdown_docs_module() - spec_path = tmp_path / "console-swagger.json" + spec_path = tmp_path / "console-openapi.json" spec_path.write_text( json.dumps( { - "definitions": { - "FormInputConfig": { - "oneOf": [ - {"$ref": "#/definitions/ParagraphInputConfig"}, - {"$ref": "#/definitions/SelectInputConfig"}, - {"$ref": "#/definitions/FileInputConfig"}, - ], - }, - "ParagraphInputConfig": { - "properties": { - "default": { - "anyOf": [ - {"$ref": "#/definitions/StringSource"}, - {"type": "null"}, - ], + "components": { + "schemas": { + "FormInputConfig": { + "oneOf": [ + {"$ref": "#/components/schemas/ParagraphInputConfig"}, + {"$ref": "#/components/schemas/SelectInputConfig"}, + {"$ref": "#/components/schemas/FileInputConfig"}, + ], + }, + "ParagraphInputConfig": { + "properties": { + "default": { + "anyOf": [ + {"$ref": "#/components/schemas/StringSource"}, + {"type": "null"}, + ], + }, + "output_variable_name": {"type": "string"}, }, - "output_variable_name": {"type": "string"}, }, - }, - "SelectInputConfig": { - "properties": { - "option_source": {"$ref": "#/definitions/StringListSource"}, + "SelectInputConfig": { + "properties": { + "option_source": {"$ref": "#/components/schemas/StringListSource"}, + }, }, - }, - "FileInputConfig": { - "properties": { - "allowed_file_types": { - "type": "array", - "items": {"$ref": "#/definitions/FileType"}, + "FileInputConfig": { + "properties": { + "allowed_file_types": { + "type": "array", + "items": {"$ref": "#/components/schemas/FileType"}, + }, }, }, }, @@ -188,24 +190,26 @@ def test_patch_union_schema_markdown_fills_converter_blank_schema_types(tmp_path assert "| allowed_file_types | [ [FileType](#filetype) ] | | No |" in patched -def test_patch_union_schema_markdown_fills_regular_definition_union_property(tmp_path): +def test_patch_union_schema_markdown_fills_regular_schema_union_property(tmp_path): module = _load_generate_swagger_markdown_docs_module() - spec_path = tmp_path / "service-swagger.json" + spec_path = tmp_path / "service-openapi.json" spec_path.write_text( json.dumps( { - "definitions": { - "DocumentMetadataResponse": { - "properties": { - "id": {"type": "string"}, - "value": { - "anyOf": [ - {"type": "string"}, - {"type": "integer"}, - {"type": "number"}, - {"type": "boolean"}, - {"type": "null"}, - ], + "components": { + "schemas": { + "DocumentMetadataResponse": { + "properties": { + "id": {"type": "string"}, + "value": { + "anyOf": [ + {"type": "string"}, + {"type": "integer"}, + {"type": "number"}, + {"type": "boolean"}, + {"type": "null"}, + ], + }, }, }, }, @@ -227,9 +231,9 @@ def test_patch_union_schema_markdown_fills_regular_definition_union_property(tmp assert "| value | string
integer
number
boolean | | No |" in patched -def test_patch_union_schema_markdown_ignores_specs_without_definitions(tmp_path): +def test_patch_union_schema_markdown_ignores_specs_without_schemas(tmp_path): module = _load_generate_swagger_markdown_docs_module() - spec_path = tmp_path / "console-swagger.json" + spec_path = tmp_path / "console-openapi.json" spec_path.write_text("{}", encoding="utf-8") assert module._patch_union_schema_markdown("unchanged", spec_path) == "unchanged" @@ -237,27 +241,29 @@ def test_patch_union_schema_markdown_ignores_specs_without_definitions(tmp_path) def test_patch_union_schema_markdown_ignores_unrenderable_shapes(tmp_path): module = _load_generate_swagger_markdown_docs_module() - spec_path = tmp_path / "console-swagger.json" + spec_path = tmp_path / "console-openapi.json" spec_path.write_text( json.dumps( { - "definitions": { - "NotAMapping": [], - "BrokenUnion": { - "oneOf": [ - {}, - {"$ref": "#/definitions/Missing"}, - {"$ref": "#/definitions/NoPropertyMapping"}, - ], + "components": { + "schemas": { + "NotAMapping": [], + "BrokenUnion": { + "oneOf": [ + {}, + {"$ref": "#/components/schemas/Missing"}, + {"$ref": "#/components/schemas/NoPropertyMapping"}, + ], + }, + "NoPropertyMapping": {"properties": []}, }, - "NoPropertyMapping": {"properties": []}, } } ), encoding="utf-8", ) - assert module._definition_ref_name(None) is None + assert module._schema_ref_name(None) is None assert module._schema_markdown_type(None) == "" assert module._schema_markdown_type({"anyOf": [{"type": "null"}]}) == "" assert module._replace_schema_table_type("unchanged", "Definition", "field", "") == "unchanged" @@ -280,24 +286,26 @@ def test_patch_union_schema_markdown_ignores_unrenderable_shapes(tmp_path): def test_convert_spec_to_markdown_patches_generated_union_tables(tmp_path, monkeypatch): module = _load_generate_swagger_markdown_docs_module() - spec_path = tmp_path / "console-swagger.json" - output_path = tmp_path / "console-swagger.md" + spec_path = tmp_path / "console-openapi.json" + output_path = tmp_path / "console-openapi.md" spec_path.write_text( json.dumps( { - "definitions": { - "FormInputConfig": { - "oneOf": [ - {"$ref": "#/definitions/ParagraphInputConfig"}, - ], - }, - "ParagraphInputConfig": { - "properties": { - "default": { - "anyOf": [ - {"$ref": "#/definitions/StringSource"}, - {"type": "null"}, - ], + "components": { + "schemas": { + "FormInputConfig": { + "oneOf": [ + {"$ref": "#/components/schemas/ParagraphInputConfig"}, + ], + }, + "ParagraphInputConfig": { + "properties": { + "default": { + "anyOf": [ + {"$ref": "#/components/schemas/StringSource"}, + {"type": "null"}, + ], + }, }, }, }, diff --git a/api/tests/unit_tests/commands/test_generate_swagger_specs.py b/api/tests/unit_tests/commands/test_generate_swagger_specs.py index 7b2ed78f56f..bfc98f5f517 100644 --- a/api/tests/unit_tests/commands/test_generate_swagger_specs.py +++ b/api/tests/unit_tests/commands/test_generate_swagger_specs.py @@ -1,4 +1,4 @@ -"""Unit tests for the standalone Swagger export helper.""" +"""Unit tests for the standalone OpenAPI export helper.""" import importlib.util import json @@ -30,42 +30,82 @@ def _load_generate_swagger_specs_module(): return module -def test_generate_specs_writes_console_web_and_service_swagger_files(tmp_path): +def _operation_ids(payload): + methods = {"delete", "get", "head", "options", "patch", "post", "put", "trace"} + for path_item in payload["paths"].values(): + for method, operation in path_item.items(): + if method in methods and isinstance(operation, dict) and "operationId" in operation: + yield operation["operationId"] + + +def _get_operations(payload): + for path_item in payload["paths"].values(): + operation = path_item.get("get") + if isinstance(operation, dict): + yield operation + + +def test_generate_specs_writes_console_web_and_service_openapi_files(tmp_path): module = _load_generate_swagger_specs_module() written_paths = module.generate_specs(tmp_path) assert [path.name for path in written_paths] == [ - "console-swagger.json", - "web-swagger.json", - "service-swagger.json", - "openapi-swagger.json", + "console-openapi.json", + "web-openapi.json", + "service-openapi.json", + "openapi-openapi.json", ] for path in written_paths: payload = json.loads(path.read_text(encoding="utf-8")) - assert payload["swagger"] == "2.0" + assert payload["openapi"].startswith("3.") assert "paths" in payload -def test_generate_specs_writes_swagger_with_resolvable_references_and_no_nulls(tmp_path): +def test_generate_specs_writes_openapi_with_resolvable_references_and_no_nulls(tmp_path): module = _load_generate_swagger_specs_module() written_paths = module.generate_specs(tmp_path) for path in written_paths: payload = json.loads(path.read_text(encoding="utf-8")) - definitions = payload["definitions"] + schemas = payload["components"]["schemas"] refs = { - item["$ref"].removeprefix("#/definitions/") + item["$ref"].removeprefix("#/components/schemas/") for item in _walk_values(payload) - if isinstance(item, dict) and isinstance(item.get("$ref"), str) + if isinstance(item, dict) + and isinstance(item.get("$ref"), str) + and item["$ref"].startswith("#/components/schemas/") } - assert refs <= set(definitions) + assert refs <= set(schemas) assert all(value is not None for value in _walk_values(payload)) +def test_generate_specs_writes_unique_operation_ids(tmp_path): + module = _load_generate_swagger_specs_module() + + written_paths = module.generate_specs(tmp_path) + + for path in written_paths: + payload = json.loads(path.read_text(encoding="utf-8")) + operation_ids = list(_operation_ids(payload)) + + assert len(operation_ids) == len(set(operation_ids)) + + +def test_generate_specs_moves_get_request_bodies_to_query_parameters(tmp_path): + module = _load_generate_swagger_specs_module() + + written_paths = module.generate_specs(tmp_path) + + for path in written_paths: + payload = json.loads(path.read_text(encoding="utf-8")) + + assert all("requestBody" not in operation for operation in _get_operations(payload)) + + def test_generate_specs_is_idempotent(tmp_path): module = _load_generate_swagger_specs_module() diff --git a/api/tests/unit_tests/controllers/common/test_schema.py b/api/tests/unit_tests/controllers/common/test_schema.py index c5da7093a02..9cd83ad54db 100644 --- a/api/tests/unit_tests/controllers/common/test_schema.py +++ b/api/tests/unit_tests/controllers/common/test_schema.py @@ -78,9 +78,9 @@ def mock_console_ns(): def test_default_ref_template_value(): - from controllers.common.schema import DEFAULT_REF_TEMPLATE_SWAGGER_2_0 + from controllers.common.schema import DEFAULT_REF_TEMPLATE_OPENAPI_3_0 - assert DEFAULT_REF_TEMPLATE_SWAGGER_2_0 == "#/definitions/{model}" + assert DEFAULT_REF_TEMPLATE_OPENAPI_3_0 == "#/components/schemas/{model}" def test_register_schema_model_calls_namespace_schema_model(): @@ -100,7 +100,7 @@ def test_register_schema_model_calls_namespace_schema_model(): def test_register_schema_model_passes_schema_from_pydantic(): - from controllers.common.schema import DEFAULT_REF_TEMPLATE_SWAGGER_2_0, register_schema_model + from controllers.common.schema import DEFAULT_REF_TEMPLATE_OPENAPI_3_0, register_schema_model namespace = MagicMock(spec=Namespace) @@ -108,24 +108,24 @@ def test_register_schema_model_passes_schema_from_pydantic(): schema = namespace.schema_model.call_args.args[1] - expected_schema = UserModel.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0) + expected_schema = UserModel.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0) assert schema == expected_schema def test_register_schema_model_promotes_nested_pydantic_definitions(): - from controllers.common.schema import DEFAULT_REF_TEMPLATE_SWAGGER_2_0, register_schema_model + from controllers.common.schema import DEFAULT_REF_TEMPLATE_OPENAPI_3_0, register_schema_model namespace = MagicMock(spec=Namespace) register_schema_model(namespace, ParentModel) called_schemas = {call.args[0]: call.args[1] for call in namespace.schema_model.call_args_list} - parent_schema = ParentModel.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0) + parent_schema = ParentModel.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0) assert set(called_schemas) == {"ParentModel", "ChildModel"} assert "$defs" not in called_schemas["ParentModel"] - assert called_schemas["ParentModel"]["properties"]["child"]["$ref"] == "#/definitions/ChildModel" + assert called_schemas["ParentModel"]["properties"]["child"]["$ref"] == "#/components/schemas/ChildModel" assert called_schemas["ChildModel"] == parent_schema["$defs"]["ChildModel"] @@ -179,7 +179,7 @@ def test_register_response_schema_model_uses_serialized_field_names(): assert "internal_name" not in schema["properties"] -def test_register_schema_model_flattens_simple_nullable_any_of_for_swagger_2(): +def test_register_schema_model_preserves_openapi_nullable_unions(): from controllers.common.schema import register_schema_model namespace = MagicMock(spec=Namespace) @@ -189,14 +189,9 @@ def test_register_schema_model_flattens_simple_nullable_any_of_for_swagger_2(): called_schemas = {call.args[0]: call.args[1] for call in namespace.schema_model.call_args_list} properties = called_schemas["NullableSchemaModel"]["properties"] - assert properties["name"]["type"] == "string" - assert properties["name"]["x-nullable"] is True - assert "anyOf" not in properties["name"] - assert properties["tags"]["type"] == "array" - assert properties["tags"]["items"] == {"type": "string"} - assert properties["tags"]["x-nullable"] is True - assert properties["owner"]["$ref"] == "#/definitions/UserModel" - assert properties["owner"]["x-nullable"] is True + assert properties["name"]["anyOf"] == [{"type": "string"}, {"type": "null"}] + assert properties["tags"]["anyOf"] == [{"items": {"type": "string"}, "type": "array"}, {"type": "null"}] + assert properties["owner"]["anyOf"] == [{"$ref": "#/components/schemas/UserModel"}, {"type": "null"}] assert "anyOf" in properties["ambiguous"] diff --git a/api/tests/unit_tests/controllers/test_swagger.py b/api/tests/unit_tests/controllers/test_swagger.py index 4e81763a20a..fae4268210d 100644 --- a/api/tests/unit_tests/controllers/test_swagger.py +++ b/api/tests/unit_tests/controllers/test_swagger.py @@ -1,20 +1,20 @@ -"""Swagger JSON rendering tests for Flask-RESTX API blueprints.""" +"""OpenAPI JSON rendering tests for Flask-RESTX API blueprints.""" import pytest from flask import Flask -def _definition_refs(value: object) -> set[str]: +def _schema_refs(value: object) -> set[str]: refs: set[str] = set() if isinstance(value, dict): ref = value.get("$ref") - if isinstance(ref, str) and ref.startswith("#/definitions/"): - refs.add(ref.removeprefix("#/definitions/")) + if isinstance(ref, str) and ref.startswith("#/components/schemas/"): + refs.add(ref.removeprefix("#/components/schemas/")) for item in value.values(): - refs.update(_definition_refs(item)) + refs.update(_schema_refs(item)) elif isinstance(value, list): for item in value: - refs.update(_definition_refs(item)) + refs.update(_schema_refs(item)) return refs @@ -31,6 +31,18 @@ def _parameters_by_name(operation: dict[str, object]) -> dict[str, dict[str, obj return result +def _multipart_form_schema(operation: dict[str, object]) -> dict[str, object]: + request_body = operation.get("requestBody") + assert isinstance(request_body, dict) + content = request_body.get("content") + assert isinstance(content, dict) + multipart = content.get("multipart/form-data") + assert isinstance(multipart, dict) + schema = multipart.get("schema") + assert isinstance(schema, dict) + return schema + + @pytest.mark.parametrize( ("first_kwargs", "second_kwargs"), [ @@ -53,7 +65,7 @@ def test_inline_model_name_includes_list_constraints( assert _inline_model_name(first_inline_model) != _inline_model_name(second_inline_model) -def test_swagger_json_endpoints_render(monkeypatch: pytest.MonkeyPatch): +def test_openapi_json_endpoints_render(monkeypatch: pytest.MonkeyPatch): from configs import dify_config from controllers.console import bp as console_bp from controllers.service_api import bp as service_api_bp @@ -70,17 +82,17 @@ def test_swagger_json_endpoints_render(monkeypatch: pytest.MonkeyPatch): client = app.test_client() - for route in ("/console/api/swagger.json", "/api/swagger.json", "/v1/swagger.json"): + for route in ("/console/api/openapi.json", "/api/openapi.json", "/v1/openapi.json"): response = client.get(route) assert response.status_code == 200 payload = response.get_json() - assert payload["swagger"] == "2.0" + assert payload["openapi"].startswith("3.") assert "paths" in payload - assert "definitions" in payload - assert isinstance(payload["definitions"], dict) - missing_refs = _definition_refs(payload) - set(payload["definitions"]) - assert not sorted(ref for ref in missing_refs if ref.startswith("_AnonymousInlineModel")) + assert "schemas" in payload["components"] + assert isinstance(payload["components"]["schemas"], dict) + missing_refs = _schema_refs(payload) - set(payload["components"]["schemas"]) + assert not missing_refs assert app.config["RESTX_INCLUDE_ALL_MODELS"] is True @@ -96,17 +108,17 @@ def test_service_document_file_routes_document_multipart_form_data(monkeypatch: app.config["RESTX_INCLUDE_ALL_MODELS"] = True app.register_blueprint(service_api_bp) - payload = app.test_client().get("/v1/swagger.json").get_json() + payload = app.test_client().get("/v1/openapi.json").get_json() paths = payload["paths"] create_operation = paths["/datasets/{dataset_id}/document/create-by-file"]["post"] - create_params = _parameters_by_name(create_operation) - assert create_operation["consumes"] == ["multipart/form-data"] - assert create_params["file"]["in"] == "formData" - assert create_params["file"]["type"] == "file" - assert create_params["file"]["required"] is True - assert create_params["data"]["in"] == "formData" - assert create_params["data"]["type"] == "string" + create_schema = _multipart_form_schema(create_operation) + create_properties = create_schema["properties"] + assert isinstance(create_properties, dict) + assert create_properties["file"] == {"type": "string", "format": "binary"} + assert create_properties["data"] == {"type": "string"} + assert create_schema["required"] == ["file"] + assert create_operation["requestBody"]["required"] is True for path in ( "/datasets/{dataset_id}/documents/{document_id}", @@ -114,13 +126,13 @@ def test_service_document_file_routes_document_multipart_form_data(monkeypatch: "/datasets/{dataset_id}/documents/{document_id}/update_by_file", ): update_operation = paths[path]["patch" if path.endswith("{document_id}") else "post"] - update_params = _parameters_by_name(update_operation) - assert update_operation["consumes"] == ["multipart/form-data"] - assert update_params["file"]["in"] == "formData" - assert update_params["file"]["type"] == "file" - assert update_params["file"]["required"] is False - assert update_params["data"]["in"] == "formData" - assert update_params["data"]["type"] == "string" + update_schema = _multipart_form_schema(update_operation) + update_properties = update_schema["properties"] + assert isinstance(update_properties, dict) + assert update_properties["file"] == {"type": "string", "format": "binary"} + assert update_properties["data"] == {"type": "string"} + assert "required" not in update_schema + assert update_operation["requestBody"]["required"] is False def test_service_document_list_documents_query_params_render(monkeypatch: pytest.MonkeyPatch): @@ -134,7 +146,7 @@ def test_service_document_list_documents_query_params_render(monkeypatch: pytest app.config["RESTX_INCLUDE_ALL_MODELS"] = True app.register_blueprint(service_api_bp) - payload = app.test_client().get("/v1/swagger.json").get_json() + payload = app.test_client().get("/v1/openapi.json").get_json() operation = payload["paths"]["/datasets/{dataset_id}/documents"]["get"] params = _parameters_by_name(operation) @@ -153,7 +165,7 @@ def test_console_account_avatar_query_param_renders_as_query(monkeypatch: pytest app.config["RESTX_INCLUDE_ALL_MODELS"] = True app.register_blueprint(console_bp) - payload = app.test_client().get("/console/api/swagger.json").get_json() + payload = app.test_client().get("/console/api/openapi.json").get_json() operation = payload["paths"]["/account/avatar"]["get"] params = _parameters_by_name(operation) diff --git a/api/uv.lock b/api/uv.lock index 55c2505225a..3445ec78321 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1630,7 +1630,7 @@ requires-dist = [ { name = "flask-login", specifier = "==0.6.3" }, { name = "flask-migrate", specifier = ">=4.1.0,<5.0.0" }, { name = "flask-orjson", specifier = ">=2.0.0,<3.0.0" }, - { name = "flask-restx", specifier = ">=1.3.2,<2.0.0" }, + { name = "flask-restx", git = "https://github.com/asukaminato0721/flask-restx?rev=27758e26f8f740d7525d5039c51a9e524b6e2b68" }, { name = "gevent", specifier = ">=26.4.0,<26.5.0" }, { name = "gevent-websocket", specifier = "==0.10.1" }, { name = "gmpy2", specifier = ">=2.3.0,<3.0.0" }, @@ -2570,8 +2570,8 @@ wheels = [ [[package]] name = "flask-restx" -version = "1.3.2" -source = { registry = "https://pypi.org/simple" } +version = "1.3.3.dev0" +source = { git = "https://github.com/asukaminato0721/flask-restx?rev=27758e26f8f740d7525d5039c51a9e524b6e2b68#27758e26f8f740d7525d5039c51a9e524b6e2b68" } dependencies = [ { name = "aniso8601" }, { name = "flask" }, @@ -2580,10 +2580,6 @@ dependencies = [ { name = "referencing" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/89/9b9ca58cbb8e9ec46f4a510ba93878e0c88d518bf03c350e3b1b7ad85cbe/flask-restx-1.3.2.tar.gz", hash = "sha256:0ae13d77e7d7e4dce513970cfa9db45364aef210e99022de26d2b73eb4dbced5", size = 2814719, upload-time = "2025-09-23T20:34:25.21Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/3f/b82cd8e733a355db1abb8297afbf59ec972c00ef90bf8d4eed287958b204/flask_restx-1.3.2-py2.py3-none-any.whl", hash = "sha256:6e035496e8223668044fc45bf769e526352fd648d9e159bd631d94fd645a687b", size = 2799859, upload-time = "2025-09-23T20:34:23.055Z" }, -] [[package]] name = "flask-sqlalchemy" diff --git a/cli/src/commands/import/app/run.ts b/cli/src/commands/import/app/run.ts index ad5bde2420c..8a04c6d2bc5 100644 --- a/cli/src/commands/import/app/run.ts +++ b/cli/src/commands/import/app/run.ts @@ -36,6 +36,12 @@ export type ImportAppResult = { readonly leakedDependencies: readonly PluginDependency[] } +type PluginDependencyLabelInput = { + readonly current_identifier?: string | null + readonly type?: PluginDependency['type'] + readonly value?: unknown +} + export async function runImportApp(opts: ImportAppOptions, deps: ImportAppDeps): Promise { const env = deps.envLookup ?? getEnv const io = deps.io ?? nullStreams() @@ -124,7 +130,7 @@ export async function runImportApp(opts: ImportAppOptions, deps: ImportAppDeps): // `value` is a loosely-typed wire object (Github | Marketplace | Package); narrow it here to // surface a human-readable identifier without depending on which variant the server returned. -export function pluginDependencyLabel(dep: PluginDependency): string { +export function pluginDependencyLabel(dep: PluginDependencyLabelInput): string { const value = dep.value if (typeof value === 'object' && value !== null) { const fields = value as Record diff --git a/packages/contracts/generated/api/console/account/zod.gen.ts b/packages/contracts/generated/api/console/account/zod.gen.ts index d1ce84faf5a..a75e3a285f7 100644 --- a/packages/contracts/generated/api/console/account/zod.gen.ts +++ b/packages/contracts/generated/api/console/account/zod.gen.ts @@ -21,7 +21,7 @@ export const zAccountAvatarPayload = z.object({ */ export const zAccount = z.object({ avatar: z.string().nullish(), - avatar_url: z.string().readonly().nullable(), + avatar_url: z.string().nullable(), created_at: z.int().nullish(), email: z.string(), id: z.string(), diff --git a/packages/contracts/generated/api/console/activate/types.gen.ts b/packages/contracts/generated/api/console/activate/types.gen.ts index ae12c3e4615..5160896b75c 100644 --- a/packages/contracts/generated/api/console/activate/types.gen.ts +++ b/packages/contracts/generated/api/console/activate/types.gen.ts @@ -18,7 +18,7 @@ export type ActivationResponse = { } export type ActivationCheckResponse = { - data?: ActivationCheckData + data?: ActivationCheckData | null is_valid: boolean } diff --git a/packages/contracts/generated/api/console/activate/zod.gen.ts b/packages/contracts/generated/api/console/activate/zod.gen.ts index 573c2d5f4c1..4f877fcd7e0 100644 --- a/packages/contracts/generated/api/console/activate/zod.gen.ts +++ b/packages/contracts/generated/api/console/activate/zod.gen.ts @@ -34,7 +34,7 @@ export const zActivationCheckData = z.object({ * ActivationCheckResponse */ export const zActivationCheckResponse = z.object({ - data: zActivationCheckData.optional(), + data: zActivationCheckData.nullish(), is_valid: z.boolean(), }) diff --git a/packages/contracts/generated/api/console/agents/types.gen.ts b/packages/contracts/generated/api/console/agents/types.gen.ts index 4aa49fc18f6..620a91cd023 100644 --- a/packages/contracts/generated/api/console/agents/types.gen.ts +++ b/packages/contracts/generated/api/console/agents/types.gen.ts @@ -17,14 +17,14 @@ export type RosterAgentCreatePayload = { description?: string icon?: string | null icon_background?: string | null - icon_type?: AgentIconType + icon_type?: AgentIconType | null name: string role?: string version_note?: string | null } export type AgentRosterResponse = { - active_config_snapshot?: AgentConfigSnapshotSummaryResponse + active_config_snapshot?: AgentConfigSnapshotSummaryResponse | null active_config_snapshot_id?: string | null agent_kind: AgentKind app_id?: string | null @@ -35,7 +35,7 @@ export type AgentRosterResponse = { description: string icon?: string | null icon_background?: string | null - icon_type?: AgentIconType + icon_type?: AgentIconType | null id: string name: string published_node_reference_count?: number @@ -63,7 +63,7 @@ export type RosterAgentUpdatePayload = { description?: string | null icon?: string | null icon_background?: string | null - icon_type?: AgentIconType + icon_type?: AgentIconType | null name?: string | null role?: string | null } @@ -92,7 +92,7 @@ export type AgentSoulConfig = { knowledge?: AgentSoulKnowledgeConfig memory?: AgentSoulMemoryConfig misc_legacy?: AgentSoulAppFeaturesConfig - model?: AgentSoulModelConfig + model?: AgentSoulModelConfig | null prompt?: AgentSoulPromptConfig sandbox?: AgentSoulSandboxConfig schema_version?: number @@ -130,7 +130,7 @@ export type AgentSource = 'agent_app' | 'imported' | 'system' | 'workflow' export type AgentStatus = 'active' | 'archived' export type AgentInviteOptionResponse = { - active_config_snapshot?: AgentConfigSnapshotSummaryResponse + active_config_snapshot?: AgentConfigSnapshotSummaryResponse | null active_config_snapshot_id?: string | null agent_kind: AgentKind app_id?: string | null @@ -142,7 +142,7 @@ export type AgentInviteOptionResponse = { existing_node_ids?: Array icon?: string | null icon_background?: string | null - icon_type?: AgentIconType + icon_type?: AgentIconType | null id: string in_current_workflow_count?: number is_in_current_workflow?: boolean @@ -174,12 +174,12 @@ export type AgentConfigRevisionResponse = { export type AgentSoulAppFeaturesConfig = { opening_statement?: string | null - retriever_resource?: AgentFeatureToggleConfig - sensitive_word_avoidance?: AgentSensitiveWordAvoidanceFeatureConfig - speech_to_text?: AgentFeatureToggleConfig + retriever_resource?: AgentFeatureToggleConfig | null + sensitive_word_avoidance?: AgentSensitiveWordAvoidanceFeatureConfig | null + speech_to_text?: AgentFeatureToggleConfig | null suggested_questions?: Array | null - suggested_questions_after_answer?: AgentSuggestedQuestionsAfterAnswerFeatureConfig - text_to_speech?: AgentTextToSpeechFeatureConfig + suggested_questions_after_answer?: AgentSuggestedQuestionsAfterAnswerFeatureConfig | null + text_to_speech?: AgentTextToSpeechFeatureConfig | null [key: string]: unknown } @@ -203,7 +203,7 @@ export type AgentSoulHumanConfig = { export type AgentSoulKnowledgeConfig = { datasets?: Array query_config?: AgentKnowledgeQueryConfig - query_mode?: AgentKnowledgeQueryMode + query_mode?: AgentKnowledgeQueryMode | null } export type AgentSoulMemoryConfig = { @@ -213,7 +213,7 @@ export type AgentSoulMemoryConfig = { } export type AgentSoulModelConfig = { - credential_ref?: AgentSoulModelCredentialRef + credential_ref?: AgentSoulModelCredentialRef | null model: string model_provider: string model_settings?: AgentSoulModelSettings @@ -252,7 +252,7 @@ export type AgentFeatureToggleConfig = { } export type AgentSensitiveWordAvoidanceFeatureConfig = { - config?: AgentModerationProviderConfig + config?: AgentModerationProviderConfig | null enabled?: boolean type?: string | null [key: string]: unknown @@ -260,7 +260,7 @@ export type AgentSensitiveWordAvoidanceFeatureConfig = { export type AgentSuggestedQuestionsAfterAnswerFeatureConfig = { enabled?: boolean - model?: AgentSoulModelConfig + model?: AgentSoulModelConfig | null prompt?: string | null [key: string]: unknown } @@ -279,7 +279,7 @@ export type AgentSecretRefConfig = { id?: string | null key?: string | null name?: string | null - permission?: AgentPermissionConfig + permission?: AgentPermissionConfig | null permission_status?: string | null provider?: string | null provider_credential_id?: string | null @@ -290,13 +290,31 @@ export type AgentSecretRefConfig = { } export type AgentEnvVariableConfig = { - default?: unknown + default?: + | string + | number + | number + | boolean + | Array + | Array + | Array + | Array + | null env_name?: string | null key?: string | null name?: string | null required?: boolean type?: string | null - value?: unknown + value?: + | string + | number + | number + | boolean + | Array + | Array + | Array + | Array + | null variable?: string | null [key: string]: unknown } @@ -356,7 +374,7 @@ export type AgentSoulModelSettings = { frequency_penalty?: number | null max_tokens?: number | null presence_penalty?: number | null - response_format?: AgentModelResponseFormatConfig + response_format?: AgentModelResponseFormatConfig | null stop?: Array | null temperature?: number | null top_p?: number | null @@ -395,7 +413,7 @@ export type AgentSkillRefConfig = { export type AgentCliToolConfig = { approved?: boolean - authorization_status?: AgentCliToolAuthorizationStatus + authorization_status?: AgentCliToolAuthorizationStatus | null command?: string | null dangerous?: boolean dangerous_accepted?: boolean @@ -413,18 +431,18 @@ export type AgentCliToolConfig = { } label?: string | null name?: string | null - permission?: AgentPermissionConfig + permission?: AgentPermissionConfig | null pre_authorized?: boolean | null requires_confirmation?: boolean risk_accepted?: boolean - risk_level?: AgentCliToolRiskLevel + risk_level?: AgentCliToolRiskLevel | null setup_command?: string | null tool_name?: string | null [key: string]: unknown } export type AgentSoulDifyToolConfig = { - credential_ref?: AgentSoulDifyToolCredentialRef + credential_ref?: AgentSoulDifyToolCredentialRef | null credential_type?: 'api-key' | 'oauth2' | 'unauthorized' description?: string | null enabled?: boolean @@ -434,16 +452,25 @@ export type AgentSoulDifyToolConfig = { provider_id?: string | null provider_type?: string runtime_parameters?: { - [key: string]: unknown + [key: string]: + | string + | number + | number + | boolean + | Array + | Array + | Array + | Array + | null } tool_name?: string | null } export type AgentModerationProviderConfig = { api_based_extension_id?: string | null - inputs_config?: AgentModerationIoConfig + inputs_config?: AgentModerationIoConfig | null keywords?: string | null - outputs_config?: AgentModerationIoConfig + outputs_config?: AgentModerationIoConfig | null [key: string]: unknown } @@ -546,9 +573,7 @@ export type DeleteAgentsByAgentIdData = { } export type DeleteAgentsByAgentIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteAgentsByAgentIdResponse diff --git a/packages/contracts/generated/api/console/agents/zod.gen.ts b/packages/contracts/generated/api/console/agents/zod.gen.ts index 35de844cf2d..e1bfc0a021f 100644 --- a/packages/contracts/generated/api/console/agents/zod.gen.ts +++ b/packages/contracts/generated/api/console/agents/zod.gen.ts @@ -16,7 +16,7 @@ export const zRosterAgentUpdatePayload = z.object({ description: z.string().nullish(), icon: z.string().max(255).nullish(), icon_background: z.string().max(255).nullish(), - icon_type: zAgentIconType.optional(), + icon_type: zAgentIconType.nullish(), name: z.string().min(1).max(255).nullish(), role: z.string().max(255).nullish(), }) @@ -88,7 +88,7 @@ export const zAgentStatus = z.enum(['active', 'archived']) * AgentRosterResponse */ export const zAgentRosterResponse = z.object({ - active_config_snapshot: zAgentConfigSnapshotSummaryResponse.optional(), + active_config_snapshot: zAgentConfigSnapshotSummaryResponse.nullish(), active_config_snapshot_id: z.string().nullish(), agent_kind: zAgentKind, app_id: z.string().nullish(), @@ -99,7 +99,7 @@ export const zAgentRosterResponse = z.object({ description: z.string(), icon: z.string().nullish(), icon_background: z.string().nullish(), - icon_type: zAgentIconType.optional(), + icon_type: zAgentIconType.nullish(), id: z.string(), name: z.string(), published_node_reference_count: z.int().optional().default(0), @@ -130,7 +130,7 @@ export const zAgentRosterListResponse = z.object({ * AgentInviteOptionResponse */ export const zAgentInviteOptionResponse = z.object({ - active_config_snapshot: zAgentConfigSnapshotSummaryResponse.optional(), + active_config_snapshot: zAgentConfigSnapshotSummaryResponse.nullish(), active_config_snapshot_id: z.string().nullish(), agent_kind: zAgentKind, app_id: z.string().nullish(), @@ -142,7 +142,7 @@ export const zAgentInviteOptionResponse = z.object({ existing_node_ids: z.array(z.string()).optional(), icon: z.string().nullish(), icon_background: z.string().nullish(), - icon_type: zAgentIconType.optional(), + icon_type: zAgentIconType.nullish(), id: z.string(), in_current_workflow_count: z.int().optional().default(0), is_in_current_workflow: z.boolean().optional().default(false), @@ -237,13 +237,35 @@ export const zAgentTextToSpeechFeatureConfig = z.object({ * AgentEnvVariableConfig */ export const zAgentEnvVariableConfig = z.object({ - default: z.unknown().optional(), + default: z + .union([ + z.string(), + z.int(), + z.number(), + z.boolean(), + z.array(z.string()), + z.array(z.int()), + z.array(z.number()), + z.array(z.boolean()), + ]) + .nullish(), env_name: z.string().max(255).nullish(), key: z.string().max(255).nullish(), name: z.string().max(255).nullish(), required: z.boolean().optional().default(false), type: z.string().max(64).nullish(), - value: z.unknown().optional(), + value: z + .union([ + z.string(), + z.int(), + z.number(), + z.boolean(), + z.array(z.string()), + z.array(z.int()), + z.array(z.number()), + z.array(z.boolean()), + ]) + .nullish(), variable: z.string().max(255).nullish(), }) @@ -309,7 +331,7 @@ export const zAgentKnowledgeQueryMode = z.enum(['generated_query', 'user_query'] export const zAgentSoulKnowledgeConfig = z.object({ datasets: z.array(zAgentKnowledgeDatasetConfig).optional(), query_config: zAgentKnowledgeQueryConfig.optional(), - query_mode: zAgentKnowledgeQueryMode.optional(), + query_mode: zAgentKnowledgeQueryMode.nullish(), }) /** @@ -413,7 +435,7 @@ export const zAgentSecretRefConfig = z.object({ id: z.string().max(255).nullish(), key: z.string().max(255).nullish(), name: z.string().max(255).nullish(), - permission: zAgentPermissionConfig.optional(), + permission: zAgentPermissionConfig.nullish(), permission_status: z.string().max(64).nullish(), provider: z.string().max(255).nullish(), provider_credential_id: z.string().max(255).nullish(), @@ -444,7 +466,7 @@ export const zAgentSoulModelSettings = z.object({ frequency_penalty: z.number().nullish(), max_tokens: z.int().nullish(), presence_penalty: z.number().nullish(), - response_format: zAgentModelResponseFormatConfig.optional(), + response_format: zAgentModelResponseFormatConfig.nullish(), stop: z.array(z.string()).nullish(), temperature: z.number().nullish(), top_p: z.number().nullish(), @@ -456,7 +478,7 @@ export const zAgentSoulModelSettings = z.object({ * Stable model selection for Agent runtime without storing secret values. */ export const zAgentSoulModelConfig = z.object({ - credential_ref: zAgentSoulModelCredentialRef.optional(), + credential_ref: zAgentSoulModelCredentialRef.nullish(), model: z.string().min(1).max(255), model_provider: z.string().min(1).max(255), model_settings: zAgentSoulModelSettings.optional(), @@ -468,7 +490,7 @@ export const zAgentSoulModelConfig = z.object({ */ export const zAgentSuggestedQuestionsAfterAnswerFeatureConfig = z.object({ enabled: z.boolean().optional().default(false), - model: zAgentSoulModelConfig.optional(), + model: zAgentSoulModelConfig.nullish(), prompt: z.string().nullish(), }) @@ -512,7 +534,7 @@ export const zAgentCliToolRiskLevel = z.enum(['dangerous', 'safe', 'unknown']) */ export const zAgentCliToolConfig = z.object({ approved: z.boolean().optional().default(false), - authorization_status: zAgentCliToolAuthorizationStatus.optional(), + authorization_status: zAgentCliToolAuthorizationStatus.nullish(), command: z.string().nullish(), dangerous: z.boolean().optional().default(false), dangerous_accepted: z.boolean().optional().default(false), @@ -528,11 +550,11 @@ export const zAgentCliToolConfig = z.object({ invoke_metadata: z.record(z.string(), z.unknown()).optional(), label: z.string().max(255).nullish(), name: z.string().max(255).nullish(), - permission: zAgentPermissionConfig.optional(), + permission: zAgentPermissionConfig.nullish(), pre_authorized: z.boolean().nullish(), requires_confirmation: z.boolean().optional().default(false), risk_accepted: z.boolean().optional().default(false), - risk_level: zAgentCliToolRiskLevel.optional(), + risk_level: zAgentCliToolRiskLevel.nullish(), setup_command: z.string().nullish(), tool_name: z.string().max(255).nullish(), }) @@ -563,7 +585,7 @@ export const zAgentSoulDifyToolCredentialRef = z.object({ * new callers should send ``plugin_id`` + ``provider`` when available. */ export const zAgentSoulDifyToolConfig = z.object({ - credential_ref: zAgentSoulDifyToolCredentialRef.optional(), + credential_ref: zAgentSoulDifyToolCredentialRef.nullish(), credential_type: z.enum(['api-key', 'oauth2', 'unauthorized']).optional().default('api-key'), description: z.string().nullish(), enabled: z.boolean().optional().default(true), @@ -572,7 +594,23 @@ export const zAgentSoulDifyToolConfig = z.object({ provider: z.string().max(255).nullish(), provider_id: z.string().max(255).nullish(), provider_type: z.string().optional().default('plugin'), - runtime_parameters: z.record(z.string(), z.unknown()).optional(), + runtime_parameters: z + .record( + z.string(), + z + .union([ + z.string(), + z.int(), + z.number(), + z.boolean(), + z.array(z.string()), + z.array(z.int()), + z.array(z.number()), + z.array(z.boolean()), + ]) + .nullable(), + ) + .optional(), tool_name: z.string().min(1).max(255).nullish(), }) @@ -597,16 +635,16 @@ export const zAgentModerationIoConfig = z.object({ */ export const zAgentModerationProviderConfig = z.object({ api_based_extension_id: z.string().nullish(), - inputs_config: zAgentModerationIoConfig.optional(), + inputs_config: zAgentModerationIoConfig.nullish(), keywords: z.string().nullish(), - outputs_config: zAgentModerationIoConfig.optional(), + outputs_config: zAgentModerationIoConfig.nullish(), }) /** * AgentSensitiveWordAvoidanceFeatureConfig */ export const zAgentSensitiveWordAvoidanceFeatureConfig = z.object({ - config: zAgentModerationProviderConfig.optional(), + config: zAgentModerationProviderConfig.nullish(), enabled: z.boolean().optional().default(false), type: z.string().nullish(), }) @@ -616,12 +654,12 @@ export const zAgentSensitiveWordAvoidanceFeatureConfig = z.object({ */ export const zAgentSoulAppFeaturesConfig = z.object({ opening_statement: z.string().nullish(), - retriever_resource: zAgentFeatureToggleConfig.optional(), - sensitive_word_avoidance: zAgentSensitiveWordAvoidanceFeatureConfig.optional(), - speech_to_text: zAgentFeatureToggleConfig.optional(), + retriever_resource: zAgentFeatureToggleConfig.nullish(), + sensitive_word_avoidance: zAgentSensitiveWordAvoidanceFeatureConfig.nullish(), + speech_to_text: zAgentFeatureToggleConfig.nullish(), suggested_questions: z.array(z.string()).nullish(), - suggested_questions_after_answer: zAgentSuggestedQuestionsAfterAnswerFeatureConfig.optional(), - text_to_speech: zAgentTextToSpeechFeatureConfig.optional(), + suggested_questions_after_answer: zAgentSuggestedQuestionsAfterAnswerFeatureConfig.nullish(), + text_to_speech: zAgentTextToSpeechFeatureConfig.nullish(), }) /** @@ -635,7 +673,7 @@ export const zAgentSoulConfig = z.object({ knowledge: zAgentSoulKnowledgeConfig.optional(), memory: zAgentSoulMemoryConfig.optional(), misc_legacy: zAgentSoulAppFeaturesConfig.optional(), - model: zAgentSoulModelConfig.optional(), + model: zAgentSoulModelConfig.nullish(), prompt: zAgentSoulPromptConfig.optional(), sandbox: zAgentSoulSandboxConfig.optional(), schema_version: z.int().optional().default(1), @@ -651,7 +689,7 @@ export const zRosterAgentCreatePayload = z.object({ description: z.string().optional().default(''), icon: z.string().max(255).nullish(), icon_background: z.string().max(255).nullish(), - icon_type: zAgentIconType.optional(), + icon_type: zAgentIconType.nullish(), name: z.string().min(1).max(255), role: z.string().max(255).optional().default(''), version_note: z.string().nullish(), @@ -709,7 +747,7 @@ export const zDeleteAgentsByAgentIdPath = z.object({ /** * Agent archived */ -export const zDeleteAgentsByAgentIdResponse = z.record(z.string(), z.never()) +export const zDeleteAgentsByAgentIdResponse = z.void() export const zGetAgentsByAgentIdPath = z.object({ agent_id: z.string(), diff --git a/packages/contracts/generated/api/console/api-based-extension/types.gen.ts b/packages/contracts/generated/api/console/api-based-extension/types.gen.ts index 1b460d21664..e24fb6efc44 100644 --- a/packages/contracts/generated/api/console/api-based-extension/types.gen.ts +++ b/packages/contracts/generated/api/console/api-based-extension/types.gen.ts @@ -58,9 +58,7 @@ export type DeleteApiBasedExtensionByIdData = { } export type DeleteApiBasedExtensionByIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteApiBasedExtensionByIdResponse diff --git a/packages/contracts/generated/api/console/api-based-extension/zod.gen.ts b/packages/contracts/generated/api/console/api-based-extension/zod.gen.ts index dd7fa0c51d8..9bb0f67728f 100644 --- a/packages/contracts/generated/api/console/api-based-extension/zod.gen.ts +++ b/packages/contracts/generated/api/console/api-based-extension/zod.gen.ts @@ -43,7 +43,7 @@ export const zDeleteApiBasedExtensionByIdPath = z.object({ /** * Extension deleted successfully */ -export const zDeleteApiBasedExtensionByIdResponse = z.record(z.string(), z.never()) +export const zDeleteApiBasedExtensionByIdResponse = z.void() export const zGetApiBasedExtensionByIdPath = z.object({ id: z.string(), diff --git a/packages/contracts/generated/api/console/api-key-auth/types.gen.ts b/packages/contracts/generated/api/console/api-key-auth/types.gen.ts index a519481a738..60e4634ca08 100644 --- a/packages/contracts/generated/api/console/api-key-auth/types.gen.ts +++ b/packages/contracts/generated/api/console/api-key-auth/types.gen.ts @@ -65,9 +65,7 @@ export type DeleteApiKeyAuthDataSourceByBindingIdData = { } export type DeleteApiKeyAuthDataSourceByBindingIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteApiKeyAuthDataSourceByBindingIdResponse diff --git a/packages/contracts/generated/api/console/api-key-auth/zod.gen.ts b/packages/contracts/generated/api/console/api-key-auth/zod.gen.ts index 65c3c92f5cc..acdb5851734 100644 --- a/packages/contracts/generated/api/console/api-key-auth/zod.gen.ts +++ b/packages/contracts/generated/api/console/api-key-auth/zod.gen.ts @@ -49,4 +49,4 @@ export const zDeleteApiKeyAuthDataSourceByBindingIdPath = z.object({ /** * Binding deleted successfully */ -export const zDeleteApiKeyAuthDataSourceByBindingIdResponse = z.record(z.string(), z.never()) +export const zDeleteApiKeyAuthDataSourceByBindingIdResponse = z.void() diff --git a/packages/contracts/generated/api/console/apps/orpc.gen.ts b/packages/contracts/generated/api/console/apps/orpc.gen.ts index 8c66e870621..f6b44fa8f77 100644 --- a/packages/contracts/generated/api/console/apps/orpc.gen.ts +++ b/packages/contracts/generated/api/console/apps/orpc.gen.ts @@ -1450,16 +1450,10 @@ export const annotations = { /** * Enable or disable app API - * - * Generated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate. - * - * @deprecated */ export const post19 = oc .route({ - deprecated: true, - description: - 'Enable or disable app API\n\nGenerated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate.', + description: 'Enable or disable app API', inputStructure: 'detailed', method: 'POST', operationId: 'postAppsByAppIdApiEnable', @@ -1510,16 +1504,10 @@ export const delete3 = oc /** * Get chat conversation details - * - * Generated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate. - * - * @deprecated */ export const get17 = oc .route({ - deprecated: true, - description: - 'Get chat conversation details\n\nGenerated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate.', + description: 'Get chat conversation details', inputStructure: 'detailed', method: 'GET', operationId: 'getAppsByAppIdChatConversationsByConversationId', @@ -1607,16 +1595,10 @@ export const byTaskId = { /** * Get chat messages for a conversation with pagination - * - * Generated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate. - * - * @deprecated */ export const get20 = oc .route({ - deprecated: true, - description: - 'Get chat messages for a conversation with pagination\n\nGenerated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate.', + description: 'Get chat messages for a conversation with pagination', inputStructure: 'detailed', method: 'GET', operationId: 'getAppsByAppIdChatMessages', @@ -1652,16 +1634,10 @@ export const delete4 = oc /** * Get completion conversation details with messages - * - * Generated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate. - * - * @deprecated */ export const get21 = oc .route({ - deprecated: true, - description: - 'Get completion conversation details with messages\n\nGenerated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate.', + description: 'Get completion conversation details with messages', inputStructure: 'detailed', method: 'GET', operationId: 'getAppsByAppIdCompletionConversationsByConversationId', @@ -1813,16 +1789,10 @@ export const convertToWorkflow = { * Copy app * * Create a copy of an existing application - * - * Generated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate. - * - * @deprecated */ export const post25 = oc .route({ - deprecated: true, - description: - 'Create a copy of an existing application\n\nGenerated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate.', + description: 'Create a copy of an existing application', inputStructure: 'detailed', method: 'POST', operationId: 'postAppsByAppIdCopy', @@ -1939,16 +1909,10 @@ export const icon = { /** * Get message details by ID - * - * Generated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate. - * - * @deprecated */ export const get26 = oc .route({ - deprecated: true, - description: - 'Get message details by ID\n\nGenerated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate.', + description: 'Get message details by ID', inputStructure: 'detailed', method: 'GET', operationId: 'getAppsByAppIdMessagesByMessageId', @@ -1998,16 +1962,10 @@ export const modelConfig = { /** * Check if app name is available - * - * Generated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate. - * - * @deprecated */ export const post29 = oc .route({ - deprecated: true, - description: - 'Check if app name is available\n\nGenerated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate.', + description: 'Check if app name is available', inputStructure: 'detailed', method: 'POST', operationId: 'postAppsByAppIdName', @@ -2151,16 +2109,10 @@ export const site = { /** * Enable or disable app site - * - * Generated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate. - * - * @deprecated */ export const post34 = oc .route({ - deprecated: true, - description: - 'Enable or disable app site\n\nGenerated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate.', + description: 'Enable or disable app site', inputStructure: 'detailed', method: 'POST', operationId: 'postAppsByAppIdSiteEnable', @@ -4614,16 +4566,9 @@ export const published = { /** * Get webhook trigger for a node - * - * Generated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate. - * - * @deprecated */ export const get77 = oc .route({ - deprecated: true, - description: - 'Generated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate.', inputStructure: 'detailed', method: 'GET', operationId: 'getAppsByAppIdWorkflowsTriggersWebhook', @@ -4791,16 +4736,10 @@ export const delete12 = oc * Get app detail * * Get application details - * - * Generated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate. - * - * @deprecated */ export const get79 = oc .route({ - deprecated: true, - description: - 'Get application details\n\nGenerated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate.', + description: 'Get application details', inputStructure: 'detailed', method: 'GET', operationId: 'getAppsByAppId', @@ -4815,16 +4754,10 @@ export const get79 = oc * Update app * * Update application details - * - * Generated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate. - * - * @deprecated */ export const put7 = oc .route({ - deprecated: true, - description: - 'Update application details\n\nGenerated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate.', + description: 'Update application details', inputStructure: 'detailed', method: 'PUT', operationId: 'putAppsByAppId', @@ -5006,16 +4939,10 @@ export const get82 = oc * Create app * * Create a new application - * - * Generated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate. - * - * @deprecated */ export const post64 = oc .route({ - deprecated: true, - description: - 'Create a new application\n\nGenerated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate.', + description: 'Create a new application', inputStructure: 'detailed', method: 'POST', operationId: 'postApps', diff --git a/packages/contracts/generated/api/console/apps/types.gen.ts b/packages/contracts/generated/api/console/apps/types.gen.ts index 73ef265f19a..42fd165ba05 100644 --- a/packages/contracts/generated/api/console/apps/types.gen.ts +++ b/packages/contracts/generated/api/console/apps/types.gen.ts @@ -16,14 +16,14 @@ export type CreateAppPayload = { description?: string | null icon?: string | null icon_background?: string | null - icon_type?: IconType + icon_type?: IconType | null mode: 'advanced-chat' | 'agent' | 'agent-chat' | 'chat' | 'completion' | 'workflow' name: string } export type AppDetail = { access_mode?: string | null - app_model_config?: ModelConfig + app_model_config?: ModelConfig | null created_at?: number | null created_by?: string | null description?: string | null @@ -35,11 +35,11 @@ export type AppDetail = { mode_compatible_with_agent: string name: string tags?: Array - tracing?: JsonValue + tracing?: JsonValue | null updated_at?: number | null updated_by?: string | null use_icon_as_answer_icon?: boolean | null - workflow?: WorkflowPartial + workflow?: WorkflowPartial | null } export type AppImportPayload = { @@ -79,7 +79,7 @@ export type WorkflowOnlineUsersResponse = { export type AppDetailWithSite = { access_mode?: string | null api_base_url?: string | null - app_model_config?: ModelConfig + app_model_config?: ModelConfig | null bound_agent_id?: string | null created_at?: number | null created_by?: string | null @@ -94,20 +94,20 @@ export type AppDetailWithSite = { max_active_requests?: number | null mode_compatible_with_agent: string name: string - site?: Site + site?: Site | null tags?: Array - tracing?: JsonValue + tracing?: JsonValue | null updated_at?: number | null updated_by?: string | null use_icon_as_answer_icon?: boolean | null - workflow?: WorkflowPartial + workflow?: WorkflowPartial | null } export type UpdateAppPayload = { description?: string | null icon?: string | null icon_background?: string | null - icon_type?: IconType + icon_type?: IconType | null max_active_requests?: number | null name: string use_icon_as_answer_icon?: boolean | null @@ -173,17 +173,17 @@ export type AgentAppComposerResponse = { agent: AgentComposerAgentResponse agent_soul: AgentSoulConfig save_options: Array - validation?: ComposerValidationFindingsResponse - variant: string + validation?: ComposerValidationFindingsResponse | null + variant: 'agent_app' } export type ComposerSavePayload = { - agent_soul?: AgentSoulConfig - binding?: ComposerBindingPayload + agent_soul?: AgentSoulConfig | null + binding?: ComposerBindingPayload | null client_revision_id?: string | null idempotency_key?: string | null new_agent_name?: string | null - node_job?: WorkflowNodeJobConfig + node_job?: WorkflowNodeJobConfig | null save_strategy: ComposerSaveStrategy soul_lock?: ComposerSoulLockPayload variant: ComposerVariant @@ -201,18 +201,18 @@ export type AgentComposerCandidatesResponse = { export type AgentComposerValidateResponse = { errors?: Array knowledge_retrieval_placeholder?: Array - result: string + result: 'success' warnings?: Array } export type AgentAppFeaturesPayload = { opening_statement?: string | null - retriever_resource?: AgentFeatureToggleConfig - sensitive_word_avoidance?: AgentSensitiveWordAvoidanceFeatureConfig - speech_to_text?: AgentFeatureToggleConfig + retriever_resource?: AgentFeatureToggleConfig | null + sensitive_word_avoidance?: AgentSensitiveWordAvoidanceFeatureConfig | null + speech_to_text?: AgentFeatureToggleConfig | null suggested_questions?: Array | null - suggested_questions_after_answer?: AgentSuggestedQuestionsAfterAnswerFeatureConfig - text_to_speech?: AgentTextToSpeechFeatureConfig + suggested_questions_after_answer?: AgentSuggestedQuestionsAfterAnswerFeatureConfig | null + text_to_speech?: AgentTextToSpeechFeatureConfig | null } export type SimpleResultResponse = { @@ -317,7 +317,7 @@ export type ConversationWithSummaryPagination = { } export type ConversationDetail = { - admin_feedback_stats?: FeedbackStat + admin_feedback_stats?: FeedbackStat | null annotated: boolean created_at?: number | null from_account_id?: string | null @@ -326,10 +326,10 @@ export type ConversationDetail = { id: string introduction?: string | null message_count: number - model_config?: ModelConfig + model_config?: ModelConfig | null status: string updated_at?: number | null - user_feedback_stats?: FeedbackStat + user_feedback_stats?: FeedbackStat | null } export type MessageInfiniteScrollPaginationResponse = { @@ -352,12 +352,12 @@ export type ConversationPagination = { export type ConversationMessageDetail = { created_at?: number | null - first_message?: MessageDetail + first_message?: MessageDetail | null from_account_id?: string | null from_end_user_id?: string | null from_source: string id: string - model_config?: ModelConfig + model_config?: ModelConfig | null status: string } @@ -397,7 +397,7 @@ export type CopyAppPayload = { description?: string | null icon?: string | null icon_background?: string | null - icon_type?: IconType + icon_type?: IconType | null name?: string | null } @@ -414,13 +414,13 @@ export type MessageFeedbackPayload = { export type AppIconPayload = { icon?: string | null icon_background?: string | null - icon_type?: IconType + icon_type?: IconType | null } export type MessageDetailResponse = { agent_thoughts?: Array - annotation?: ConversationAnnotation - annotation_hit_history?: ConversationAnnotationHitHistory + annotation?: ConversationAnnotation | null + annotation_hit_history?: ConversationAnnotationHitHistory | null answer_tokens?: number | null conversation_id: string created_at?: number | null @@ -434,9 +434,9 @@ export type MessageDetailResponse = { inputs: { [key: string]: JsonValue } - message?: JsonValue + message?: JsonValue | null message_files?: Array - message_metadata_dict?: JsonValue + message_metadata_dict?: JsonValue | null message_tokens?: number | null parent_message_id?: string | null provider_response_latency?: number | null @@ -490,7 +490,12 @@ export type AppMcpServerResponse = { description: string id: string name: string - parameters: unknown + parameters: + | { + [key: string]: unknown + } + | Array + | string server_code: string status: AppMcpServerStatus updated_at?: number | null @@ -597,6 +602,15 @@ export type WorkflowTriggerListResponse = { data: Array } +export type WorkflowExecutionStatus + = | 'failed' + | 'partial-succeeded' + | 'paused' + | 'running' + | 'scheduled' + | 'stopped' + | 'succeeded' + export type WorkflowAppLogPaginationResponse = { data: Array has_more: boolean @@ -621,8 +635,8 @@ export type WorkflowRunPaginationResponse = { export type WorkflowRunDetailResponse = { created_at?: number | null - created_by_account?: SimpleAccount - created_by_end_user?: SimpleEndUser + created_by_account?: SimpleAccount | null + created_by_end_user?: SimpleEndUser | null created_by_role?: string | null elapsed_time?: number | null error?: string | null @@ -677,7 +691,7 @@ export type WorkflowCommentDetail = { content: string created_at?: number | null created_by: string - created_by_account?: WorkflowCommentAccount + created_by_account?: WorkflowCommentAccount | null id: string mentions: Array position_x: number @@ -686,7 +700,7 @@ export type WorkflowCommentDetail = { resolved: boolean resolved_at?: number | null resolved_by?: string | null - resolved_by_account?: WorkflowCommentAccount + resolved_by_account?: WorkflowCommentAccount | null updated_at?: number | null } @@ -734,7 +748,7 @@ export type WorkflowPaginationResponse = { export type WorkflowResponse = { conversation_variables: Array created_at: number - created_by?: SimpleAccount + created_by?: SimpleAccount | null environment_variables: Array features: { [key: string]: unknown @@ -749,7 +763,7 @@ export type WorkflowResponse = { rag_pipeline_variables: Array tool_published: boolean updated_at: number - updated_by?: SimpleAccount + updated_by?: SimpleAccount | null version: string } @@ -805,19 +819,19 @@ export type HumanInputDeliveryTestPayload = { } export type WorkflowAgentComposerResponse = { - active_config_snapshot?: AgentConfigSnapshotSummaryResponse - agent?: AgentComposerAgentResponse + active_config_snapshot?: AgentConfigSnapshotSummaryResponse | null + agent?: AgentComposerAgentResponse | null agent_soul: AgentSoulConfig app_id?: string | null - binding?: AgentComposerBindingResponse + binding?: AgentComposerBindingResponse | null effective_declared_outputs?: Array - impact_summary?: AgentComposerImpactResponse + impact_summary?: AgentComposerImpactResponse | null node_id?: string | null node_job: WorkflowNodeJobConfig save_options: Array soul_lock: AgentComposerSoulLockResponse - validation?: ComposerValidationFindingsResponse - variant: string + validation?: ComposerValidationFindingsResponse | null + variant: 'workflow' workflow_id?: string | null } @@ -829,8 +843,8 @@ export type AgentComposerImpactResponse = { export type WorkflowRunNodeExecutionResponse = { created_at?: number | null - created_by_account?: SimpleAccount - created_by_end_user?: SimpleEndUser + created_by_account?: SimpleAccount | null + created_by_end_user?: SimpleEndUser | null created_by_role?: string | null elapsed_time?: number | null error?: string | null @@ -893,7 +907,7 @@ export type OutputPreviewView = { node_id: string output_name: string status: NodeOutputStatus - type?: DeclaredOutputType + type?: DeclaredOutputType | null value?: unknown } @@ -932,7 +946,7 @@ export type WorkflowDraftVariable = { export type WorkflowDraftVariableUpdatePayload = { name?: string | null - value?: unknown + value?: unknown | null } export type PublishWorkflowPayload = { @@ -969,7 +983,7 @@ export type ApiKeyItem = { export type AppPartial = { access_mode?: string | null - app_model_config?: ModelConfigPartial + app_model_config?: ModelConfigPartial | null author_name?: string | null create_user_name?: string | null created_at?: number | null @@ -987,7 +1001,7 @@ export type AppPartial = { updated_at?: number | null updated_by?: string | null use_icon_as_answer_icon?: boolean | null - workflow?: WorkflowPartial + workflow?: WorkflowPartial | null } export type IconType = 'emoji' | 'image' | 'link' @@ -1022,7 +1036,7 @@ export type ImportStatus = 'completed' | 'completed-with-warnings' | 'failed' | export type PluginDependency = { current_identifier?: string | null type: Type - value: unknown + value: Github | Marketplace | Package } export type WorkflowOnlineUsersByApp = { @@ -1051,7 +1065,7 @@ export type Site = { description?: string | null icon?: string | null icon_background?: string | null - icon_type?: unknown + icon_type?: string | IconType | null privacy_policy?: string | null prompt_public?: boolean | null show_workflow_steps?: boolean | null @@ -1064,7 +1078,7 @@ export type Site = { export type AdvancedChatWorkflowRunForListResponse = { conversation_id?: string | null created_at?: number | null - created_by_account?: SimpleAccount + created_by_account?: SimpleAccount | null elapsed_time?: number | null exceptions_count?: number | null finished_at?: number | null @@ -1104,7 +1118,7 @@ export type AgentSoulConfig = { knowledge?: AgentSoulKnowledgeConfig memory?: AgentSoulMemoryConfig misc_legacy?: AgentSoulAppFeaturesConfig - model?: AgentSoulModelConfig + model?: AgentSoulModelConfig | null prompt?: AgentSoulPromptConfig sandbox?: AgentSoulSandboxConfig schema_version?: number @@ -1158,7 +1172,14 @@ export type AgentComposerSoulCandidatesResponse = { dify_tools?: Array human_contacts?: Array knowledge_datasets?: Array - skills_files?: Array + skills_files?: Array< + | ({ + kind: 'skill' + } & AgentComposerSkillCandidateResponse) + | ({ + kind: 'file' + } & AgentComposerFileCandidateResponse) + > } export type ComposerCandidateCapabilities = { @@ -1184,7 +1205,7 @@ export type AgentFeatureToggleConfig = { } export type AgentSensitiveWordAvoidanceFeatureConfig = { - config?: AgentModerationProviderConfig + config?: AgentModerationProviderConfig | null enabled?: boolean type?: string | null [key: string]: unknown @@ -1192,7 +1213,7 @@ export type AgentSensitiveWordAvoidanceFeatureConfig = { export type AgentSuggestedQuestionsAfterAnswerFeatureConfig = { enabled?: boolean - model?: AgentSoulModelConfig + model?: AgentSoulModelConfig | null prompt?: string | null [key: string]: unknown } @@ -1222,7 +1243,7 @@ export type SandboxFileEntryResponse = { export type SandboxToolFileResponse = { reference: string - transfer_method?: string + transfer_method?: 'tool_file' } export type AnnotationHitHistory = { @@ -1236,7 +1257,7 @@ export type AnnotationHitHistory = { } export type ConversationWithSummary = { - admin_feedback_stats?: FeedbackStat + admin_feedback_stats?: FeedbackStat | null annotated: boolean created_at?: number | null from_account_id?: string | null @@ -1246,14 +1267,14 @@ export type ConversationWithSummary = { from_source: string id: string message_count: number - model_config?: SimpleModelConfig + model_config?: SimpleModelConfig | null name: string read_at?: number | null status: string - status_count?: StatusCount + status_count?: StatusCount | null summary_or_query: string updated_at?: number | null - user_feedback_stats?: FeedbackStat + user_feedback_stats?: FeedbackStat | null } export type FeedbackStat = { @@ -1262,27 +1283,27 @@ export type FeedbackStat = { } export type Conversation = { - admin_feedback_stats?: FeedbackStat - annotation?: ConversationAnnotation + admin_feedback_stats?: FeedbackStat | null + annotation?: ConversationAnnotation | null created_at?: number | null - first_message?: SimpleMessageDetail + first_message?: SimpleMessageDetail | null from_account_id?: string | null from_account_name?: string | null from_end_user_id?: string | null from_end_user_session_id?: string | null from_source: string id: string - model_config?: SimpleModelConfig + model_config?: SimpleModelConfig | null read_at?: number | null status: string updated_at?: number | null - user_feedback_stats?: FeedbackStat + user_feedback_stats?: FeedbackStat | null } export type MessageDetail = { agent_thoughts: Array - annotation?: ConversationAnnotation - annotation_hit_history?: ConversationAnnotationHitHistory + annotation?: ConversationAnnotation | null + annotation_hit_history?: ConversationAnnotationHitHistory | null answer_tokens: number conversation_id: string created_at?: number | null @@ -1333,7 +1354,7 @@ export type AgentThought = { } export type ConversationAnnotation = { - account?: SimpleAccount + account?: SimpleAccount | null content: string created_at?: number | null id: string @@ -1341,14 +1362,14 @@ export type ConversationAnnotation = { } export type ConversationAnnotationHitHistory = { - annotation_create_account?: SimpleAccount + annotation_create_account?: SimpleAccount | null created_at?: number | null id: string } export type HumanInputContent = { - form_definition?: HumanInputFormDefinition - form_submission_data?: HumanInputFormSubmissionData + form_definition?: HumanInputFormDefinition | null + form_submission_data?: HumanInputFormSubmissionData | null submitted: boolean type?: ExecutionContentType workflow_run_id: string @@ -1356,7 +1377,7 @@ export type HumanInputContent = { export type Feedback = { content?: string | null - from_account?: SimpleAccount + from_account?: SimpleAccount | null from_end_user_id?: string | null from_source: string rating: string @@ -1378,27 +1399,27 @@ export type AppMcpServerStatus = 'active' | 'inactive' | 'normal' export type WorkflowAppLogPartialResponse = { created_at?: number | null - created_by_account?: SimpleAccount - created_by_end_user?: SimpleEndUser + created_by_account?: SimpleAccount | null + created_by_end_user?: SimpleEndUser | null created_by_role?: string | null created_from?: string | null details?: unknown id: string - workflow_run?: WorkflowRunForLogResponse + workflow_run?: WorkflowRunForLogResponse | null } export type WorkflowArchivedLogPartialResponse = { created_at?: number | null - created_by_account?: SimpleAccount - created_by_end_user?: SimpleEndUser + created_by_account?: SimpleAccount | null + created_by_end_user?: SimpleEndUser | null id: string trigger_metadata?: unknown - workflow_run?: WorkflowRunForArchivedLogResponse + workflow_run?: WorkflowRunForArchivedLogResponse | null } export type WorkflowRunForListResponse = { created_at?: number | null - created_by_account?: SimpleAccount + created_by_account?: SimpleAccount | null elapsed_time?: number | null exceptions_count?: number | null finished_at?: number | null @@ -1427,7 +1448,7 @@ export type WorkflowCommentBasic = { content: string created_at?: number | null created_by: string - created_by_account?: WorkflowCommentAccount + created_by_account?: WorkflowCommentAccount | null id: string mention_count: number participants: Array @@ -1437,7 +1458,7 @@ export type WorkflowCommentBasic = { resolved: boolean resolved_at?: number | null resolved_by?: string | null - resolved_by_account?: WorkflowCommentAccount + resolved_by_account?: WorkflowCommentAccount | null updated_at?: number | null } @@ -1461,7 +1482,7 @@ export type WorkflowCommentAccount = { } export type WorkflowCommentMention = { - mentioned_user_account?: WorkflowCommentAccount + mentioned_user_account?: WorkflowCommentAccount | null mentioned_user_id: string reply_id?: string | null } @@ -1470,7 +1491,7 @@ export type WorkflowCommentReply = { content: string created_at?: number | null created_by: string - created_by_account?: WorkflowCommentAccount + created_by_account?: WorkflowCommentAccount | null id: string } @@ -1523,11 +1544,11 @@ export type AgentComposerBindingResponse = { } export type DeclaredOutputConfig = { - array_item?: DeclaredArrayItem - check?: DeclaredOutputCheckConfig + array_item?: DeclaredArrayItem | null + check?: DeclaredOutputCheckConfig | null description?: string | null failure_strategy?: DeclaredOutputFailureStrategy - file?: DeclaredOutputFileConfig + file?: DeclaredOutputFileConfig | null id?: string | null name: string required?: boolean @@ -1546,24 +1567,15 @@ export type AgentComposerImpactBindingResponse = { workflow_id: string } -export type WorkflowExecutionStatus - = | 'failed' - | 'partial-succeeded' - | 'paused' - | 'running' - | 'scheduled' - | 'stopped' - | 'succeeded' - export type NodeStatus = 'failed' | 'idle' | 'ready' | 'running' export type NodeOutputView = { name: string - output_check?: CheckResultView + output_check?: CheckResultView | null retried?: number status: NodeOutputStatus - type?: DeclaredOutputType - type_check?: CheckResultView + type?: DeclaredOutputType | null + type_check?: CheckResultView | null value_preview?: unknown } @@ -1593,7 +1605,7 @@ export type WorkflowDraftVariableWithoutValue = { export type ModelConfigPartial = { created_at?: number | null created_by?: string | null - model_dict?: JsonValue + model_dict?: JsonValue | null pre_prompt?: string | null updated_at?: number | null updated_by?: string | null @@ -1632,12 +1644,12 @@ export type AgentStatus = 'active' | 'archived' export type AgentSoulAppFeaturesConfig = { opening_statement?: string | null - retriever_resource?: AgentFeatureToggleConfig - sensitive_word_avoidance?: AgentSensitiveWordAvoidanceFeatureConfig - speech_to_text?: AgentFeatureToggleConfig + retriever_resource?: AgentFeatureToggleConfig | null + sensitive_word_avoidance?: AgentSensitiveWordAvoidanceFeatureConfig | null + speech_to_text?: AgentFeatureToggleConfig | null suggested_questions?: Array | null - suggested_questions_after_answer?: AgentSuggestedQuestionsAfterAnswerFeatureConfig - text_to_speech?: AgentTextToSpeechFeatureConfig + suggested_questions_after_answer?: AgentSuggestedQuestionsAfterAnswerFeatureConfig | null + text_to_speech?: AgentTextToSpeechFeatureConfig | null [key: string]: unknown } @@ -1661,7 +1673,7 @@ export type AgentSoulHumanConfig = { export type AgentSoulKnowledgeConfig = { datasets?: Array query_config?: AgentKnowledgeQueryConfig - query_mode?: AgentKnowledgeQueryMode + query_mode?: AgentKnowledgeQueryMode | null } export type AgentSoulMemoryConfig = { @@ -1671,7 +1683,7 @@ export type AgentSoulMemoryConfig = { } export type AgentSoulModelConfig = { - credential_ref?: AgentSoulModelCredentialRef + credential_ref?: AgentSoulModelCredentialRef | null model: string model_provider: string model_settings?: AgentSoulModelSettings @@ -1724,16 +1736,16 @@ export type WorkflowPreviousNodeOutputRef = { name?: string | null node_id?: string | null output?: string | null - selector?: Array | null - value_selector?: Array | null + selector?: Array | null + value_selector?: Array | null variable?: string | null - variable_selector?: Array | null + variable_selector?: Array | null [key: string]: unknown } export type AgentCliToolConfig = { approved?: boolean - authorization_status?: AgentCliToolAuthorizationStatus + authorization_status?: AgentCliToolAuthorizationStatus | null command?: string | null dangerous?: boolean dangerous_accepted?: boolean @@ -1751,11 +1763,11 @@ export type AgentCliToolConfig = { } label?: string | null name?: string | null - permission?: AgentPermissionConfig + permission?: AgentPermissionConfig | null pre_authorized?: boolean | null requires_confirmation?: boolean risk_accepted?: boolean - risk_level?: AgentCliToolRiskLevel + risk_level?: AgentCliToolRiskLevel | null setup_command?: string | null tool_name?: string | null [key: string]: unknown @@ -1783,7 +1795,7 @@ export type AgentComposerSkillCandidateResponse = { description?: string | null file_id?: string | null id?: string | null - kind?: string + kind?: 'skill' name?: string | null path?: string | null [key: string]: unknown @@ -1792,7 +1804,7 @@ export type AgentComposerSkillCandidateResponse = { export type AgentComposerFileCandidateResponse = { file_id?: string | null id?: string | null - kind?: string + kind?: 'file' name?: string | null reference?: string | null remote_url?: string | null @@ -1806,14 +1818,14 @@ export type AgentComposerFileCandidateResponse = { export type AgentModerationProviderConfig = { api_based_extension_id?: string | null - inputs_config?: AgentModerationIoConfig + inputs_config?: AgentModerationIoConfig | null keywords?: string | null - outputs_config?: AgentModerationIoConfig + outputs_config?: AgentModerationIoConfig | null [key: string]: unknown } export type SimpleModelConfig = { - model_dict?: JsonValue + model_dict?: JsonValue | null pre_prompt?: string | null } @@ -1891,9 +1903,9 @@ export type DeclaredArrayItem = { } export type DeclaredOutputCheckConfig = { - benchmark_file_ref?: AgentFileRefConfig + benchmark_file_ref?: AgentFileRefConfig | null enabled?: boolean - model_ref?: AgentSoulModelConfig + model_ref?: AgentSoulModelConfig | null prompt?: string | null } @@ -1919,7 +1931,7 @@ export type AgentSecretRefConfig = { id?: string | null key?: string | null name?: string | null - permission?: AgentPermissionConfig + permission?: AgentPermissionConfig | null permission_status?: string | null provider?: string | null provider_credential_id?: string | null @@ -1930,13 +1942,31 @@ export type AgentSecretRefConfig = { } export type AgentEnvVariableConfig = { - default?: unknown + default?: + | string + | number + | number + | boolean + | Array + | Array + | Array + | Array + | null env_name?: string | null key?: string | null name?: string | null required?: boolean type?: string | null - value?: unknown + value?: + | string + | number + | number + | boolean + | Array + | Array + | Array + | Array + | null variable?: string | null [key: string]: unknown } @@ -1976,7 +2006,7 @@ export type AgentSoulModelSettings = { frequency_penalty?: number | null max_tokens?: number | null presence_penalty?: number | null - response_format?: AgentModelResponseFormatConfig + response_format?: AgentModelResponseFormatConfig | null stop?: Array | null temperature?: number | null top_p?: number | null @@ -2014,7 +2044,7 @@ export type AgentSkillRefConfig = { } export type AgentSoulDifyToolConfig = { - credential_ref?: AgentSoulDifyToolCredentialRef + credential_ref?: AgentSoulDifyToolCredentialRef | null credential_type?: 'api-key' | 'oauth2' | 'unauthorized' description?: string | null enabled?: boolean @@ -2024,7 +2054,16 @@ export type AgentSoulDifyToolConfig = { provider_id?: string | null provider_type?: string runtime_parameters?: { - [key: string]: unknown + [key: string]: + | string + | number + | number + | boolean + | Array + | Array + | Array + | Array + | null } tool_name?: string | null } @@ -2064,7 +2103,19 @@ export type UserActionConfig = { title: string } -export type FormInputConfig = unknown +export type FormInputConfig + = | ({ + type: 'paragraph' + } & ParagraphInputConfig) + | ({ + type: 'select' + } & SelectInputConfig) + | ({ + type: 'file' + } & FileInputConfig) + | ({ + type: 'file-list' + } & FileListInputConfig) export type JsonValue2 = unknown @@ -2090,15 +2141,15 @@ export type AgentSoulDifyToolCredentialRef = { export type ButtonStyle = 'accent' | 'default' | 'ghost' | 'primary' export type ParagraphInputConfig = { - default?: StringSource + default?: StringSource | null output_variable_name: string - type?: string + type?: 'paragraph' } export type SelectInputConfig = { option_source: StringListSource output_variable_name: string - type?: string + type?: 'select' } export type FileInputConfig = { @@ -2106,7 +2157,7 @@ export type FileInputConfig = { allowed_file_types?: Array allowed_file_upload_methods?: Array output_variable_name: string - type?: string + type?: 'file' } export type FileListInputConfig = { @@ -2115,7 +2166,7 @@ export type FileListInputConfig = { allowed_file_upload_methods?: Array number_limits?: number output_variable_name: string - type?: string + type?: 'file-list' } export type StringSource = { @@ -2144,7 +2195,7 @@ export type WorkflowCommentDetailWritable = { content: string created_at?: number | null created_by: string - created_by_account?: WorkflowCommentAccountWritable + created_by_account?: WorkflowCommentAccountWritable | null id: string mentions: Array position_x: number @@ -2153,7 +2204,7 @@ export type WorkflowCommentDetailWritable = { resolved: boolean resolved_at?: number | null resolved_by?: string | null - resolved_by_account?: WorkflowCommentAccountWritable + resolved_by_account?: WorkflowCommentAccountWritable | null updated_at?: number | null } @@ -2161,7 +2212,7 @@ export type WorkflowCommentBasicWritable = { content: string created_at?: number | null created_by: string - created_by_account?: WorkflowCommentAccountWritable + created_by_account?: WorkflowCommentAccountWritable | null id: string mention_count: number participants: Array @@ -2171,7 +2222,7 @@ export type WorkflowCommentBasicWritable = { resolved: boolean resolved_at?: number | null resolved_by?: string | null - resolved_by_account?: WorkflowCommentAccountWritable + resolved_by_account?: WorkflowCommentAccountWritable | null updated_at?: number | null } @@ -2182,7 +2233,7 @@ export type WorkflowCommentAccountWritable = { } export type WorkflowCommentMentionWritable = { - mentioned_user_account?: WorkflowCommentAccountWritable + mentioned_user_account?: WorkflowCommentAccountWritable | null mentioned_user_id: string reply_id?: string | null } @@ -2191,7 +2242,7 @@ export type WorkflowCommentReplyWritable = { content: string created_at?: number | null created_by: string - created_by_account?: WorkflowCommentAccountWritable + created_by_account?: WorkflowCommentAccountWritable | null id: string } @@ -2339,9 +2390,7 @@ export type DeleteAppsByAppIdErrors = { export type DeleteAppsByAppIdError = DeleteAppsByAppIdErrors[keyof DeleteAppsByAppIdErrors] export type DeleteAppsByAppIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteAppsByAppIdResponse = DeleteAppsByAppIdResponses[keyof DeleteAppsByAppIdResponses] @@ -3144,9 +3193,7 @@ export type PostAppsByAppIdAnnotationsByAnnotationIdError export type PostAppsByAppIdAnnotationsByAnnotationIdResponses = { 200: Annotation - 204: { - [key: string]: never - } + 204: void } export type PostAppsByAppIdAnnotationsByAnnotationIdResponse @@ -3290,9 +3337,7 @@ export type DeleteAppsByAppIdChatConversationsByConversationIdError = DeleteAppsByAppIdChatConversationsByConversationIdErrors[keyof DeleteAppsByAppIdChatConversationsByConversationIdErrors] export type DeleteAppsByAppIdChatConversationsByConversationIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteAppsByAppIdChatConversationsByConversationIdResponse @@ -3454,9 +3499,7 @@ export type DeleteAppsByAppIdCompletionConversationsByConversationIdError = DeleteAppsByAppIdCompletionConversationsByConversationIdErrors[keyof DeleteAppsByAppIdCompletionConversationsByConversationIdErrors] export type DeleteAppsByAppIdCompletionConversationsByConversationIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteAppsByAppIdCompletionConversationsByConversationIdResponse @@ -4247,9 +4290,7 @@ export type DeleteAppsByAppIdTraceConfigError = DeleteAppsByAppIdTraceConfigErrors[keyof DeleteAppsByAppIdTraceConfigErrors] export type DeleteAppsByAppIdTraceConfigResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteAppsByAppIdTraceConfigResponse @@ -4384,7 +4425,7 @@ export type GetAppsByAppIdWorkflowAppLogsData = { keyword?: string | null limit?: number page?: number - status?: string | null + status?: WorkflowExecutionStatus | null } url: '/apps/{app_id}/workflow-app-logs' } @@ -4410,7 +4451,7 @@ export type GetAppsByAppIdWorkflowArchivedLogsData = { keyword?: string | null limit?: number page?: number - status?: string | null + status?: WorkflowExecutionStatus | null } url: '/apps/{app_id}/workflow-archived-logs' } @@ -4681,9 +4722,7 @@ export type DeleteAppsByAppIdWorkflowCommentsByCommentIdData = { } export type DeleteAppsByAppIdWorkflowCommentsByCommentIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteAppsByAppIdWorkflowCommentsByCommentIdResponse @@ -4752,9 +4791,7 @@ export type DeleteAppsByAppIdWorkflowCommentsByCommentIdRepliesByReplyIdData = { } export type DeleteAppsByAppIdWorkflowCommentsByCommentIdRepliesByReplyIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteAppsByAppIdWorkflowCommentsByCommentIdRepliesByReplyIdResponse @@ -5426,9 +5463,7 @@ export type DeleteAppsByAppIdWorkflowsDraftNodesByNodeIdVariablesData = { } export type DeleteAppsByAppIdWorkflowsDraftNodesByNodeIdVariablesResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteAppsByAppIdWorkflowsDraftNodesByNodeIdVariablesResponse @@ -5674,9 +5709,7 @@ export type DeleteAppsByAppIdWorkflowsDraftVariablesData = { } export type DeleteAppsByAppIdWorkflowsDraftVariablesResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteAppsByAppIdWorkflowsDraftVariablesResponse @@ -5688,8 +5721,8 @@ export type GetAppsByAppIdWorkflowsDraftVariablesData = { app_id: string } query?: { - limit?: number - page?: number + limit?: string + page?: string } url: '/apps/{app_id}/workflows/draft/variables' } @@ -5721,9 +5754,7 @@ export type DeleteAppsByAppIdWorkflowsDraftVariablesByVariableIdError = DeleteAppsByAppIdWorkflowsDraftVariablesByVariableIdErrors[keyof DeleteAppsByAppIdWorkflowsDraftVariablesByVariableIdErrors] export type DeleteAppsByAppIdWorkflowsDraftVariablesByVariableIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteAppsByAppIdWorkflowsDraftVariablesByVariableIdResponse @@ -5802,9 +5833,7 @@ export type PutAppsByAppIdWorkflowsDraftVariablesByVariableIdResetError export type PutAppsByAppIdWorkflowsDraftVariablesByVariableIdResetResponses = { 200: WorkflowDraftVariable - 204: { - [key: string]: never - } + 204: void } export type PutAppsByAppIdWorkflowsDraftVariablesByVariableIdResetResponse @@ -5964,7 +5993,9 @@ export type GetAppsByAppIdWorkflowsTriggersWebhookData = { query: { credential_id?: string | null datasource_type: string - inputs: string + inputs: { + [key: string]: unknown + } } url: '/apps/{app_id}/workflows/triggers/webhook' } @@ -6107,9 +6138,7 @@ export type DeleteAppsByResourceIdApiKeysByApiKeyIdData = { } export type DeleteAppsByResourceIdApiKeysByApiKeyIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteAppsByResourceIdApiKeysByApiKeyIdResponse diff --git a/packages/contracts/generated/api/console/apps/zod.gen.ts b/packages/contracts/generated/api/console/apps/zod.gen.ts index 6f16228b794..14a8d50f4e0 100644 --- a/packages/contracts/generated/api/console/apps/zod.gen.ts +++ b/packages/contracts/generated/api/console/apps/zod.gen.ts @@ -394,6 +394,19 @@ export const zWorkflowTriggerListResponse = z.object({ data: z.array(zWorkflowTriggerResponse), }) +/** + * WorkflowExecutionStatus + */ +export const zWorkflowExecutionStatus = z.enum([ + 'failed', + 'partial-succeeded', + 'paused', + 'running', + 'scheduled', + 'stopped', + 'succeeded', +]) + /** * WorkflowRunExportResponse */ @@ -580,7 +593,7 @@ export const zWorkflowDraftVariableList = z.object({ */ export const zWorkflowDraftVariableUpdatePayload = z.object({ name: z.string().nullish(), - value: z.unknown().optional(), + value: z.unknown().nullish(), }) /** @@ -642,7 +655,7 @@ export const zCreateAppPayload = z.object({ description: z.string().max(400).nullish(), icon: z.string().nullish(), icon_background: z.string().nullish(), - icon_type: zIconType.optional(), + icon_type: zIconType.nullish(), mode: z.enum(['advanced-chat', 'agent', 'agent-chat', 'chat', 'completion', 'workflow']), name: z.string().min(1), }) @@ -654,7 +667,7 @@ export const zUpdateAppPayload = z.object({ description: z.string().max(400).nullish(), icon: z.string().nullish(), icon_background: z.string().nullish(), - icon_type: zIconType.optional(), + icon_type: zIconType.nullish(), max_active_requests: z.int().nullish(), name: z.string().min(1), use_icon_as_answer_icon: z.boolean().nullish(), @@ -667,7 +680,7 @@ export const zCopyAppPayload = z.object({ description: z.string().max(400).nullish(), icon: z.string().nullish(), icon_background: z.string().nullish(), - icon_type: zIconType.optional(), + icon_type: zIconType.nullish(), name: z.string().nullish(), }) @@ -677,7 +690,7 @@ export const zCopyAppPayload = z.object({ export const zAppIconPayload = z.object({ icon: z.string().nullish(), icon_background: z.string().nullish(), - icon_type: zIconType.optional(), + icon_type: zIconType.nullish(), }) /** @@ -747,7 +760,7 @@ export const zSite = z.object({ description: z.string().nullish(), icon: z.string().nullish(), icon_background: z.string().nullish(), - icon_type: z.unknown().optional(), + icon_type: z.union([z.string(), zIconType]).nullish(), privacy_policy: z.string().nullish(), prompt_public: z.boolean().nullish(), show_workflow_steps: z.boolean().nullish(), @@ -835,7 +848,7 @@ export const zComposerValidationWarningResponse = z.object({ export const zAgentComposerValidateResponse = z.object({ errors: z.array(z.string()).optional(), knowledge_retrieval_placeholder: z.array(zComposerKnowledgePlaceholderResponse).optional(), - result: z.string(), + result: z.literal('success'), warnings: z.array(zComposerValidationWarningResponse).optional(), }) @@ -906,7 +919,7 @@ export const zSandboxListResponse = z.object({ */ export const zSandboxToolFileResponse = z.object({ reference: z.string(), - transfer_method: z.string().optional().default('tool_file'), + transfer_method: z.literal('tool_file').optional().default('tool_file'), }) /** @@ -1021,7 +1034,7 @@ export const zAppMcpServerResponse = z.object({ description: z.string(), id: z.string(), name: z.string(), - parameters: z.unknown(), + parameters: z.union([z.record(z.string(), z.unknown()), z.array(z.unknown()), z.string()]), server_code: z.string(), status: zAppMcpServerStatus, updated_at: z.int().nullish(), @@ -1042,7 +1055,7 @@ export const zSimpleAccount = z.object({ export const zAdvancedChatWorkflowRunForListResponse = z.object({ conversation_id: z.string().nullish(), created_at: z.int().nullish(), - created_by_account: zSimpleAccount.optional(), + created_by_account: zSimpleAccount.nullish(), elapsed_time: z.number().nullish(), exceptions_count: z.int().nullish(), finished_at: z.int().nullish(), @@ -1068,7 +1081,7 @@ export const zAdvancedChatWorkflowRunPaginationResponse = z.object({ * ConversationAnnotation */ export const zConversationAnnotation = z.object({ - account: zSimpleAccount.optional(), + account: zSimpleAccount.nullish(), content: z.string(), created_at: z.int().nullish(), id: z.string(), @@ -1079,7 +1092,7 @@ export const zConversationAnnotation = z.object({ * ConversationAnnotationHitHistory */ export const zConversationAnnotationHitHistory = z.object({ - annotation_create_account: zSimpleAccount.optional(), + annotation_create_account: zSimpleAccount.nullish(), created_at: z.int().nullish(), id: z.string(), }) @@ -1089,7 +1102,7 @@ export const zConversationAnnotationHitHistory = z.object({ */ export const zFeedback = z.object({ content: z.string().nullish(), - from_account: zSimpleAccount.optional(), + from_account: zSimpleAccount.nullish(), from_end_user_id: z.string().nullish(), from_source: z.string(), rating: z.string(), @@ -1100,8 +1113,8 @@ export const zFeedback = z.object({ */ export const zMessageDetail = z.object({ agent_thoughts: z.array(zAgentThought), - annotation: zConversationAnnotation.optional(), - annotation_hit_history: zConversationAnnotationHitHistory.optional(), + annotation: zConversationAnnotation.nullish(), + annotation_hit_history: zConversationAnnotationHitHistory.nullish(), answer_tokens: z.int(), conversation_id: z.string(), created_at: z.int().nullish(), @@ -1129,7 +1142,7 @@ export const zMessageDetail = z.object({ */ export const zWorkflowRunForListResponse = z.object({ created_at: z.int().nullish(), - created_by_account: zSimpleAccount.optional(), + created_by_account: zSimpleAccount.nullish(), elapsed_time: z.number().nullish(), exceptions_count: z.int().nullish(), finished_at: z.int().nullish(), @@ -1165,8 +1178,8 @@ export const zSimpleEndUser = z.object({ */ export const zWorkflowRunDetailResponse = z.object({ created_at: z.int().nullish(), - created_by_account: zSimpleAccount.optional(), - created_by_end_user: zSimpleEndUser.optional(), + created_by_account: zSimpleAccount.nullish(), + created_by_end_user: zSimpleEndUser.nullish(), created_by_role: z.string().nullish(), elapsed_time: z.number().nullish(), error: z.string().nullish(), @@ -1187,8 +1200,8 @@ export const zWorkflowRunDetailResponse = z.object({ */ export const zWorkflowRunNodeExecutionResponse = z.object({ created_at: z.int().nullish(), - created_by_account: zSimpleAccount.optional(), - created_by_end_user: zSimpleEndUser.optional(), + created_by_account: zSimpleAccount.nullish(), + created_by_end_user: zSimpleEndUser.nullish(), created_by_role: z.string().nullish(), elapsed_time: z.number().nullish(), error: z.string().nullish(), @@ -1243,7 +1256,7 @@ export const zWorkflowCommentMentionUsersPayload = z.object({ * WorkflowCommentAccount */ export const zWorkflowCommentAccount = z.object({ - avatar_url: z.string().readonly().nullable(), + avatar_url: z.string().nullable(), email: z.string(), id: z.string(), name: z.string(), @@ -1256,7 +1269,7 @@ export const zWorkflowCommentBasic = z.object({ content: z.string(), created_at: z.int().nullish(), created_by: z.string(), - created_by_account: zWorkflowCommentAccount.optional(), + created_by_account: zWorkflowCommentAccount.nullish(), id: z.string(), mention_count: z.int(), participants: z.array(zWorkflowCommentAccount), @@ -1266,7 +1279,7 @@ export const zWorkflowCommentBasic = z.object({ resolved: z.boolean(), resolved_at: z.int().nullish(), resolved_by: z.string().nullish(), - resolved_by_account: zWorkflowCommentAccount.optional(), + resolved_by_account: zWorkflowCommentAccount.nullish(), updated_at: z.int().nullish(), }) @@ -1281,7 +1294,7 @@ export const zWorkflowCommentBasicList = z.object({ * WorkflowCommentMention */ export const zWorkflowCommentMention = z.object({ - mentioned_user_account: zWorkflowCommentAccount.optional(), + mentioned_user_account: zWorkflowCommentAccount.nullish(), mentioned_user_id: z.string(), reply_id: z.string().nullish(), }) @@ -1293,7 +1306,7 @@ export const zWorkflowCommentReply = z.object({ content: z.string(), created_at: z.int().nullish(), created_by: z.string(), - created_by_account: zWorkflowCommentAccount.optional(), + created_by_account: zWorkflowCommentAccount.nullish(), id: z.string(), }) @@ -1304,7 +1317,7 @@ export const zWorkflowCommentDetail = z.object({ content: z.string(), created_at: z.int().nullish(), created_by: z.string(), - created_by_account: zWorkflowCommentAccount.optional(), + created_by_account: zWorkflowCommentAccount.nullish(), id: z.string(), mentions: z.array(zWorkflowCommentMention), position_x: z.number(), @@ -1313,7 +1326,7 @@ export const zWorkflowCommentDetail = z.object({ resolved: z.boolean(), resolved_at: z.int().nullish(), resolved_by: z.string().nullish(), - resolved_by_account: zWorkflowCommentAccount.optional(), + resolved_by_account: zWorkflowCommentAccount.nullish(), updated_at: z.int().nullish(), }) @@ -1365,7 +1378,7 @@ export const zPipelineVariableResponse = z.object({ export const zWorkflowResponse = z.object({ conversation_variables: z.array(zWorkflowConversationVariableResponse), created_at: z.int(), - created_by: zSimpleAccount.optional(), + created_by: zSimpleAccount.nullish(), environment_variables: z.array(zWorkflowEnvironmentVariableResponse), features: z.record(z.string(), z.unknown()), graph: z.record(z.string(), z.unknown()), @@ -1376,7 +1389,7 @@ export const zWorkflowResponse = z.object({ rag_pipeline_variables: z.array(zPipelineVariableResponse), tool_published: z.boolean(), updated_at: z.int(), - updated_by: zSimpleAccount.optional(), + updated_by: zSimpleAccount.nullish(), version: z.string(), }) @@ -1417,19 +1430,6 @@ export const zAgentComposerImpactResponse = z.object({ workflow_node_count: z.int(), }) -/** - * WorkflowExecutionStatus - */ -export const zWorkflowExecutionStatus = z.enum([ - 'failed', - 'partial-succeeded', - 'paused', - 'running', - 'scheduled', - 'stopped', - 'succeeded', -]) - /** * NodeStatus * @@ -1471,7 +1471,7 @@ export const zOutputPreviewView = z.object({ node_id: z.string(), output_name: z.string(), status: zNodeOutputStatus, - type: zDeclaredOutputType.optional(), + type: zDeclaredOutputType.nullish(), value: z.unknown().optional(), }) @@ -1498,7 +1498,7 @@ export const zWorkflowDraftVariableListWithoutValue = z.object({ export const zModelConfigPartial = z.object({ created_at: z.int().nullish(), created_by: z.string().nullish(), - model_dict: zJsonValue.optional(), + model_dict: zJsonValue.nullish(), pre_prompt: z.string().nullish(), updated_at: z.int().nullish(), updated_by: z.string().nullish(), @@ -1509,7 +1509,7 @@ export const zModelConfigPartial = z.object({ */ export const zAppPartial = z.object({ access_mode: z.string().nullish(), - app_model_config: zModelConfigPartial.optional(), + app_model_config: zModelConfigPartial.nullish(), author_name: z.string().nullish(), create_user_name: z.string().nullish(), created_at: z.int().nullish(), @@ -1527,7 +1527,7 @@ export const zAppPartial = z.object({ updated_at: z.int().nullish(), updated_by: z.string().nullish(), use_icon_as_answer_icon: z.boolean().nullish(), - workflow: zWorkflowPartial.optional(), + workflow: zWorkflowPartial.nullish(), }) /** @@ -1563,7 +1563,7 @@ export const zModelConfig = z.object({ */ export const zAppDetail = z.object({ access_mode: z.string().nullish(), - app_model_config: zModelConfig.optional(), + app_model_config: zModelConfig.nullish(), created_at: z.int().nullish(), created_by: z.string().nullish(), description: z.string().nullish(), @@ -1575,11 +1575,11 @@ export const zAppDetail = z.object({ mode_compatible_with_agent: z.string(), name: z.string(), tags: z.array(zTag).optional(), - tracing: zJsonValue.optional(), + tracing: zJsonValue.nullish(), updated_at: z.int().nullish(), updated_by: z.string().nullish(), use_icon_as_answer_icon: z.boolean().nullish(), - workflow: zWorkflowPartial.optional(), + workflow: zWorkflowPartial.nullish(), }) /** @@ -1588,7 +1588,7 @@ export const zAppDetail = z.object({ export const zAppDetailWithSite = z.object({ access_mode: z.string().nullish(), api_base_url: z.string().nullish(), - app_model_config: zModelConfig.optional(), + app_model_config: zModelConfig.nullish(), bound_agent_id: z.string().nullish(), created_at: z.int().nullish(), created_by: z.string().nullish(), @@ -1603,20 +1603,20 @@ export const zAppDetailWithSite = z.object({ max_active_requests: z.int().nullish(), mode_compatible_with_agent: z.string(), name: z.string(), - site: zSite.optional(), + site: zSite.nullish(), tags: z.array(zTag).optional(), - tracing: zJsonValue.optional(), + tracing: zJsonValue.nullish(), updated_at: z.int().nullish(), updated_by: z.string().nullish(), use_icon_as_answer_icon: z.boolean().nullish(), - workflow: zWorkflowPartial.optional(), + workflow: zWorkflowPartial.nullish(), }) /** * ConversationDetail */ export const zConversationDetail = z.object({ - admin_feedback_stats: zFeedbackStat.optional(), + admin_feedback_stats: zFeedbackStat.nullish(), annotated: z.boolean(), created_at: z.int().nullish(), from_account_id: z.string().nullish(), @@ -1625,10 +1625,10 @@ export const zConversationDetail = z.object({ id: z.string(), introduction: z.string().nullish(), message_count: z.int(), - model_config: zModelConfig.optional(), + model_config: zModelConfig.nullish(), status: z.string(), updated_at: z.int().nullish(), - user_feedback_stats: zFeedbackStat.optional(), + user_feedback_stats: zFeedbackStat.nullish(), }) /** @@ -1636,12 +1636,12 @@ export const zConversationDetail = z.object({ */ export const zConversationMessageDetail = z.object({ created_at: z.int().nullish(), - first_message: zMessageDetail.optional(), + first_message: zMessageDetail.nullish(), from_account_id: z.string().nullish(), from_end_user_id: z.string().nullish(), from_source: z.string(), id: z.string(), - model_config: zModelConfig.optional(), + model_config: zModelConfig.nullish(), status: z.string(), }) @@ -1650,22 +1650,6 @@ export const zConversationMessageDetail = z.object({ */ export const zType = z.enum(['github', 'marketplace', 'package']) -/** - * PluginDependency - */ -export const zPluginDependency = z.object({ - current_identifier: z.string().nullish(), - type: zType, - value: z.unknown(), -}) - -/** - * CheckDependenciesResult - */ -export const zCheckDependenciesResult = z.object({ - leaked_dependencies: z.array(zPluginDependency).optional(), -}) - /** * Github */ @@ -1692,6 +1676,22 @@ export const zPackage = z.object({ version: z.string().nullish(), }) +/** + * PluginDependency + */ +export const zPluginDependency = z.object({ + current_identifier: z.string().nullish(), + type: zType, + value: z.union([zGithub, zMarketplace, zPackage]), +}) + +/** + * CheckDependenciesResult + */ +export const zCheckDependenciesResult = z.object({ + leaked_dependencies: z.array(zPluginDependency).optional(), +}) + /** * WorkflowOnlineUser */ @@ -1787,10 +1787,14 @@ export const zWorkflowPreviousNodeOutputRef = z.object({ name: z.string().max(255).nullish(), node_id: z.string().max(255).nullish(), output: z.string().max(255).nullish(), - selector: z.array(z.unknown()).nullish(), - value_selector: z.array(z.unknown()).nullish(), + selector: z.array(z.union([z.string(), z.int(), z.number(), z.boolean(), z.null()])).nullish(), + value_selector: z + .array(z.union([z.string(), z.int(), z.number(), z.boolean(), z.null()])) + .nullish(), variable: z.string().max(255).nullish(), - variable_selector: z.array(z.unknown()).nullish(), + variable_selector: z + .array(z.union([z.string(), z.int(), z.number(), z.boolean(), z.null()])) + .nullish(), }) /** @@ -1832,7 +1836,7 @@ export const zAgentComposerSkillCandidateResponse = z.object({ description: z.string().nullish(), file_id: z.string().max(255).nullish(), id: z.string().max(255).nullish(), - kind: z.string().optional().default('skill'), + kind: z.literal('skill').optional().default('skill'), name: z.string().max(255).nullish(), path: z.string().nullish(), }) @@ -1843,7 +1847,7 @@ export const zAgentComposerSkillCandidateResponse = z.object({ export const zAgentComposerFileCandidateResponse = z.object({ file_id: z.string().max(255).nullish(), id: z.string().max(255).nullish(), - kind: z.string().optional().default('file'), + kind: z.literal('file').optional().default('file'), name: z.string().max(255).nullish(), reference: z.string().max(255).nullish(), remote_url: z.string().nullish(), @@ -1858,7 +1862,7 @@ export const zAgentComposerFileCandidateResponse = z.object({ * SimpleModelConfig */ export const zSimpleModelConfig = z.object({ - model_dict: zJsonValue.optional(), + model_dict: zJsonValue.nullish(), pre_prompt: z.string().nullish(), }) @@ -1876,7 +1880,7 @@ export const zStatusCount = z.object({ * ConversationWithSummary */ export const zConversationWithSummary = z.object({ - admin_feedback_stats: zFeedbackStat.optional(), + admin_feedback_stats: zFeedbackStat.nullish(), annotated: z.boolean(), created_at: z.int().nullish(), from_account_id: z.string().nullish(), @@ -1886,14 +1890,14 @@ export const zConversationWithSummary = z.object({ from_source: z.string(), id: z.string(), message_count: z.int(), - model_config: zSimpleModelConfig.optional(), + model_config: zSimpleModelConfig.nullish(), name: z.string(), read_at: z.int().nullish(), status: z.string(), - status_count: zStatusCount.optional(), + status_count: zStatusCount.nullish(), summary_or_query: z.string(), updated_at: z.int().nullish(), - user_feedback_stats: zFeedbackStat.optional(), + user_feedback_stats: zFeedbackStat.nullish(), }) /** @@ -1921,21 +1925,21 @@ export const zSimpleMessageDetail = z.object({ * Conversation */ export const zConversation = z.object({ - admin_feedback_stats: zFeedbackStat.optional(), - annotation: zConversationAnnotation.optional(), + admin_feedback_stats: zFeedbackStat.nullish(), + annotation: zConversationAnnotation.nullish(), created_at: z.int().nullish(), - first_message: zSimpleMessageDetail.optional(), + first_message: zSimpleMessageDetail.nullish(), from_account_id: z.string().nullish(), from_account_name: z.string().nullish(), from_end_user_id: z.string().nullish(), from_end_user_session_id: z.string().nullish(), from_source: z.string(), id: z.string(), - model_config: zSimpleModelConfig.optional(), + model_config: zSimpleModelConfig.nullish(), read_at: z.int().nullish(), status: z.string(), updated_at: z.int().nullish(), - user_feedback_stats: zFeedbackStat.optional(), + user_feedback_stats: zFeedbackStat.nullish(), }) /** @@ -1976,13 +1980,13 @@ export const zWorkflowRunForLogResponse = z.object({ */ export const zWorkflowAppLogPartialResponse = z.object({ created_at: z.int().nullish(), - created_by_account: zSimpleAccount.optional(), - created_by_end_user: zSimpleEndUser.optional(), + created_by_account: zSimpleAccount.nullish(), + created_by_end_user: zSimpleEndUser.nullish(), created_by_role: z.string().nullish(), created_from: z.string().nullish(), details: z.unknown().optional(), id: z.string(), - workflow_run: zWorkflowRunForLogResponse.optional(), + workflow_run: zWorkflowRunForLogResponse.nullish(), }) /** @@ -2012,11 +2016,11 @@ export const zWorkflowRunForArchivedLogResponse = z.object({ */ export const zWorkflowArchivedLogPartialResponse = z.object({ created_at: z.int().nullish(), - created_by_account: zSimpleAccount.optional(), - created_by_end_user: zSimpleEndUser.optional(), + created_by_account: zSimpleAccount.nullish(), + created_by_end_user: zSimpleEndUser.nullish(), id: z.string(), trigger_metadata: z.unknown().optional(), - workflow_run: zWorkflowRunForArchivedLogResponse.optional(), + workflow_run: zWorkflowRunForArchivedLogResponse.nullish(), }) /** @@ -2088,11 +2092,11 @@ export const zCheckResultView = z.object({ */ export const zNodeOutputView = z.object({ name: z.string(), - output_check: zCheckResultView.optional(), + output_check: zCheckResultView.nullish(), retried: z.int().optional().default(0), status: zNodeOutputStatus, - type: zDeclaredOutputType.optional(), - type_check: zCheckResultView.optional(), + type: zDeclaredOutputType.nullish(), + type_check: zCheckResultView.nullish(), value_preview: z.unknown().optional(), }) @@ -2122,13 +2126,35 @@ export const zWorkflowRunSnapshotView = z.object({ * AgentEnvVariableConfig */ export const zAgentEnvVariableConfig = z.object({ - default: z.unknown().optional(), + default: z + .union([ + z.string(), + z.int(), + z.number(), + z.boolean(), + z.array(z.string()), + z.array(z.int()), + z.array(z.number()), + z.array(z.boolean()), + ]) + .nullish(), env_name: z.string().max(255).nullish(), key: z.string().max(255).nullish(), name: z.string().max(255).nullish(), required: z.boolean().optional().default(false), type: z.string().max(64).nullish(), - value: z.unknown().optional(), + value: z + .union([ + z.string(), + z.int(), + z.number(), + z.boolean(), + z.array(z.string()), + z.array(z.int()), + z.array(z.number()), + z.array(z.boolean()), + ]) + .nullish(), variable: z.string().max(255).nullish(), }) @@ -2170,7 +2196,7 @@ export const zAgentKnowledgeQueryMode = z.enum(['generated_query', 'user_query'] export const zAgentSoulKnowledgeConfig = z.object({ datasets: z.array(zAgentKnowledgeDatasetConfig).optional(), query_config: zAgentKnowledgeQueryConfig.optional(), - query_mode: zAgentKnowledgeQueryMode.optional(), + query_mode: zAgentKnowledgeQueryMode.nullish(), }) /** @@ -2302,7 +2328,7 @@ export const zAgentSecretRefConfig = z.object({ id: z.string().max(255).nullish(), key: z.string().max(255).nullish(), name: z.string().max(255).nullish(), - permission: zAgentPermissionConfig.optional(), + permission: zAgentPermissionConfig.nullish(), permission_status: z.string().max(64).nullish(), provider: z.string().max(255).nullish(), provider_credential_id: z.string().max(255).nullish(), @@ -2339,7 +2365,7 @@ export const zAgentCliToolRiskLevel = z.enum(['dangerous', 'safe', 'unknown']) */ export const zAgentCliToolConfig = z.object({ approved: z.boolean().optional().default(false), - authorization_status: zAgentCliToolAuthorizationStatus.optional(), + authorization_status: zAgentCliToolAuthorizationStatus.nullish(), command: z.string().nullish(), dangerous: z.boolean().optional().default(false), dangerous_accepted: z.boolean().optional().default(false), @@ -2355,11 +2381,11 @@ export const zAgentCliToolConfig = z.object({ invoke_metadata: z.record(z.string(), z.unknown()).optional(), label: z.string().max(255).nullish(), name: z.string().max(255).nullish(), - permission: zAgentPermissionConfig.optional(), + permission: zAgentPermissionConfig.nullish(), pre_authorized: z.boolean().nullish(), requires_confirmation: z.boolean().optional().default(false), risk_accepted: z.boolean().optional().default(false), - risk_level: zAgentCliToolRiskLevel.optional(), + risk_level: zAgentCliToolRiskLevel.nullish(), setup_command: z.string().nullish(), tool_name: z.string().max(255).nullish(), }) @@ -2372,7 +2398,22 @@ export const zAgentComposerSoulCandidatesResponse = z.object({ dify_tools: z.array(zAgentComposerDifyToolCandidateResponse).optional(), human_contacts: z.array(zAgentHumanContactConfig).optional(), knowledge_datasets: z.array(zAgentKnowledgeDatasetConfig).optional(), - skills_files: z.array(z.unknown()).optional(), + skills_files: z + .array( + z.union([ + z + .object({ + kind: z.literal('skill'), + }) + .and(zAgentComposerSkillCandidateResponse), + z + .object({ + kind: z.literal('file'), + }) + .and(zAgentComposerFileCandidateResponse), + ]), + ) + .optional(), }) /** @@ -2399,22 +2440,20 @@ export const zAgentModerationIoConfig = z.object({ */ export const zAgentModerationProviderConfig = z.object({ api_based_extension_id: z.string().nullish(), - inputs_config: zAgentModerationIoConfig.optional(), + inputs_config: zAgentModerationIoConfig.nullish(), keywords: z.string().nullish(), - outputs_config: zAgentModerationIoConfig.optional(), + outputs_config: zAgentModerationIoConfig.nullish(), }) /** * AgentSensitiveWordAvoidanceFeatureConfig */ export const zAgentSensitiveWordAvoidanceFeatureConfig = z.object({ - config: zAgentModerationProviderConfig.optional(), + config: zAgentModerationProviderConfig.nullish(), enabled: z.boolean().optional().default(false), type: z.string().nullish(), }) -export const zFormInputConfig = z.unknown() - export const zJsonValue2 = z.unknown() /** @@ -2461,7 +2500,7 @@ export const zDeclaredOutputRetryConfig = z.object({ */ export const zDeclaredOutputFailureStrategy = z.object({ default_value: z.unknown().optional(), - on_failure: zOutputErrorStrategy.optional(), + on_failure: zOutputErrorStrategy.optional().default('stop'), retry: zDeclaredOutputRetryConfig.optional(), }) @@ -2479,7 +2518,7 @@ export const zAgentSoulModelSettings = z.object({ frequency_penalty: z.number().nullish(), max_tokens: z.int().nullish(), presence_penalty: z.number().nullish(), - response_format: zAgentModelResponseFormatConfig.optional(), + response_format: zAgentModelResponseFormatConfig.nullish(), stop: z.array(z.string()).nullish(), temperature: z.number().nullish(), top_p: z.number().nullish(), @@ -2491,7 +2530,7 @@ export const zAgentSoulModelSettings = z.object({ * Stable model selection for Agent runtime without storing secret values. */ export const zAgentSoulModelConfig = z.object({ - credential_ref: zAgentSoulModelCredentialRef.optional(), + credential_ref: zAgentSoulModelCredentialRef.nullish(), model: z.string().min(1).max(255), model_provider: z.string().min(1).max(255), model_settings: zAgentSoulModelSettings.optional(), @@ -2503,7 +2542,7 @@ export const zAgentSoulModelConfig = z.object({ */ export const zAgentSuggestedQuestionsAfterAnswerFeatureConfig = z.object({ enabled: z.boolean().optional().default(false), - model: zAgentSoulModelConfig.optional(), + model: zAgentSoulModelConfig.nullish(), prompt: z.string().nullish(), }) @@ -2517,12 +2556,12 @@ export const zAgentSuggestedQuestionsAfterAnswerFeatureConfig = z.object({ */ export const zAgentAppFeaturesPayload = z.object({ opening_statement: z.string().nullish(), - retriever_resource: zAgentFeatureToggleConfig.optional(), - sensitive_word_avoidance: zAgentSensitiveWordAvoidanceFeatureConfig.optional(), - speech_to_text: zAgentFeatureToggleConfig.optional(), + retriever_resource: zAgentFeatureToggleConfig.nullish(), + sensitive_word_avoidance: zAgentSensitiveWordAvoidanceFeatureConfig.nullish(), + speech_to_text: zAgentFeatureToggleConfig.nullish(), suggested_questions: z.array(z.string()).nullish(), - suggested_questions_after_answer: zAgentSuggestedQuestionsAfterAnswerFeatureConfig.optional(), - text_to_speech: zAgentTextToSpeechFeatureConfig.optional(), + suggested_questions_after_answer: zAgentSuggestedQuestionsAfterAnswerFeatureConfig.nullish(), + text_to_speech: zAgentTextToSpeechFeatureConfig.nullish(), }) /** @@ -2530,12 +2569,12 @@ export const zAgentAppFeaturesPayload = z.object({ */ export const zAgentSoulAppFeaturesConfig = z.object({ opening_statement: z.string().nullish(), - retriever_resource: zAgentFeatureToggleConfig.optional(), - sensitive_word_avoidance: zAgentSensitiveWordAvoidanceFeatureConfig.optional(), - speech_to_text: zAgentFeatureToggleConfig.optional(), + retriever_resource: zAgentFeatureToggleConfig.nullish(), + sensitive_word_avoidance: zAgentSensitiveWordAvoidanceFeatureConfig.nullish(), + speech_to_text: zAgentFeatureToggleConfig.nullish(), suggested_questions: z.array(z.string()).nullish(), - suggested_questions_after_answer: zAgentSuggestedQuestionsAfterAnswerFeatureConfig.optional(), - text_to_speech: zAgentTextToSpeechFeatureConfig.optional(), + suggested_questions_after_answer: zAgentSuggestedQuestionsAfterAnswerFeatureConfig.nullish(), + text_to_speech: zAgentTextToSpeechFeatureConfig.nullish(), }) /** @@ -2546,9 +2585,9 @@ export const zAgentSoulAppFeaturesConfig = z.object({ * Per PRD §OUTPUT 配置框, output check is **file-only** and optional. Stage 4 §4.3. */ export const zDeclaredOutputCheckConfig = z.object({ - benchmark_file_ref: zAgentFileRefConfig.optional(), + benchmark_file_ref: zAgentFileRefConfig.nullish(), enabled: z.boolean().optional().default(false), - model_ref: zAgentSoulModelConfig.optional(), + model_ref: zAgentSoulModelConfig.nullish(), prompt: z.string().nullish(), }) @@ -2562,11 +2601,11 @@ export const zDeclaredOutputCheckConfig = z.object({ * code can call ``output.failure_strategy.on_failure`` without None-guards. */ export const zDeclaredOutputConfig = z.object({ - array_item: zDeclaredArrayItem.optional(), - check: zDeclaredOutputCheckConfig.optional(), + array_item: zDeclaredArrayItem.nullish(), + check: zDeclaredOutputCheckConfig.nullish(), description: z.string().nullish(), failure_strategy: zDeclaredOutputFailureStrategy.optional(), - file: zDeclaredOutputFileConfig.optional(), + file: zDeclaredOutputFileConfig.nullish(), id: z.string().nullish(), name: z.string().min(1).max(255), required: z.boolean().optional().default(true), @@ -2580,7 +2619,7 @@ export const zWorkflowNodeJobConfig = z.object({ declared_outputs: z.array(zDeclaredOutputConfig).optional(), human_contacts: z.array(zAgentHumanContactConfig).optional(), metadata: zWorkflowNodeJobMetadata.optional(), - mode: zWorkflowNodeJobMode.optional(), + mode: zWorkflowNodeJobMode.optional().default('tell_agent_what_to_do'), previous_node_output_refs: z.array(zWorkflowPreviousNodeOutputRef).optional(), schema_version: z.int().optional().default(1), workflow_prompt: z.string().optional().default(''), @@ -2612,7 +2651,7 @@ export const zAgentSoulDifyToolCredentialRef = z.object({ * new callers should send ``plugin_id`` + ``provider`` when available. */ export const zAgentSoulDifyToolConfig = z.object({ - credential_ref: zAgentSoulDifyToolCredentialRef.optional(), + credential_ref: zAgentSoulDifyToolCredentialRef.nullish(), credential_type: z.enum(['api-key', 'oauth2', 'unauthorized']).optional().default('api-key'), description: z.string().nullish(), enabled: z.boolean().optional().default(true), @@ -2621,7 +2660,23 @@ export const zAgentSoulDifyToolConfig = z.object({ provider: z.string().max(255).nullish(), provider_id: z.string().max(255).nullish(), provider_type: z.string().optional().default('plugin'), - runtime_parameters: z.record(z.string(), z.unknown()).optional(), + runtime_parameters: z + .record( + z.string(), + z + .union([ + z.string(), + z.int(), + z.number(), + z.boolean(), + z.array(z.string()), + z.array(z.int()), + z.array(z.number()), + z.array(z.boolean()), + ]) + .nullable(), + ) + .optional(), tool_name: z.string().min(1).max(255).nullish(), }) @@ -2644,7 +2699,7 @@ export const zAgentSoulConfig = z.object({ knowledge: zAgentSoulKnowledgeConfig.optional(), memory: zAgentSoulMemoryConfig.optional(), misc_legacy: zAgentSoulAppFeaturesConfig.optional(), - model: zAgentSoulModelConfig.optional(), + model: zAgentSoulModelConfig.nullish(), prompt: zAgentSoulPromptConfig.optional(), sandbox: zAgentSoulSandboxConfig.optional(), schema_version: z.int().optional().default(1), @@ -2660,20 +2715,20 @@ export const zAgentAppComposerResponse = z.object({ agent: zAgentComposerAgentResponse, agent_soul: zAgentSoulConfig, save_options: z.array(zComposerSaveStrategy), - validation: zComposerValidationFindingsResponse.optional(), - variant: z.string(), + validation: zComposerValidationFindingsResponse.nullish(), + variant: z.literal('agent_app'), }) /** * ComposerSavePayload */ export const zComposerSavePayload = z.object({ - agent_soul: zAgentSoulConfig.optional(), - binding: zComposerBindingPayload.optional(), + agent_soul: zAgentSoulConfig.nullish(), + binding: zComposerBindingPayload.nullish(), client_revision_id: z.string().nullish(), idempotency_key: z.string().nullish(), new_agent_name: z.string().min(1).max(255).nullish(), - node_job: zWorkflowNodeJobConfig.optional(), + node_job: zWorkflowNodeJobConfig.nullish(), save_strategy: zComposerSaveStrategy, soul_lock: zComposerSoulLockPayload.optional(), variant: zComposerVariant, @@ -2684,19 +2739,19 @@ export const zComposerSavePayload = z.object({ * WorkflowAgentComposerResponse */ export const zWorkflowAgentComposerResponse = z.object({ - active_config_snapshot: zAgentConfigSnapshotSummaryResponse.optional(), - agent: zAgentComposerAgentResponse.optional(), + active_config_snapshot: zAgentConfigSnapshotSummaryResponse.nullish(), + agent: zAgentComposerAgentResponse.nullish(), agent_soul: zAgentSoulConfig, app_id: z.string().nullish(), - binding: zAgentComposerBindingResponse.optional(), + binding: zAgentComposerBindingResponse.nullish(), effective_declared_outputs: z.array(zDeclaredOutputConfig).optional(), - impact_summary: zAgentComposerImpactResponse.optional(), + impact_summary: zAgentComposerImpactResponse.nullish(), node_id: z.string().nullish(), node_job: zWorkflowNodeJobConfig, save_options: z.array(zComposerSaveStrategy), soul_lock: zAgentComposerSoulLockResponse, - validation: zComposerValidationFindingsResponse.optional(), - variant: z.string(), + validation: zComposerValidationFindingsResponse.nullish(), + variant: z.literal('workflow'), workflow_id: z.string().nullish(), }) @@ -2713,77 +2768,11 @@ export const zButtonStyle = z.enum(['accent', 'default', 'ghost', 'primary']) * User action configuration. */ export const zUserActionConfig = z.object({ - button_style: zButtonStyle.optional(), + button_style: zButtonStyle.optional().default('default'), id: z.string().max(20), title: z.string().max(100), }) -/** - * HumanInputFormDefinition - */ -export const zHumanInputFormDefinition = z.object({ - actions: z.array(zUserActionConfig).optional(), - display_in_ui: z.boolean().optional().default(false), - expiration_time: z.int(), - form_content: z.string(), - form_id: z.string(), - form_token: z.string().nullish(), - inputs: z.array(zFormInputConfig).optional(), - node_id: z.string(), - node_title: z.string(), - resolved_default_values: z.record(z.string(), z.unknown()).optional(), -}) - -/** - * HumanInputContent - */ -export const zHumanInputContent = z.object({ - form_definition: zHumanInputFormDefinition.optional(), - form_submission_data: zHumanInputFormSubmissionData.optional(), - submitted: z.boolean(), - type: zExecutionContentType.optional(), - workflow_run_id: z.string(), -}) - -/** - * MessageDetailResponse - */ -export const zMessageDetailResponse = z.object({ - agent_thoughts: z.array(zAgentThought).optional(), - annotation: zConversationAnnotation.optional(), - annotation_hit_history: zConversationAnnotationHitHistory.optional(), - answer_tokens: z.int().nullish(), - conversation_id: z.string(), - created_at: z.int().nullish(), - error: z.string().nullish(), - extra_contents: z.array(zHumanInputContent).optional(), - feedbacks: z.array(zFeedback).optional(), - from_account_id: z.string().nullish(), - from_end_user_id: z.string().nullish(), - from_source: z.string(), - id: z.string(), - inputs: z.record(z.string(), zJsonValue), - message: zJsonValue.optional(), - message_files: z.array(zMessageFile).optional(), - message_metadata_dict: zJsonValue.optional(), - message_tokens: z.int().nullish(), - parent_message_id: z.string().nullish(), - provider_response_latency: z.number().nullish(), - query: z.string(), - re_sign_file_url_answer: z.string(), - status: z.string(), - workflow_run_id: z.string().nullish(), -}) - -/** - * MessageInfiniteScrollPaginationResponse - */ -export const zMessageInfiniteScrollPaginationResponse = z.object({ - data: z.array(zMessageDetailResponse), - has_more: z.boolean(), - limit: z.int(), -}) - /** * FileType */ @@ -2807,7 +2796,7 @@ export const zFileInputConfig = z.object({ allowed_file_types: z.array(zFileType).optional(), allowed_file_upload_methods: z.array(zFileTransferMethod).optional(), output_variable_name: z.string(), - type: z.string().optional().default('file'), + type: z.literal('file').optional().default('file'), }) /** @@ -2819,7 +2808,7 @@ export const zFileListInputConfig = z.object({ allowed_file_upload_methods: z.array(zFileTransferMethod).optional(), number_limits: z.int().gte(0).optional().default(0), output_variable_name: z.string(), - type: z.string().optional().default('file-list'), + type: z.literal('file-list').optional().default('file-list'), }) /** @@ -2847,9 +2836,9 @@ export const zStringSource = z.object({ * Form input definition. */ export const zParagraphInputConfig = z.object({ - default: zStringSource.optional(), + default: zStringSource.nullish(), output_variable_name: z.string(), - type: z.string().optional().default('paragraph'), + type: z.literal('paragraph').optional().default('paragraph'), }) /** @@ -2867,7 +2856,80 @@ export const zStringListSource = z.object({ export const zSelectInputConfig = z.object({ option_source: zStringListSource, output_variable_name: z.string(), - type: z.string().optional().default('select'), + type: z.literal('select').optional().default('select'), +}) + +export const zFormInputConfig = z.discriminatedUnion('type', [ + zParagraphInputConfig.extend({ type: z.literal('paragraph') }), + zSelectInputConfig.extend({ type: z.literal('select') }), + zFileInputConfig.extend({ type: z.literal('file') }), + zFileListInputConfig.extend({ type: z.literal('file-list') }), +]) + +/** + * HumanInputFormDefinition + */ +export const zHumanInputFormDefinition = z.object({ + actions: z.array(zUserActionConfig).optional(), + display_in_ui: z.boolean().optional().default(false), + expiration_time: z.int(), + form_content: z.string(), + form_id: z.string(), + form_token: z.string().nullish(), + inputs: z.array(zFormInputConfig).optional(), + node_id: z.string(), + node_title: z.string(), + resolved_default_values: z.record(z.string(), z.unknown()).optional(), +}) + +/** + * HumanInputContent + */ +export const zHumanInputContent = z.object({ + form_definition: zHumanInputFormDefinition.nullish(), + form_submission_data: zHumanInputFormSubmissionData.nullish(), + submitted: z.boolean(), + type: zExecutionContentType.optional().default('human_input'), + workflow_run_id: z.string(), +}) + +/** + * MessageDetailResponse + */ +export const zMessageDetailResponse = z.object({ + agent_thoughts: z.array(zAgentThought).optional(), + annotation: zConversationAnnotation.nullish(), + annotation_hit_history: zConversationAnnotationHitHistory.nullish(), + answer_tokens: z.int().nullish(), + conversation_id: z.string(), + created_at: z.int().nullish(), + error: z.string().nullish(), + extra_contents: z.array(zHumanInputContent).optional(), + feedbacks: z.array(zFeedback).optional(), + from_account_id: z.string().nullish(), + from_end_user_id: z.string().nullish(), + from_source: z.string(), + id: z.string(), + inputs: z.record(z.string(), zJsonValue), + message: zJsonValue.nullish(), + message_files: z.array(zMessageFile).optional(), + message_metadata_dict: zJsonValue.nullish(), + message_tokens: z.int().nullish(), + parent_message_id: z.string().nullish(), + provider_response_latency: z.number().nullish(), + query: z.string(), + re_sign_file_url_answer: z.string(), + status: z.string(), + workflow_run_id: z.string().nullish(), +}) + +/** + * MessageInfiniteScrollPaginationResponse + */ +export const zMessageInfiniteScrollPaginationResponse = z.object({ + data: z.array(zMessageDetailResponse), + has_more: z.boolean(), + limit: z.int(), }) /** @@ -2886,7 +2948,7 @@ export const zWorkflowCommentBasicWritable = z.object({ content: z.string(), created_at: z.int().nullish(), created_by: z.string(), - created_by_account: zWorkflowCommentAccountWritable.optional(), + created_by_account: zWorkflowCommentAccountWritable.nullish(), id: z.string(), mention_count: z.int(), participants: z.array(zWorkflowCommentAccountWritable), @@ -2896,7 +2958,7 @@ export const zWorkflowCommentBasicWritable = z.object({ resolved: z.boolean(), resolved_at: z.int().nullish(), resolved_by: z.string().nullish(), - resolved_by_account: zWorkflowCommentAccountWritable.optional(), + resolved_by_account: zWorkflowCommentAccountWritable.nullish(), updated_at: z.int().nullish(), }) @@ -2911,7 +2973,7 @@ export const zWorkflowCommentBasicListWritable = z.object({ * WorkflowCommentMention */ export const zWorkflowCommentMentionWritable = z.object({ - mentioned_user_account: zWorkflowCommentAccountWritable.optional(), + mentioned_user_account: zWorkflowCommentAccountWritable.nullish(), mentioned_user_id: z.string(), reply_id: z.string().nullish(), }) @@ -2923,7 +2985,7 @@ export const zWorkflowCommentReplyWritable = z.object({ content: z.string(), created_at: z.int().nullish(), created_by: z.string(), - created_by_account: zWorkflowCommentAccountWritable.optional(), + created_by_account: zWorkflowCommentAccountWritable.nullish(), id: z.string(), }) @@ -2934,7 +2996,7 @@ export const zWorkflowCommentDetailWritable = z.object({ content: z.string(), created_at: z.int().nullish(), created_by: z.string(), - created_by_account: zWorkflowCommentAccountWritable.optional(), + created_by_account: zWorkflowCommentAccountWritable.nullish(), id: z.string(), mentions: z.array(zWorkflowCommentMentionWritable), position_x: z.number(), @@ -2943,7 +3005,7 @@ export const zWorkflowCommentDetailWritable = z.object({ resolved: z.boolean(), resolved_at: z.int().nullish(), resolved_by: z.string().nullish(), - resolved_by_account: zWorkflowCommentAccountWritable.optional(), + resolved_by_account: zWorkflowCommentAccountWritable.nullish(), updated_at: z.int().nullish(), }) @@ -3020,7 +3082,7 @@ export const zDeleteAppsByAppIdPath = z.object({ /** * App deleted successfully */ -export const zDeleteAppsByAppIdResponse = z.record(z.string(), z.never()) +export const zDeleteAppsByAppIdResponse = z.void() export const zGetAppsByAppIdPath = z.object({ app_id: z.string(), @@ -3424,10 +3486,7 @@ export const zPostAppsByAppIdAnnotationsByAnnotationIdPath = z.object({ app_id: z.string(), }) -export const zPostAppsByAppIdAnnotationsByAnnotationIdResponse = z.union([ - zAnnotation, - z.record(z.string(), z.never()), -]) +export const zPostAppsByAppIdAnnotationsByAnnotationIdResponse = z.union([zAnnotation, z.void()]) export const zGetAppsByAppIdAnnotationsByAnnotationIdHitHistoriesPath = z.object({ annotation_id: z.string(), @@ -3495,10 +3554,7 @@ export const zDeleteAppsByAppIdChatConversationsByConversationIdPath = z.object( /** * Conversation deleted successfully */ -export const zDeleteAppsByAppIdChatConversationsByConversationIdResponse = z.record( - z.string(), - z.never(), -) +export const zDeleteAppsByAppIdChatConversationsByConversationIdResponse = z.void() export const zGetAppsByAppIdChatConversationsByConversationIdPath = z.object({ app_id: z.string(), @@ -3572,10 +3628,7 @@ export const zDeleteAppsByAppIdCompletionConversationsByConversationIdPath = z.o /** * Conversation deleted successfully */ -export const zDeleteAppsByAppIdCompletionConversationsByConversationIdResponse = z.record( - z.string(), - z.never(), -) +export const zDeleteAppsByAppIdCompletionConversationsByConversationIdResponse = z.void() export const zGetAppsByAppIdCompletionConversationsByConversationIdPath = z.object({ app_id: z.string(), @@ -3982,7 +4035,7 @@ export const zDeleteAppsByAppIdTraceConfigPath = z.object({ /** * Tracing configuration deleted successfully */ -export const zDeleteAppsByAppIdTraceConfigResponse = z.record(z.string(), z.never()) +export const zDeleteAppsByAppIdTraceConfigResponse = z.void() export const zGetAppsByAppIdTraceConfigPath = z.object({ app_id: z.string(), @@ -4052,7 +4105,7 @@ export const zGetAppsByAppIdWorkflowAppLogsQuery = z.object({ keyword: z.string().nullish(), limit: z.int().gte(1).lte(100).optional().default(20), page: z.int().gte(1).lte(99999).optional().default(1), - status: z.string().nullish(), + status: zWorkflowExecutionStatus.nullish(), }) /** @@ -4073,7 +4126,7 @@ export const zGetAppsByAppIdWorkflowArchivedLogsQuery = z.object({ keyword: z.string().nullish(), limit: z.int().gte(1).lte(100).optional().default(20), page: z.int().gte(1).lte(99999).optional().default(1), - status: z.string().nullish(), + status: zWorkflowExecutionStatus.nullish(), }) /** @@ -4245,7 +4298,7 @@ export const zDeleteAppsByAppIdWorkflowCommentsByCommentIdPath = z.object({ /** * Comment deleted successfully */ -export const zDeleteAppsByAppIdWorkflowCommentsByCommentIdResponse = z.record(z.string(), z.never()) +export const zDeleteAppsByAppIdWorkflowCommentsByCommentIdResponse = z.void() export const zGetAppsByAppIdWorkflowCommentsByCommentIdPath = z.object({ app_id: z.string(), @@ -4291,10 +4344,7 @@ export const zDeleteAppsByAppIdWorkflowCommentsByCommentIdRepliesByReplyIdPath = /** * Reply deleted successfully */ -export const zDeleteAppsByAppIdWorkflowCommentsByCommentIdRepliesByReplyIdResponse = z.record( - z.string(), - z.never(), -) +export const zDeleteAppsByAppIdWorkflowCommentsByCommentIdRepliesByReplyIdResponse = z.void() export const zPutAppsByAppIdWorkflowCommentsByCommentIdRepliesByReplyIdBody = zWorkflowCommentReplyPayload @@ -4713,10 +4763,7 @@ export const zDeleteAppsByAppIdWorkflowsDraftNodesByNodeIdVariablesPath = z.obje /** * Node variables deleted successfully */ -export const zDeleteAppsByAppIdWorkflowsDraftNodesByNodeIdVariablesResponse = z.record( - z.string(), - z.never(), -) +export const zDeleteAppsByAppIdWorkflowsDraftNodesByNodeIdVariablesResponse = z.void() export const zGetAppsByAppIdWorkflowsDraftNodesByNodeIdVariablesPath = z.object({ app_id: z.string(), @@ -4826,15 +4873,15 @@ export const zDeleteAppsByAppIdWorkflowsDraftVariablesPath = z.object({ /** * Workflow variables deleted successfully */ -export const zDeleteAppsByAppIdWorkflowsDraftVariablesResponse = z.record(z.string(), z.never()) +export const zDeleteAppsByAppIdWorkflowsDraftVariablesResponse = z.void() export const zGetAppsByAppIdWorkflowsDraftVariablesPath = z.object({ app_id: z.string(), }) export const zGetAppsByAppIdWorkflowsDraftVariablesQuery = z.object({ - limit: z.int().gte(1).lte(100).optional().default(20), - page: z.int().gte(1).lte(100000).optional().default(1), + limit: z.string().optional(), + page: z.string().optional(), }) /** @@ -4850,10 +4897,7 @@ export const zDeleteAppsByAppIdWorkflowsDraftVariablesByVariableIdPath = z.objec /** * Variable deleted successfully */ -export const zDeleteAppsByAppIdWorkflowsDraftVariablesByVariableIdResponse = z.record( - z.string(), - z.never(), -) +export const zDeleteAppsByAppIdWorkflowsDraftVariablesByVariableIdResponse = z.void() export const zGetAppsByAppIdWorkflowsDraftVariablesByVariableIdPath = z.object({ app_id: z.string(), @@ -4885,7 +4929,7 @@ export const zPutAppsByAppIdWorkflowsDraftVariablesByVariableIdResetPath = z.obj export const zPutAppsByAppIdWorkflowsDraftVariablesByVariableIdResetResponse = z.union([ zWorkflowDraftVariable, - z.record(z.string(), z.never()), + z.void(), ]) export const zGetAppsByAppIdWorkflowsPublishPath = z.object({ @@ -4965,7 +5009,7 @@ export const zGetAppsByAppIdWorkflowsTriggersWebhookPath = z.object({ export const zGetAppsByAppIdWorkflowsTriggersWebhookQuery = z.object({ credential_id: z.string().nullish(), datasource_type: z.string(), - inputs: z.string(), + inputs: z.record(z.string(), z.unknown()), }) /** @@ -5034,7 +5078,7 @@ export const zDeleteAppsByResourceIdApiKeysByApiKeyIdPath = z.object({ /** * API key deleted successfully */ -export const zDeleteAppsByResourceIdApiKeysByApiKeyIdResponse = z.record(z.string(), z.never()) +export const zDeleteAppsByResourceIdApiKeysByApiKeyIdResponse = z.void() export const zGetAppsByServerIdServerRefreshPath = z.object({ server_id: z.string(), diff --git a/packages/contracts/generated/api/console/data-source/types.gen.ts b/packages/contracts/generated/api/console/data-source/types.gen.ts index 6c2ef8db583..a065af49001 100644 --- a/packages/contracts/generated/api/console/data-source/types.gen.ts +++ b/packages/contracts/generated/api/console/data-source/types.gen.ts @@ -19,7 +19,7 @@ export type DataSourceIntegrateResponse = { is_bound: boolean link: string provider: string - source_info: DataSourceIntegrateWorkspaceResponse + source_info: DataSourceIntegrateWorkspaceResponse | null } export type DataSourceIntegrateWorkspaceResponse = { @@ -31,7 +31,7 @@ export type DataSourceIntegrateWorkspaceResponse = { } export type DataSourceIntegratePageResponse = { - page_icon: DataSourceIntegrateIconResponse + page_icon: DataSourceIntegrateIconResponse | null page_id: string page_name: string parent_id: string diff --git a/packages/contracts/generated/api/console/data-source/zod.gen.ts b/packages/contracts/generated/api/console/data-source/zod.gen.ts index 1511f3de868..e5cf1735c3f 100644 --- a/packages/contracts/generated/api/console/data-source/zod.gen.ts +++ b/packages/contracts/generated/api/console/data-source/zod.gen.ts @@ -22,7 +22,7 @@ export const zDataSourceIntegrateIconResponse = z.object({ * DataSourceIntegratePageResponse */ export const zDataSourceIntegratePageResponse = z.object({ - page_icon: zDataSourceIntegrateIconResponse, + page_icon: zDataSourceIntegrateIconResponse.nullable(), page_id: z.string(), page_name: z.string(), parent_id: z.string(), @@ -50,7 +50,7 @@ export const zDataSourceIntegrateResponse = z.object({ is_bound: z.boolean(), link: z.string(), provider: z.string(), - source_info: zDataSourceIntegrateWorkspaceResponse, + source_info: zDataSourceIntegrateWorkspaceResponse.nullable(), }) /** diff --git a/packages/contracts/generated/api/console/datasets/types.gen.ts b/packages/contracts/generated/api/console/datasets/types.gen.ts index a9170dd2619..748bbac760d 100644 --- a/packages/contracts/generated/api/console/datasets/types.gen.ts +++ b/packages/contracts/generated/api/console/datasets/types.gen.ts @@ -18,7 +18,7 @@ export type DatasetCreatePayload = { external_knowledge_id?: string | null indexing_technique?: string | null name: string - permission?: PermissionEnum + permission?: PermissionEnum | null provider?: string } @@ -39,7 +39,7 @@ export type DatasetDetailResponse = { embedding_model_provider: string | null enable_api: boolean external_knowledge_info?: DatasetExternalKnowledgeInfoResponse - external_retrieval_model: DatasetExternalRetrievalModelResponse + external_retrieval_model: DatasetExternalRetrievalModelResponse | null icon_info?: DatasetIconInfoResponse id: string indexing_technique: string | null @@ -169,7 +169,7 @@ export type IndexingEstimateResponse = { } export type KnowledgeConfig = { - data_source?: DataSource + data_source?: DataSource | null doc_form?: string doc_language?: string duplicate?: boolean @@ -179,8 +179,8 @@ export type KnowledgeConfig = { is_multimodal?: boolean name?: string | null original_document_id?: string | null - process_rule?: ProcessRule - retrieval_model?: RetrievalModel + process_rule?: ProcessRule | null + retrieval_model?: RetrievalModel | null summary_index_setting?: { [key: string]: unknown } | null @@ -234,7 +234,7 @@ export type DatasetDetailWithPartialMembersResponse = { embedding_model_provider: string | null enable_api: boolean external_knowledge_info?: DatasetExternalKnowledgeInfoResponse - external_retrieval_model: DatasetExternalRetrievalModelResponse + external_retrieval_model: DatasetExternalRetrievalModelResponse | null icon_info?: DatasetIconInfoResponse id: string indexing_technique: string | null @@ -274,7 +274,7 @@ export type DatasetUpdatePayload = { partial_member_list?: Array<{ [key: string]: string }> | null - permission?: PermissionEnum + permission?: PermissionEnum | null retrieval_model?: { [key: string]: unknown } | null @@ -454,7 +454,7 @@ export type HitTestingPayload = { [key: string]: unknown } | null query: string - retrieval_model?: RetrievalModel + retrieval_model?: RetrievalModel | null } export type HitTestingResponse = { @@ -524,7 +524,7 @@ export type DatasetListItemResponse = { embedding_model_provider: string | null enable_api: boolean external_knowledge_info?: DatasetExternalKnowledgeInfoResponse - external_retrieval_model: DatasetExternalRetrievalModelResponse + external_retrieval_model: DatasetExternalRetrievalModelResponse | null icon_info?: DatasetIconInfoResponse id: string indexing_technique: string | null @@ -582,7 +582,7 @@ export type DatasetRetrievalModelResponse = { score_threshold_enabled: boolean search_method: string top_k: number - weights?: DatasetWeightedScoreResponse + weights?: DatasetWeightedScoreResponse | null } export type DatasetSummaryIndexSettingResponse = { @@ -665,19 +665,19 @@ export type DataSource = { export type ProcessRule = { mode: ProcessRuleMode - rules?: Rule + rules?: Rule | null } export type RetrievalModel = { - metadata_filtering_conditions?: MetadataFilteringCondition + metadata_filtering_conditions?: MetadataFilteringCondition | null reranking_enable: boolean reranking_mode?: string | null - reranking_model?: RerankingModel + reranking_model?: RerankingModel | null score_threshold?: number | null score_threshold_enabled: boolean search_method: RetrievalMethod top_k: number - weights?: WeightModel + weights?: WeightModel | null } export type DatasetResponse = { @@ -747,7 +747,7 @@ export type DocumentMetadataResponse = { id: string name: string type: string - value?: unknown + value?: string | number | number | boolean | null } export type SegmentResponse = { @@ -806,7 +806,7 @@ export type HitTestingRecord = { score: number | null segment: HitTestingSegment summary: string | null - tsne_position: unknown + tsne_position: unknown | null } export type DatasetMetadataListItemResponse = { @@ -861,9 +861,9 @@ export type DatasetWeightedScore = { export type InfoList = { data_source_type: 'notion_import' | 'upload_file' | 'website_crawl' - file_info_list?: FileInfo + file_info_list?: FileInfo | null notion_info_list?: Array | null - website_info_list?: WebsiteInfo + website_info_list?: WebsiteInfo | null } export type ProcessRuleMode = 'automatic' | 'custom' | 'hierarchical' @@ -871,8 +871,8 @@ export type ProcessRuleMode = 'automatic' | 'custom' | 'hierarchical' export type Rule = { parent_mode?: 'full-doc' | 'paragraph' | null pre_processing_rules?: Array | null - segmentation?: Segmentation - subchunk_segmentation?: Segmentation + segmentation?: Segmentation | null + subchunk_segmentation?: Segmentation | null } export type MetadataFilteringCondition = { @@ -892,15 +892,15 @@ export type RetrievalMethod | 'semantic_search' export type WeightModel = { - keyword_setting?: WeightKeywordSetting - vector_setting?: WeightVectorSetting + keyword_setting?: WeightKeywordSetting | null + vector_setting?: WeightVectorSetting | null weight_type?: 'customized' | 'keyword_first' | 'semantic_first' | null } export type MetadataDetail = { id: string name: string - value?: unknown + value?: string | number | number | null } export type SegmentAttachmentResponse = { @@ -957,7 +957,7 @@ export type HitTestingSegment = { export type DatasetQueryContentResponse = { content: string content_type: string - file_info?: DatasetQueryFileInfoResponse + file_info?: DatasetQueryFileInfoResponse | null } export type DatasetKeywordSettingResponse = { @@ -1029,7 +1029,7 @@ export type Condition = { | '≤' | '≥' name: string - value?: unknown + value?: string | Array | number | number | null } export type WeightKeywordSetting = { @@ -1044,7 +1044,7 @@ export type WeightVectorSetting = { export type HitTestingDocument = { data_source_type: string - doc_metadata: unknown + doc_metadata: unknown | null doc_type: string | null id: string name: string @@ -1060,7 +1060,7 @@ export type DatasetQueryFileInfoResponse = { } export type NotionPage = { - page_icon?: NotionIcon + page_icon?: NotionIcon | null page_id: string page_name: string type: string @@ -1173,9 +1173,7 @@ export type DeleteDatasetsApiKeysByApiKeyIdData = { } export type DeleteDatasetsApiKeysByApiKeyIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteDatasetsApiKeysByApiKeyIdResponse @@ -1284,9 +1282,7 @@ export type DeleteDatasetsExternalKnowledgeApiByExternalKnowledgeApiIdData = { } export type DeleteDatasetsExternalKnowledgeApiByExternalKnowledgeApiIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteDatasetsExternalKnowledgeApiByExternalKnowledgeApiIdResponse @@ -1474,9 +1470,7 @@ export type DeleteDatasetsByDatasetIdData = { } export type DeleteDatasetsByDatasetIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteDatasetsByDatasetIdResponse @@ -1626,9 +1620,7 @@ export type DeleteDatasetsByDatasetIdDocumentsData = { } export type DeleteDatasetsByDatasetIdDocumentsResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteDatasetsByDatasetIdDocumentsResponse @@ -1732,9 +1724,7 @@ export type PostDatasetsByDatasetIdDocumentsMetadataData = { } export type PostDatasetsByDatasetIdDocumentsMetadataResponses = { - 204: { - [key: string]: never - } + 204: void } export type PostDatasetsByDatasetIdDocumentsMetadataResponse @@ -1768,9 +1758,7 @@ export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdData = { } export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdResponse @@ -1956,9 +1944,7 @@ export type PatchDatasetsByDatasetIdDocumentsByDocumentIdProcessingPauseData = { } export type PatchDatasetsByDatasetIdDocumentsByDocumentIdProcessingPauseResponses = { - 204: { - [key: string]: never - } + 204: void } export type PatchDatasetsByDatasetIdDocumentsByDocumentIdProcessingPauseResponse @@ -1975,9 +1961,7 @@ export type PatchDatasetsByDatasetIdDocumentsByDocumentIdProcessingResumeData = } export type PatchDatasetsByDatasetIdDocumentsByDocumentIdProcessingResumeResponses = { - 204: { - [key: string]: never - } + 204: void } export type PatchDatasetsByDatasetIdDocumentsByDocumentIdProcessingResumeResponse @@ -2080,9 +2064,7 @@ export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsData = { } export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsResponse @@ -2158,9 +2140,7 @@ export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdDat } export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdResponse @@ -2257,9 +2237,7 @@ export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChi export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksByChildChunkIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksByChildChunkIdResponse @@ -2473,9 +2451,7 @@ export type PostDatasetsByDatasetIdMetadataBuiltInByActionData = { } export type PostDatasetsByDatasetIdMetadataBuiltInByActionResponses = { - 204: { - [key: string]: never - } + 204: void } export type PostDatasetsByDatasetIdMetadataBuiltInByActionResponse @@ -2492,9 +2468,7 @@ export type DeleteDatasetsByDatasetIdMetadataByMetadataIdData = { } export type DeleteDatasetsByDatasetIdMetadataByMetadataIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteDatasetsByDatasetIdMetadataByMetadataIdResponse @@ -2603,9 +2577,7 @@ export type PostDatasetsByDatasetIdRetryData = { } export type PostDatasetsByDatasetIdRetryResponses = { - 204: { - [key: string]: never - } + 204: void } export type PostDatasetsByDatasetIdRetryResponse @@ -2679,9 +2651,7 @@ export type DeleteDatasetsByResourceIdApiKeysByApiKeyIdData = { } export type DeleteDatasetsByResourceIdApiKeysByApiKeyIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteDatasetsByResourceIdApiKeysByApiKeyIdResponse diff --git a/packages/contracts/generated/api/console/datasets/zod.gen.ts b/packages/contracts/generated/api/console/datasets/zod.gen.ts index 44b491d01a5..3cc933a5686 100644 --- a/packages/contracts/generated/api/console/datasets/zod.gen.ts +++ b/packages/contracts/generated/api/console/datasets/zod.gen.ts @@ -298,7 +298,7 @@ export const zDatasetCreatePayload = z.object({ external_knowledge_id: z.string().nullish(), indexing_technique: z.string().nullish(), name: z.string().min(1).max(40), - permission: zPermissionEnum.optional(), + permission: zPermissionEnum.nullish().default('only_me'), provider: z.string().optional().default('vendor'), }) @@ -317,7 +317,7 @@ export const zDatasetUpdatePayload = z.object({ is_multimodal: z.boolean().nullish().default(false), name: z.string().min(1).max(40).nullish(), partial_member_list: z.array(z.record(z.string(), z.string())).nullish(), - permission: zPermissionEnum.optional(), + permission: zPermissionEnum.nullish(), retrieval_model: z.record(z.string(), z.unknown()).nullish(), summary_index_setting: z.record(z.string(), z.unknown()).nullish(), }) @@ -509,7 +509,7 @@ export const zDocumentMetadataResponse = z.object({ id: z.string(), name: z.string(), type: z.string(), - value: z.unknown().optional(), + value: z.union([z.string(), z.int(), z.number(), z.boolean()]).nullish(), }) /** @@ -740,7 +740,7 @@ export const zRetrievalMethod = z.enum([ export const zMetadataDetail = z.object({ id: z.string(), name: z.string(), - value: z.unknown().optional(), + value: z.union([z.string(), z.int(), z.number()]).nullish(), }) /** @@ -883,7 +883,7 @@ export const zDatasetRetrievalModelResponse = z.object({ score_threshold_enabled: z.boolean(), search_method: z.string(), top_k: z.int(), - weights: zDatasetWeightedScoreResponse.optional(), + weights: zDatasetWeightedScoreResponse.nullish(), }) /** @@ -906,7 +906,7 @@ export const zDatasetDetailResponse = z.object({ embedding_model_provider: z.string().nullable(), enable_api: z.boolean(), external_knowledge_info: zDatasetExternalKnowledgeInfoResponse.optional(), - external_retrieval_model: zDatasetExternalRetrievalModelResponse, + external_retrieval_model: zDatasetExternalRetrievalModelResponse.nullable(), icon_info: zDatasetIconInfoResponse.optional(), id: z.string(), indexing_technique: z.string().nullable(), @@ -947,7 +947,7 @@ export const zDatasetDetailWithPartialMembersResponse = z.object({ embedding_model_provider: z.string().nullable(), enable_api: z.boolean(), external_knowledge_info: zDatasetExternalKnowledgeInfoResponse.optional(), - external_retrieval_model: zDatasetExternalRetrievalModelResponse, + external_retrieval_model: zDatasetExternalRetrievalModelResponse.nullable(), icon_info: zDatasetIconInfoResponse.optional(), id: z.string(), indexing_technique: z.string().nullable(), @@ -989,7 +989,7 @@ export const zDatasetListItemResponse = z.object({ embedding_model_provider: z.string().nullable(), enable_api: z.boolean(), external_knowledge_info: zDatasetExternalKnowledgeInfoResponse.optional(), - external_retrieval_model: zDatasetExternalRetrievalModelResponse, + external_retrieval_model: zDatasetExternalRetrievalModelResponse.nullable(), icon_info: zDatasetIconInfoResponse.optional(), id: z.string(), indexing_technique: z.string().nullable(), @@ -1127,8 +1127,8 @@ export const zSegmentation = z.object({ export const zRule = z.object({ parent_mode: z.enum(['full-doc', 'paragraph']).nullish(), pre_processing_rules: z.array(zPreProcessingRule).nullish(), - segmentation: zSegmentation.optional(), - subchunk_segmentation: zSegmentation.optional(), + segmentation: zSegmentation.nullish(), + subchunk_segmentation: zSegmentation.nullish(), }) /** @@ -1136,7 +1136,7 @@ export const zRule = z.object({ */ export const zProcessRule = z.object({ mode: zProcessRuleMode, - rules: zRule.optional(), + rules: zRule.nullish(), }) /** @@ -1166,7 +1166,7 @@ export const zCondition = z.object({ '≥', ]), name: z.string(), - value: z.unknown().optional(), + value: z.union([z.string(), z.array(z.string()), z.int(), z.number()]).nullish(), }) /** @@ -1199,8 +1199,8 @@ export const zWeightVectorSetting = z.object({ * WeightModel */ export const zWeightModel = z.object({ - keyword_setting: zWeightKeywordSetting.optional(), - vector_setting: zWeightVectorSetting.optional(), + keyword_setting: zWeightKeywordSetting.nullish(), + vector_setting: zWeightVectorSetting.nullish(), weight_type: z.enum(['customized', 'keyword_first', 'semantic_first']).nullish(), }) @@ -1208,15 +1208,15 @@ export const zWeightModel = z.object({ * RetrievalModel */ export const zRetrievalModel = z.object({ - metadata_filtering_conditions: zMetadataFilteringCondition.optional(), + metadata_filtering_conditions: zMetadataFilteringCondition.nullish(), reranking_enable: z.boolean(), reranking_mode: z.string().nullish(), - reranking_model: zRerankingModel.optional(), + reranking_model: zRerankingModel.nullish(), score_threshold: z.number().nullish(), score_threshold_enabled: z.boolean(), search_method: zRetrievalMethod, top_k: z.int(), - weights: zWeightModel.optional(), + weights: zWeightModel.nullish(), }) /** @@ -1226,7 +1226,7 @@ export const zHitTestingPayload = z.object({ attachment_ids: z.array(z.string()).nullish(), external_retrieval_model: z.record(z.string(), z.unknown()).nullish(), query: z.string().max(250), - retrieval_model: zRetrievalModel.optional(), + retrieval_model: zRetrievalModel.nullish(), }) /** @@ -1234,7 +1234,7 @@ export const zHitTestingPayload = z.object({ */ export const zHitTestingDocument = z.object({ data_source_type: z.string(), - doc_metadata: z.unknown(), + doc_metadata: z.unknown().nullable(), doc_type: z.string().nullable(), id: z.string(), name: z.string(), @@ -1278,7 +1278,7 @@ export const zHitTestingRecord = z.object({ score: z.number().nullable(), segment: zHitTestingSegment, summary: z.string().nullable(), - tsne_position: z.unknown(), + tsne_position: z.unknown().nullable(), }) /** @@ -1307,7 +1307,7 @@ export const zDatasetQueryFileInfoResponse = z.object({ export const zDatasetQueryContentResponse = z.object({ content: z.string(), content_type: z.string(), - file_info: zDatasetQueryFileInfoResponse.optional(), + file_info: zDatasetQueryFileInfoResponse.nullish(), }) /** @@ -1347,7 +1347,7 @@ export const zNotionIcon = z.object({ * NotionPage */ export const zNotionPage = z.object({ - page_icon: zNotionIcon.optional(), + page_icon: zNotionIcon.nullish(), page_id: z.string(), page_name: z.string(), type: z.string(), @@ -1367,9 +1367,9 @@ export const zNotionInfo = z.object({ */ export const zInfoList = z.object({ data_source_type: z.enum(['notion_import', 'upload_file', 'website_crawl']), - file_info_list: zFileInfo.optional(), + file_info_list: zFileInfo.nullish(), notion_info_list: z.array(zNotionInfo).nullish(), - website_info_list: zWebsiteInfo.optional(), + website_info_list: zWebsiteInfo.nullish(), }) /** @@ -1383,7 +1383,7 @@ export const zDataSource = z.object({ * KnowledgeConfig */ export const zKnowledgeConfig = z.object({ - data_source: zDataSource.optional(), + data_source: zDataSource.nullish(), doc_form: z.string().optional().default('text_model'), doc_language: z.string().optional().default('English'), duplicate: z.boolean().optional().default(true), @@ -1393,8 +1393,8 @@ export const zKnowledgeConfig = z.object({ is_multimodal: z.boolean().optional().default(false), name: z.string().nullish(), original_document_id: z.string().nullish(), - process_rule: zProcessRule.optional(), - retrieval_model: zRetrievalModel.optional(), + process_rule: zProcessRule.nullish(), + retrieval_model: zRetrievalModel.nullish(), summary_index_setting: z.record(z.string(), z.unknown()).nullish(), }) @@ -1441,7 +1441,7 @@ export const zDeleteDatasetsApiKeysByApiKeyIdPath = z.object({ /** * API key deleted successfully */ -export const zDeleteDatasetsApiKeysByApiKeyIdResponse = z.record(z.string(), z.never()) +export const zDeleteDatasetsApiKeysByApiKeyIdResponse = z.void() export const zGetDatasetsBatchImportStatusByJobIdPath = z.object({ job_id: z.string(), @@ -1495,10 +1495,7 @@ export const zDeleteDatasetsExternalKnowledgeApiByExternalKnowledgeApiIdPath = z /** * External knowledge API deleted successfully */ -export const zDeleteDatasetsExternalKnowledgeApiByExternalKnowledgeApiIdResponse = z.record( - z.string(), - z.never(), -) +export const zDeleteDatasetsExternalKnowledgeApiByExternalKnowledgeApiIdResponse = z.void() export const zGetDatasetsExternalKnowledgeApiByExternalKnowledgeApiIdPath = z.object({ external_knowledge_api_id: z.string(), @@ -1593,7 +1590,7 @@ export const zDeleteDatasetsByDatasetIdPath = z.object({ /** * Dataset deleted successfully */ -export const zDeleteDatasetsByDatasetIdResponse = z.record(z.string(), z.never()) +export const zDeleteDatasetsByDatasetIdResponse = z.void() export const zGetDatasetsByDatasetIdPath = z.object({ dataset_id: z.string(), @@ -1664,7 +1661,7 @@ export const zDeleteDatasetsByDatasetIdDocumentsPath = z.object({ /** * Documents deleted successfully */ -export const zDeleteDatasetsByDatasetIdDocumentsResponse = z.record(z.string(), z.never()) +export const zDeleteDatasetsByDatasetIdDocumentsResponse = z.void() export const zGetDatasetsByDatasetIdDocumentsPath = z.object({ dataset_id: z.string(), @@ -1729,7 +1726,7 @@ export const zPostDatasetsByDatasetIdDocumentsMetadataPath = z.object({ /** * Documents metadata updated successfully */ -export const zPostDatasetsByDatasetIdDocumentsMetadataResponse = z.record(z.string(), z.never()) +export const zPostDatasetsByDatasetIdDocumentsMetadataResponse = z.void() export const zPatchDatasetsByDatasetIdDocumentsStatusByActionBatchPath = z.object({ action: z.string(), @@ -1749,10 +1746,7 @@ export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdPath = z.object({ /** * Document deleted successfully */ -export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdResponse = z.record( - z.string(), - z.never(), -) +export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdResponse = z.void() export const zGetDatasetsByDatasetIdDocumentsByDocumentIdPath = z.object({ dataset_id: z.string(), @@ -1850,10 +1844,7 @@ export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdProcessingPausePath = /** * Document paused successfully */ -export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdProcessingPauseResponse = z.record( - z.string(), - z.never(), -) +export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdProcessingPauseResponse = z.void() export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdProcessingResumePath = z.object({ dataset_id: z.string(), @@ -1863,10 +1854,7 @@ export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdProcessingResumePath /** * Document resumed successfully */ -export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdProcessingResumeResponse = z.record( - z.string(), - z.never(), -) +export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdProcessingResumeResponse = z.void() export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdProcessingByActionPath = z.object({ action: z.string(), @@ -1932,10 +1920,7 @@ export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsQuery = z.ob /** * Segments deleted successfully */ -export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsResponse = z.record( - z.string(), - z.never(), -) +export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsResponse = z.void() export const zGetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsPath = z.object({ dataset_id: z.string(), @@ -1991,10 +1976,7 @@ export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdP /** * Segment deleted successfully */ -export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdResponse = z.record( - z.string(), - z.never(), -) +export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdResponse = z.void() export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdBody = zSegmentUpdatePayload @@ -2075,7 +2057,7 @@ export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdC * Child chunk deleted successfully */ export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksByChildChunkIdResponse - = z.record(z.string(), z.never()) + = z.void() export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksByChildChunkIdBody = zChildChunkUpdatePayload @@ -2185,10 +2167,7 @@ export const zPostDatasetsByDatasetIdMetadataBuiltInByActionPath = z.object({ /** * Action completed successfully */ -export const zPostDatasetsByDatasetIdMetadataBuiltInByActionResponse = z.record( - z.string(), - z.never(), -) +export const zPostDatasetsByDatasetIdMetadataBuiltInByActionResponse = z.void() export const zDeleteDatasetsByDatasetIdMetadataByMetadataIdPath = z.object({ dataset_id: z.string(), @@ -2198,10 +2177,7 @@ export const zDeleteDatasetsByDatasetIdMetadataByMetadataIdPath = z.object({ /** * Metadata deleted successfully */ -export const zDeleteDatasetsByDatasetIdMetadataByMetadataIdResponse = z.record( - z.string(), - z.never(), -) +export const zDeleteDatasetsByDatasetIdMetadataByMetadataIdResponse = z.void() export const zPatchDatasetsByDatasetIdMetadataByMetadataIdBody = zMetadataUpdatePayload @@ -2260,7 +2236,7 @@ export const zPostDatasetsByDatasetIdRetryPath = z.object({ /** * Documents retry started successfully */ -export const zPostDatasetsByDatasetIdRetryResponse = z.record(z.string(), z.never()) +export const zPostDatasetsByDatasetIdRetryResponse = z.void() export const zGetDatasetsByDatasetIdUseCheckPath = z.object({ dataset_id: z.string(), @@ -2297,4 +2273,4 @@ export const zDeleteDatasetsByResourceIdApiKeysByApiKeyIdPath = z.object({ /** * API key deleted successfully */ -export const zDeleteDatasetsByResourceIdApiKeysByApiKeyIdResponse = z.record(z.string(), z.never()) +export const zDeleteDatasetsByResourceIdApiKeysByApiKeyIdResponse = z.void() diff --git a/packages/contracts/generated/api/console/explore/types.gen.ts b/packages/contracts/generated/api/console/explore/types.gen.ts index 329c1f7722c..2e468e062d6 100644 --- a/packages/contracts/generated/api/console/explore/types.gen.ts +++ b/packages/contracts/generated/api/console/explore/types.gen.ts @@ -10,7 +10,7 @@ export type RecommendedAppListResponse = { } export type RecommendedAppResponse = { - app?: RecommendedAppInfoResponse + app?: RecommendedAppInfoResponse | null app_id: string can_trial?: boolean | null categories?: Array diff --git a/packages/contracts/generated/api/console/explore/zod.gen.ts b/packages/contracts/generated/api/console/explore/zod.gen.ts index c65c47be918..dcbb95e20b5 100644 --- a/packages/contracts/generated/api/console/explore/zod.gen.ts +++ b/packages/contracts/generated/api/console/explore/zod.gen.ts @@ -18,7 +18,7 @@ export const zRecommendedAppInfoResponse = z.object({ * RecommendedAppResponse */ export const zRecommendedAppResponse = z.object({ - app: zRecommendedAppInfoResponse.optional(), + app: zRecommendedAppInfoResponse.nullish(), app_id: z.string(), can_trial: z.boolean().nullish(), categories: z.array(z.string()).optional(), diff --git a/packages/contracts/generated/api/console/features/types.gen.ts b/packages/contracts/generated/api/console/features/types.gen.ts index 68b2dc0d9eb..da4dac47c24 100644 --- a/packages/contracts/generated/api/console/features/types.gen.ts +++ b/packages/contracts/generated/api/console/features/types.gen.ts @@ -22,7 +22,7 @@ export type FeatureModel = { model_load_balancing_enabled: boolean next_credit_reset_date: number trigger_event: Quota - vector_space: LimitationModel + vector_space: LimitationModel | null webapp_copyright_enabled: boolean workspace_members: LicenseLimitationModel } diff --git a/packages/contracts/generated/api/console/features/zod.gen.ts b/packages/contracts/generated/api/console/features/zod.gen.ts index 0e26f296b60..a5a66a25782 100644 --- a/packages/contracts/generated/api/console/features/zod.gen.ts +++ b/packages/contracts/generated/api/console/features/zod.gen.ts @@ -60,33 +60,48 @@ export const zSubscriptionModel = z.object({ */ export const zBillingModel = z.object({ enabled: z.boolean().default(false), - subscription: zSubscriptionModel, + subscription: zSubscriptionModel.default({ interval: '', plan: 'sandbox' }), }) /** * FeatureModel */ export const zFeatureModel = z.object({ - annotation_quota_limit: zLimitationModel, - api_rate_limit: zQuota, - apps: zLimitationModel, - billing: zBillingModel, + annotation_quota_limit: zLimitationModel.default({ limit: 10, size: 0 }), + api_rate_limit: zQuota.default({ + limit: 5000, + reset_date: 0, + usage: 0, + }), + apps: zLimitationModel.default({ limit: 10, size: 0 }), + billing: zBillingModel.default({ + enabled: false, + subscription: { interval: '', plan: 'sandbox' }, + }), can_replace_logo: z.boolean().default(false), dataset_operator_enabled: z.boolean().default(false), docs_processing: z.string().default('standard'), - documents_upload_quota: zLimitationModel, - education: zEducationModel, + documents_upload_quota: zLimitationModel.default({ limit: 50, size: 0 }), + education: zEducationModel.default({ activated: false, enabled: false }), human_input_email_delivery_enabled: z.boolean().default(false), is_allow_transfer_workspace: z.boolean().default(true), - knowledge_pipeline: zKnowledgePipeline, + knowledge_pipeline: zKnowledgePipeline.default({ publish_enabled: false }), knowledge_rate_limit: z.int().default(10), - members: zLimitationModel, + members: zLimitationModel.default({ limit: 1, size: 0 }), model_load_balancing_enabled: z.boolean().default(false), next_credit_reset_date: z.int().default(0), - trigger_event: zQuota, - vector_space: zLimitationModel, + trigger_event: zQuota.default({ + limit: 3000, + reset_date: 0, + usage: 0, + }), + vector_space: zLimitationModel.nullable().default({ limit: 5, size: 0 }), webapp_copyright_enabled: z.boolean().default(false), - workspace_members: zLicenseLimitationModel, + workspace_members: zLicenseLimitationModel.default({ + enabled: false, + limit: 0, + size: 0, + }), }) /** diff --git a/packages/contracts/generated/api/console/info/types.gen.ts b/packages/contracts/generated/api/console/info/types.gen.ts index 88ee9fd59d2..ceeaeeadaae 100644 --- a/packages/contracts/generated/api/console/info/types.gen.ts +++ b/packages/contracts/generated/api/console/info/types.gen.ts @@ -6,7 +6,7 @@ export type ClientOptions = { export type TenantInfoResponse = { created_at?: number | null - custom_config?: WorkspaceCustomConfigResponse + custom_config?: WorkspaceCustomConfigResponse | null id: string in_trial?: boolean | null name?: string | null diff --git a/packages/contracts/generated/api/console/info/zod.gen.ts b/packages/contracts/generated/api/console/info/zod.gen.ts index f903e9307d9..b3c4d966cec 100644 --- a/packages/contracts/generated/api/console/info/zod.gen.ts +++ b/packages/contracts/generated/api/console/info/zod.gen.ts @@ -15,7 +15,7 @@ export const zWorkspaceCustomConfigResponse = z.object({ */ export const zTenantInfoResponse = z.object({ created_at: z.int().nullish(), - custom_config: zWorkspaceCustomConfigResponse.optional(), + custom_config: zWorkspaceCustomConfigResponse.nullish(), id: z.string(), in_trial: z.boolean().nullish(), name: z.string().nullish(), diff --git a/packages/contracts/generated/api/console/installed-apps/types.gen.ts b/packages/contracts/generated/api/console/installed-apps/types.gen.ts index b1b08934c84..355b24eeb6f 100644 --- a/packages/contracts/generated/api/console/installed-apps/types.gen.ts +++ b/packages/contracts/generated/api/console/installed-apps/types.gen.ts @@ -143,9 +143,7 @@ export type DeleteInstalledAppsByInstalledAppIdData = { } export type DeleteInstalledAppsByInstalledAppIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteInstalledAppsByInstalledAppIdResponse @@ -288,9 +286,7 @@ export type DeleteInstalledAppsByInstalledAppIdConversationsByCIdData = { } export type DeleteInstalledAppsByInstalledAppIdConversationsByCIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteInstalledAppsByInstalledAppIdConversationsByCIdResponse @@ -510,9 +506,7 @@ export type DeleteInstalledAppsByInstalledAppIdSavedMessagesByMessageIdData = { } export type DeleteInstalledAppsByInstalledAppIdSavedMessagesByMessageIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteInstalledAppsByInstalledAppIdSavedMessagesByMessageIdResponse diff --git a/packages/contracts/generated/api/console/installed-apps/zod.gen.ts b/packages/contracts/generated/api/console/installed-apps/zod.gen.ts index 6fd4856c933..bfa75c9d997 100644 --- a/packages/contracts/generated/api/console/installed-apps/zod.gen.ts +++ b/packages/contracts/generated/api/console/installed-apps/zod.gen.ts @@ -155,7 +155,7 @@ export const zDeleteInstalledAppsByInstalledAppIdPath = z.object({ /** * App uninstalled successfully */ -export const zDeleteInstalledAppsByInstalledAppIdResponse = z.record(z.string(), z.never()) +export const zDeleteInstalledAppsByInstalledAppIdResponse = z.void() export const zPatchInstalledAppsByInstalledAppIdPath = z.object({ installed_app_id: z.string(), @@ -255,10 +255,7 @@ export const zDeleteInstalledAppsByInstalledAppIdConversationsByCIdPath = z.obje /** * Conversation deleted successfully */ -export const zDeleteInstalledAppsByInstalledAppIdConversationsByCIdResponse = z.record( - z.string(), - z.never(), -) +export const zDeleteInstalledAppsByInstalledAppIdConversationsByCIdResponse = z.void() export const zPostInstalledAppsByInstalledAppIdConversationsByCIdNameBody = zConversationRenamePayload @@ -407,10 +404,7 @@ export const zDeleteInstalledAppsByInstalledAppIdSavedMessagesByMessageIdPath = /** * Saved message deleted successfully */ -export const zDeleteInstalledAppsByInstalledAppIdSavedMessagesByMessageIdResponse = z.record( - z.string(), - z.never(), -) +export const zDeleteInstalledAppsByInstalledAppIdSavedMessagesByMessageIdResponse = z.void() export const zPostInstalledAppsByInstalledAppIdTextToAudioBody = zTextToAudioPayload diff --git a/packages/contracts/generated/api/console/notion/types.gen.ts b/packages/contracts/generated/api/console/notion/types.gen.ts index ac0431b2d31..92b26116c9e 100644 --- a/packages/contracts/generated/api/console/notion/types.gen.ts +++ b/packages/contracts/generated/api/console/notion/types.gen.ts @@ -21,7 +21,7 @@ export type NotionIntegrateWorkspaceResponse = { export type NotionIntegratePageResponse = { is_bound: boolean - page_icon: DataSourceIntegrateIconResponse + page_icon: DataSourceIntegrateIconResponse | null page_id: string page_name: string parent_id: string | null diff --git a/packages/contracts/generated/api/console/notion/zod.gen.ts b/packages/contracts/generated/api/console/notion/zod.gen.ts index 6e371766b9b..633ae90be35 100644 --- a/packages/contracts/generated/api/console/notion/zod.gen.ts +++ b/packages/contracts/generated/api/console/notion/zod.gen.ts @@ -23,7 +23,7 @@ export const zDataSourceIntegrateIconResponse = z.object({ */ export const zNotionIntegratePageResponse = z.object({ is_bound: z.boolean(), - page_icon: zDataSourceIntegrateIconResponse, + page_icon: zDataSourceIntegrateIconResponse.nullable(), page_id: z.string(), page_name: z.string(), parent_id: z.string().nullable(), diff --git a/packages/contracts/generated/api/console/rag/types.gen.ts b/packages/contracts/generated/api/console/rag/types.gen.ts index ad46a4b2765..00d9fe00132 100644 --- a/packages/contracts/generated/api/console/rag/types.gen.ts +++ b/packages/contracts/generated/api/console/rag/types.gen.ts @@ -47,7 +47,7 @@ export type DatasetDetailResponse = { embedding_model_provider: string | null enable_api: boolean external_knowledge_info?: DatasetExternalKnowledgeInfoResponse - external_retrieval_model: DatasetExternalRetrievalModelResponse + external_retrieval_model: DatasetExternalRetrievalModelResponse | null icon_info?: DatasetIconInfoResponse id: string indexing_technique: string | null @@ -115,8 +115,8 @@ export type SimpleResultResponse = { export type WorkflowRunDetailResponse = { created_at?: number | null - created_by_account?: SimpleAccount - created_by_end_user?: SimpleEndUser + created_by_account?: SimpleAccount | null + created_by_end_user?: SimpleEndUser | null created_by_role?: string | null elapsed_time?: number | null error?: string | null @@ -146,7 +146,7 @@ export type WorkflowPaginationResponse = { export type WorkflowResponse = { conversation_variables: Array created_at: number - created_by?: SimpleAccount + created_by?: SimpleAccount | null environment_variables: Array features: { [key: string]: unknown @@ -161,7 +161,7 @@ export type WorkflowResponse = { rag_pipeline_variables: Array tool_published: boolean updated_at: number - updated_by?: SimpleAccount + updated_by?: SimpleAccount | null version: string } @@ -190,8 +190,8 @@ export type DatasourceVariablesPayload = { export type WorkflowRunNodeExecutionResponse = { created_at?: number | null - created_by_account?: SimpleAccount - created_by_end_user?: SimpleEndUser + created_by_account?: SimpleAccount | null + created_by_end_user?: SimpleEndUser | null created_by_role?: string | null elapsed_time?: number | null error?: string | null @@ -238,7 +238,7 @@ export type DraftWorkflowRunPayload = { export type WorkflowDraftVariablePatchPayload = { name?: string | null - value?: unknown + value?: unknown | null } export type RagPipelineWorkflowPublishResponse = { @@ -304,7 +304,7 @@ export type DatasetRetrievalModelResponse = { score_threshold_enabled: boolean search_method: string top_k: number - weights?: DatasetWeightedScoreResponse + weights?: DatasetWeightedScoreResponse | null } export type DatasetSummaryIndexSettingResponse = { @@ -336,12 +336,12 @@ export type PipelineTemplateItemResponse = { export type PluginDependency = { current_identifier?: string | null type: Type - value: unknown + value: Github | Marketplace | Package } export type WorkflowRunForListResponse = { created_at?: number | null - created_by_account?: SimpleAccount + created_by_account?: SimpleAccount | null elapsed_time?: number | null exceptions_count?: number | null finished_at?: number | null @@ -455,9 +455,7 @@ export type DeleteRagPipelineCustomizedTemplatesByTemplateIdData = { } export type DeleteRagPipelineCustomizedTemplatesByTemplateIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteRagPipelineCustomizedTemplatesByTemplateIdResponse @@ -473,9 +471,7 @@ export type PatchRagPipelineCustomizedTemplatesByTemplateIdData = { } export type PatchRagPipelineCustomizedTemplatesByTemplateIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type PatchRagPipelineCustomizedTemplatesByTemplateIdResponse @@ -681,9 +677,7 @@ export type PostRagPipelinesByPipelineIdCustomizedPublishData = { } export type PostRagPipelinesByPipelineIdCustomizedPublishResponses = { - 204: { - [key: string]: never - } + 204: void } export type PostRagPipelinesByPipelineIdCustomizedPublishResponse @@ -1360,9 +1354,7 @@ export type DeleteRagPipelinesByPipelineIdWorkflowsByWorkflowIdData = { } export type DeleteRagPipelinesByPipelineIdWorkflowsByWorkflowIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteRagPipelinesByPipelineIdWorkflowsByWorkflowIdResponse diff --git a/packages/contracts/generated/api/console/rag/zod.gen.ts b/packages/contracts/generated/api/console/rag/zod.gen.ts index 9c0c586bc6b..7506d274fec 100644 --- a/packages/contracts/generated/api/console/rag/zod.gen.ts +++ b/packages/contracts/generated/api/console/rag/zod.gen.ts @@ -118,7 +118,7 @@ export const zDraftWorkflowRunPayload = z.object({ */ export const zWorkflowDraftVariablePatchPayload = z.object({ name: z.string().nullish(), - value: z.unknown().optional(), + value: z.unknown().nullish(), }) /** @@ -261,7 +261,7 @@ export const zSimpleAccount = z.object({ */ export const zWorkflowRunForListResponse = z.object({ created_at: z.int().nullish(), - created_by_account: zSimpleAccount.optional(), + created_by_account: zSimpleAccount.nullish(), elapsed_time: z.number().nullish(), exceptions_count: z.int().nullish(), finished_at: z.int().nullish(), @@ -297,8 +297,8 @@ export const zSimpleEndUser = z.object({ */ export const zWorkflowRunDetailResponse = z.object({ created_at: z.int().nullish(), - created_by_account: zSimpleAccount.optional(), - created_by_end_user: zSimpleEndUser.optional(), + created_by_account: zSimpleAccount.nullish(), + created_by_end_user: zSimpleEndUser.nullish(), created_by_role: z.string().nullish(), elapsed_time: z.number().nullish(), error: z.string().nullish(), @@ -319,8 +319,8 @@ export const zWorkflowRunDetailResponse = z.object({ */ export const zWorkflowRunNodeExecutionResponse = z.object({ created_at: z.int().nullish(), - created_by_account: zSimpleAccount.optional(), - created_by_end_user: zSimpleEndUser.optional(), + created_by_account: zSimpleAccount.nullish(), + created_by_end_user: zSimpleEndUser.nullish(), created_by_role: z.string().nullish(), elapsed_time: z.number().nullish(), error: z.string().nullish(), @@ -397,7 +397,7 @@ export const zPipelineVariableResponse = z.object({ export const zWorkflowResponse = z.object({ conversation_variables: z.array(zWorkflowConversationVariableResponse), created_at: z.int(), - created_by: zSimpleAccount.optional(), + created_by: zSimpleAccount.nullish(), environment_variables: z.array(zWorkflowEnvironmentVariableResponse), features: z.record(z.string(), z.unknown()), graph: z.record(z.string(), z.unknown()), @@ -408,7 +408,7 @@ export const zWorkflowResponse = z.object({ rag_pipeline_variables: z.array(zPipelineVariableResponse), tool_published: z.boolean(), updated_at: z.int(), - updated_by: zSimpleAccount.optional(), + updated_by: zSimpleAccount.nullish(), version: z.string(), }) @@ -435,22 +435,6 @@ export const zDatasetRerankingModelResponse = z.object({ */ export const zType = z.enum(['github', 'marketplace', 'package']) -/** - * PluginDependency - */ -export const zPluginDependency = z.object({ - current_identifier: z.string().nullish(), - type: zType, - value: z.unknown(), -}) - -/** - * RagPipelineImportCheckDependenciesResponse - */ -export const zRagPipelineImportCheckDependenciesResponse = z.object({ - leaked_dependencies: z.array(zPluginDependency).optional(), -}) - /** * Github */ @@ -477,6 +461,22 @@ export const zPackage = z.object({ version: z.string().nullish(), }) +/** + * PluginDependency + */ +export const zPluginDependency = z.object({ + current_identifier: z.string().nullish(), + type: zType, + value: z.union([zGithub, zMarketplace, zPackage]), +}) + +/** + * RagPipelineImportCheckDependenciesResponse + */ +export const zRagPipelineImportCheckDependenciesResponse = z.object({ + leaked_dependencies: z.array(zPluginDependency).optional(), +}) + /** * DatasetKeywordSettingResponse */ @@ -513,7 +513,7 @@ export const zDatasetRetrievalModelResponse = z.object({ score_threshold_enabled: z.boolean(), search_method: z.string(), top_k: z.int(), - weights: zDatasetWeightedScoreResponse.optional(), + weights: zDatasetWeightedScoreResponse.nullish(), }) /** @@ -536,7 +536,7 @@ export const zDatasetDetailResponse = z.object({ embedding_model_provider: z.string().nullable(), enable_api: z.boolean(), external_knowledge_info: zDatasetExternalKnowledgeInfoResponse.optional(), - external_retrieval_model: zDatasetExternalRetrievalModelResponse, + external_retrieval_model: zDatasetExternalRetrievalModelResponse.nullable(), icon_info: zDatasetIconInfoResponse.optional(), id: z.string(), indexing_technique: z.string().nullable(), @@ -564,10 +564,7 @@ export const zDeleteRagPipelineCustomizedTemplatesByTemplateIdPath = z.object({ /** * Pipeline template deleted */ -export const zDeleteRagPipelineCustomizedTemplatesByTemplateIdResponse = z.record( - z.string(), - z.never(), -) +export const zDeleteRagPipelineCustomizedTemplatesByTemplateIdResponse = z.void() export const zPatchRagPipelineCustomizedTemplatesByTemplateIdBody = zCustomizedPipelineTemplatePayload @@ -579,10 +576,7 @@ export const zPatchRagPipelineCustomizedTemplatesByTemplateIdPath = z.object({ /** * Pipeline template updated */ -export const zPatchRagPipelineCustomizedTemplatesByTemplateIdResponse = z.record( - z.string(), - z.never(), -) +export const zPatchRagPipelineCustomizedTemplatesByTemplateIdResponse = z.void() export const zPostRagPipelineCustomizedTemplatesByTemplateIdPath = z.object({ template_id: z.string(), @@ -685,10 +679,7 @@ export const zPostRagPipelinesByPipelineIdCustomizedPublishPath = z.object({ /** * Pipeline template published */ -export const zPostRagPipelinesByPipelineIdCustomizedPublishResponse = z.record( - z.string(), - z.never(), -) +export const zPostRagPipelinesByPipelineIdCustomizedPublishResponse = z.void() export const zGetRagPipelinesByPipelineIdExportsPath = z.object({ pipeline_id: z.string(), @@ -1134,10 +1125,7 @@ export const zDeleteRagPipelinesByPipelineIdWorkflowsByWorkflowIdPath = z.object /** * Workflow deleted successfully */ -export const zDeleteRagPipelinesByPipelineIdWorkflowsByWorkflowIdResponse = z.record( - z.string(), - z.never(), -) +export const zDeleteRagPipelinesByPipelineIdWorkflowsByWorkflowIdResponse = z.void() export const zPatchRagPipelinesByPipelineIdWorkflowsByWorkflowIdPath = z.object({ pipeline_id: z.string(), diff --git a/packages/contracts/generated/api/console/snippets/types.gen.ts b/packages/contracts/generated/api/console/snippets/types.gen.ts index ae46ee98134..f12b39725db 100644 --- a/packages/contracts/generated/api/console/snippets/types.gen.ts +++ b/packages/contracts/generated/api/console/snippets/types.gen.ts @@ -12,8 +12,8 @@ export type WorkflowRunPaginationResponse = { export type WorkflowRunDetailResponse = { created_at?: number | null - created_by_account?: SimpleAccount - created_by_end_user?: SimpleEndUser + created_by_account?: SimpleAccount | null + created_by_end_user?: SimpleEndUser | null created_by_role?: string | null elapsed_time?: number | null error?: string | null @@ -43,7 +43,7 @@ export type WorkflowPaginationResponse = { export type SnippetWorkflowResponse = { conversation_variables: Array created_at: number - created_by?: SimpleAccount + created_by?: SimpleAccount | null environment_variables: Array features: { [key: string]: unknown @@ -61,7 +61,7 @@ export type SnippetWorkflowResponse = { rag_pipeline_variables: Array tool_published: boolean updated_at: number - updated_by?: SimpleAccount + updated_by?: SimpleAccount | null version: string } @@ -96,8 +96,8 @@ export type SnippetLoopNodeRunPayload = { export type WorkflowRunNodeExecutionResponse = { created_at?: number | null - created_by_account?: SimpleAccount - created_by_end_user?: SimpleEndUser + created_by_account?: SimpleAccount | null + created_by_end_user?: SimpleEndUser | null created_by_role?: string | null elapsed_time?: number | null error?: string | null @@ -165,7 +165,7 @@ export type WorkflowDraftVariable = { export type WorkflowDraftVariableUpdatePayload = { name?: string | null - value?: unknown + value?: unknown | null } export type PublishWorkflowPayload = { @@ -176,7 +176,7 @@ export type PublishWorkflowPayload = { export type WorkflowRunForListResponse = { created_at?: number | null - created_by_account?: SimpleAccount + created_by_account?: SimpleAccount | null elapsed_time?: number | null exceptions_count?: number | null finished_at?: number | null @@ -204,7 +204,7 @@ export type SimpleEndUser = { export type WorkflowResponse = { conversation_variables: Array created_at: number - created_by?: SimpleAccount + created_by?: SimpleAccount | null environment_variables: Array features: { [key: string]: unknown @@ -219,7 +219,7 @@ export type WorkflowResponse = { rag_pipeline_variables: Array tool_published: boolean updated_at: number - updated_by?: SimpleAccount + updated_by?: SimpleAccount | null version: string } @@ -630,9 +630,7 @@ export type DeleteSnippetsBySnippetIdWorkflowsDraftNodesByNodeIdVariablesData = } export type DeleteSnippetsBySnippetIdWorkflowsDraftNodesByNodeIdVariablesResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteSnippetsBySnippetIdWorkflowsDraftNodesByNodeIdVariablesResponse @@ -708,9 +706,7 @@ export type DeleteSnippetsBySnippetIdWorkflowsDraftVariablesData = { } export type DeleteSnippetsBySnippetIdWorkflowsDraftVariablesResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteSnippetsBySnippetIdWorkflowsDraftVariablesResponse @@ -755,9 +751,7 @@ export type DeleteSnippetsBySnippetIdWorkflowsDraftVariablesByVariableIdError = DeleteSnippetsBySnippetIdWorkflowsDraftVariablesByVariableIdErrors[keyof DeleteSnippetsBySnippetIdWorkflowsDraftVariablesByVariableIdErrors] export type DeleteSnippetsBySnippetIdWorkflowsDraftVariablesByVariableIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteSnippetsBySnippetIdWorkflowsDraftVariablesByVariableIdResponse @@ -836,9 +830,7 @@ export type PutSnippetsBySnippetIdWorkflowsDraftVariablesByVariableIdResetError export type PutSnippetsBySnippetIdWorkflowsDraftVariablesByVariableIdResetResponses = { 200: WorkflowDraftVariable - 204: { - [key: string]: never - } + 204: void } export type PutSnippetsBySnippetIdWorkflowsDraftVariablesByVariableIdResetResponse diff --git a/packages/contracts/generated/api/console/snippets/zod.gen.ts b/packages/contracts/generated/api/console/snippets/zod.gen.ts index 20b932592a6..85a1f63d45e 100644 --- a/packages/contracts/generated/api/console/snippets/zod.gen.ts +++ b/packages/contracts/generated/api/console/snippets/zod.gen.ts @@ -76,7 +76,7 @@ export const zWorkflowDraftVariableList = z.object({ */ export const zWorkflowDraftVariableUpdatePayload = z.object({ name: z.string().nullish(), - value: z.unknown().optional(), + value: z.unknown().nullish(), }) /** @@ -102,7 +102,7 @@ export const zSimpleAccount = z.object({ */ export const zWorkflowRunForListResponse = z.object({ created_at: z.int().nullish(), - created_by_account: zSimpleAccount.optional(), + created_by_account: zSimpleAccount.nullish(), elapsed_time: z.number().nullish(), exceptions_count: z.int().nullish(), finished_at: z.int().nullish(), @@ -138,8 +138,8 @@ export const zSimpleEndUser = z.object({ */ export const zWorkflowRunDetailResponse = z.object({ created_at: z.int().nullish(), - created_by_account: zSimpleAccount.optional(), - created_by_end_user: zSimpleEndUser.optional(), + created_by_account: zSimpleAccount.nullish(), + created_by_end_user: zSimpleEndUser.nullish(), created_by_role: z.string().nullish(), elapsed_time: z.number().nullish(), error: z.string().nullish(), @@ -160,8 +160,8 @@ export const zWorkflowRunDetailResponse = z.object({ */ export const zWorkflowRunNodeExecutionResponse = z.object({ created_at: z.int().nullish(), - created_by_account: zSimpleAccount.optional(), - created_by_end_user: zSimpleEndUser.optional(), + created_by_account: zSimpleAccount.nullish(), + created_by_end_user: zSimpleEndUser.nullish(), created_by_role: z.string().nullish(), elapsed_time: z.number().nullish(), error: z.string().nullish(), @@ -238,7 +238,7 @@ export const zPipelineVariableResponse = z.object({ export const zSnippetWorkflowResponse = z.object({ conversation_variables: z.array(zWorkflowConversationVariableResponse), created_at: z.int(), - created_by: zSimpleAccount.optional(), + created_by: zSimpleAccount.nullish(), environment_variables: z.array(zWorkflowEnvironmentVariableResponse), features: z.record(z.string(), z.unknown()), graph: z.record(z.string(), z.unknown()), @@ -250,7 +250,7 @@ export const zSnippetWorkflowResponse = z.object({ rag_pipeline_variables: z.array(zPipelineVariableResponse), tool_published: z.boolean(), updated_at: z.int(), - updated_by: zSimpleAccount.optional(), + updated_by: zSimpleAccount.nullish(), version: z.string(), }) @@ -260,7 +260,7 @@ export const zSnippetWorkflowResponse = z.object({ export const zWorkflowResponse = z.object({ conversation_variables: z.array(zWorkflowConversationVariableResponse), created_at: z.int(), - created_by: zSimpleAccount.optional(), + created_by: zSimpleAccount.nullish(), environment_variables: z.array(zWorkflowEnvironmentVariableResponse), features: z.record(z.string(), z.unknown()), graph: z.record(z.string(), z.unknown()), @@ -271,7 +271,7 @@ export const zWorkflowResponse = z.object({ rag_pipeline_variables: z.array(zPipelineVariableResponse), tool_published: z.boolean(), updated_at: z.int(), - updated_by: zSimpleAccount.optional(), + updated_by: zSimpleAccount.nullish(), version: z.string(), }) @@ -487,10 +487,7 @@ export const zDeleteSnippetsBySnippetIdWorkflowsDraftNodesByNodeIdVariablesPath /** * Node variables deleted successfully */ -export const zDeleteSnippetsBySnippetIdWorkflowsDraftNodesByNodeIdVariablesResponse = z.record( - z.string(), - z.never(), -) +export const zDeleteSnippetsBySnippetIdWorkflowsDraftNodesByNodeIdVariablesResponse = z.void() export const zGetSnippetsBySnippetIdWorkflowsDraftNodesByNodeIdVariablesPath = z.object({ node_id: z.string(), @@ -531,10 +528,7 @@ export const zDeleteSnippetsBySnippetIdWorkflowsDraftVariablesPath = z.object({ /** * Workflow variables deleted successfully */ -export const zDeleteSnippetsBySnippetIdWorkflowsDraftVariablesResponse = z.record( - z.string(), - z.never(), -) +export const zDeleteSnippetsBySnippetIdWorkflowsDraftVariablesResponse = z.void() export const zGetSnippetsBySnippetIdWorkflowsDraftVariablesPath = z.object({ snippet_id: z.string(), @@ -559,10 +553,7 @@ export const zDeleteSnippetsBySnippetIdWorkflowsDraftVariablesByVariableIdPath = /** * Variable deleted successfully */ -export const zDeleteSnippetsBySnippetIdWorkflowsDraftVariablesByVariableIdResponse = z.record( - z.string(), - z.never(), -) +export const zDeleteSnippetsBySnippetIdWorkflowsDraftVariablesByVariableIdResponse = z.void() export const zGetSnippetsBySnippetIdWorkflowsDraftVariablesByVariableIdPath = z.object({ snippet_id: z.string(), @@ -596,7 +587,7 @@ export const zPutSnippetsBySnippetIdWorkflowsDraftVariablesByVariableIdResetPath export const zPutSnippetsBySnippetIdWorkflowsDraftVariablesByVariableIdResetResponse = z.union([ zWorkflowDraftVariable, - z.record(z.string(), z.never()), + z.void(), ]) export const zGetSnippetsBySnippetIdWorkflowsPublishPath = z.object({ diff --git a/packages/contracts/generated/api/console/system-features/zod.gen.ts b/packages/contracts/generated/api/console/system-features/zod.gen.ts index bbd08821186..7464057a391 100644 --- a/packages/contracts/generated/api/console/system-features/zod.gen.ts +++ b/packages/contracts/generated/api/console/system-features/zod.gen.ts @@ -43,8 +43,12 @@ export const zLicenseLimitationModel = z.object({ */ export const zLicenseModel = z.object({ expired_at: z.string().default(''), - status: zLicenseStatus, - workspaces: zLicenseLimitationModel, + status: zLicenseStatus.default('none'), + workspaces: zLicenseLimitationModel.default({ + enabled: false, + limit: 0, + size: 0, + }), }) /** @@ -61,7 +65,7 @@ export const zPluginInstallationScope = z.enum([ * PluginInstallationPermissionModel */ export const zPluginInstallationPermissionModel = z.object({ - plugin_installation_scope: zPluginInstallationScope, + plugin_installation_scope: zPluginInstallationScope.default('all'), restrict_to_marketplace_only: z.boolean().default(false), }) @@ -80,14 +84,20 @@ export const zWebAppAuthModel = z.object({ allow_email_password_login: z.boolean().default(false), allow_sso: z.boolean().default(false), enabled: z.boolean().default(false), - sso_config: zWebAppAuthSsoModel, + sso_config: zWebAppAuthSsoModel.default({ protocol: '' }), }) /** * SystemFeatureModel */ export const zSystemFeatureModel = z.object({ - branding: zBrandingModel, + branding: zBrandingModel.default({ + application_title: '', + enabled: false, + favicon: '', + login_page_logo: '', + workspace_logo: '', + }), enable_change_email: z.boolean().default(true), enable_collaboration_mode: z.boolean().default(true), enable_creators_platform: z.boolean().default(false), @@ -100,13 +110,30 @@ export const zSystemFeatureModel = z.object({ is_allow_create_workspace: z.boolean().default(false), is_allow_register: z.boolean().default(false), is_email_setup: z.boolean().default(false), - license: zLicenseModel, + license: zLicenseModel.default({ + expired_at: '', + status: 'none', + workspaces: { + enabled: false, + limit: 0, + size: 0, + }, + }), max_plugin_package_size: z.int().default(15728640), - plugin_installation_permission: zPluginInstallationPermissionModel, - plugin_manager: zPluginManagerModel, + plugin_installation_permission: zPluginInstallationPermissionModel.default({ + plugin_installation_scope: 'all', + restrict_to_marketplace_only: false, + }), + plugin_manager: zPluginManagerModel.default({ enabled: false }), sso_enforced_for_signin: z.boolean().default(false), sso_enforced_for_signin_protocol: z.string().default(''), - webapp_auth: zWebAppAuthModel, + webapp_auth: zWebAppAuthModel.default({ + allow_email_code_login: false, + allow_email_password_login: false, + allow_sso: false, + enabled: false, + sso_config: { protocol: '' }, + }), }) /** diff --git a/packages/contracts/generated/api/console/tags/types.gen.ts b/packages/contracts/generated/api/console/tags/types.gen.ts index 8ecf1a55e75..2f80bf565f0 100644 --- a/packages/contracts/generated/api/console/tags/types.gen.ts +++ b/packages/contracts/generated/api/console/tags/types.gen.ts @@ -61,9 +61,7 @@ export type DeleteTagsByTagIdData = { } export type DeleteTagsByTagIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteTagsByTagIdResponse = DeleteTagsByTagIdResponses[keyof DeleteTagsByTagIdResponses] diff --git a/packages/contracts/generated/api/console/tags/zod.gen.ts b/packages/contracts/generated/api/console/tags/zod.gen.ts index b0479b09508..2c83bb29ac5 100644 --- a/packages/contracts/generated/api/console/tags/zod.gen.ts +++ b/packages/contracts/generated/api/console/tags/zod.gen.ts @@ -58,7 +58,7 @@ export const zDeleteTagsByTagIdPath = z.object({ /** * Tag deleted successfully */ -export const zDeleteTagsByTagIdResponse = z.record(z.string(), z.never()) +export const zDeleteTagsByTagIdResponse = z.void() export const zPatchTagsByTagIdBody = zTagUpdateRequestPayload diff --git a/packages/contracts/generated/api/console/website/orpc.gen.ts b/packages/contracts/generated/api/console/website/orpc.gen.ts index 5ed1fcd8abf..3632d32121a 100644 --- a/packages/contracts/generated/api/console/website/orpc.gen.ts +++ b/packages/contracts/generated/api/console/website/orpc.gen.ts @@ -32,7 +32,7 @@ export const get = oc .input( z.object({ params: zGetWebsiteCrawlStatusByJobIdPath, - query: zGetWebsiteCrawlStatusByJobIdQuery, + query: zGetWebsiteCrawlStatusByJobIdQuery.optional(), }), ) .output(zGetWebsiteCrawlStatusByJobIdResponse) diff --git a/packages/contracts/generated/api/console/website/types.gen.ts b/packages/contracts/generated/api/console/website/types.gen.ts index c8ea0de7b65..8dba2c1370c 100644 --- a/packages/contracts/generated/api/console/website/types.gen.ts +++ b/packages/contracts/generated/api/console/website/types.gen.ts @@ -40,8 +40,8 @@ export type GetWebsiteCrawlStatusByJobIdData = { path: { job_id: string } - query: { - provider: 'firecrawl' | 'jinareader' | 'watercrawl' + query?: { + provider?: string } url: '/website/crawl/status/{job_id}' } diff --git a/packages/contracts/generated/api/console/website/zod.gen.ts b/packages/contracts/generated/api/console/website/zod.gen.ts index 88f1d4a7c81..13038ac0b1a 100644 --- a/packages/contracts/generated/api/console/website/zod.gen.ts +++ b/packages/contracts/generated/api/console/website/zod.gen.ts @@ -23,7 +23,7 @@ export const zGetWebsiteCrawlStatusByJobIdPath = z.object({ }) export const zGetWebsiteCrawlStatusByJobIdQuery = z.object({ - provider: z.enum(['firecrawl', 'jinareader', 'watercrawl']), + provider: z.string().optional(), }) /** diff --git a/packages/contracts/generated/api/console/workflow/types.gen.ts b/packages/contracts/generated/api/console/workflow/types.gen.ts index cf794515d40..e8a205378a3 100644 --- a/packages/contracts/generated/api/console/workflow/types.gen.ts +++ b/packages/contracts/generated/api/console/workflow/types.gen.ts @@ -18,7 +18,7 @@ export type PausedNodeResponse = { export type HumanInputPauseTypeResponse = { backstage_input_url?: string | null form_id: string - type: string + type: 'human_input' } export type GetWorkflowByWorkflowRunIdEventsData = { diff --git a/packages/contracts/generated/api/console/workflow/zod.gen.ts b/packages/contracts/generated/api/console/workflow/zod.gen.ts index 6a737a683f8..2a834f2ec3a 100644 --- a/packages/contracts/generated/api/console/workflow/zod.gen.ts +++ b/packages/contracts/generated/api/console/workflow/zod.gen.ts @@ -8,7 +8,7 @@ import * as z from 'zod' export const zHumanInputPauseTypeResponse = z.object({ backstage_input_url: z.string().nullish(), form_id: z.string(), - type: z.string(), + type: z.literal('human_input'), }) /** diff --git a/packages/contracts/generated/api/console/workspaces/types.gen.ts b/packages/contracts/generated/api/console/workspaces/types.gen.ts index 696620bbe5e..335634279a5 100644 --- a/packages/contracts/generated/api/console/workspaces/types.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/types.gen.ts @@ -6,7 +6,7 @@ export type ClientOptions = { export type TenantInfoResponse = { created_at?: number | null - custom_config?: WorkspaceCustomConfigResponse + custom_config?: WorkspaceCustomConfigResponse | null id: string in_trial?: boolean | null name?: string | null @@ -32,7 +32,7 @@ export type CreateSnippetPayload = { graph?: { [key: string]: unknown } | null - icon_info?: IconInfo + icon_info?: IconInfo | null input_fields?: Array | null name: string type?: 'group' | 'node' @@ -77,7 +77,7 @@ export type SnippetImportPayload = { export type UpdateSnippetPayload = { description?: string | null - icon_info?: IconInfo + icon_info?: IconInfo | null name?: string | null } @@ -85,6 +85,8 @@ export type AccountWithRoleList = { accounts: Array } +export type ModelType = 'llm' | 'moderation' | 'rerank' | 'speech2text' | 'text-embedding' | 'tts' + export type ParserPostDefault = { model_settings: Array } @@ -223,7 +225,7 @@ export type ParserDeleteModels = { export type ParserPostModels = { config_from?: string | null credential_id?: string | null - load_balancing?: LoadBalancingPayload + load_balancing?: LoadBalancingPayload | null model: string model_type: ModelType } @@ -459,7 +461,7 @@ export type McpProviderCreatePayload = { icon: string icon_background?: string icon_type: string - identity_mode?: IdentityMode + identity_mode?: IdentityMode | null name: string server_identifier: string server_url: string @@ -478,7 +480,7 @@ export type McpProviderUpdatePayload = { icon: string icon_background?: string icon_type: string - identity_mode?: IdentityMode + identity_mode?: IdentityMode | null name: string provider_id: string server_identifier: string @@ -641,8 +643,6 @@ export type Inner = { export type TenantAccountRole = 'admin' | 'dataset_operator' | 'editor' | 'normal' | 'owner' -export type ModelType = 'llm' | 'moderation' | 'rerank' | 'speech2text' | 'text-embedding' | 'tts' - export type LoadBalancingPayload = { configs?: Array<{ [key: string]: unknown @@ -866,9 +866,7 @@ export type DeleteWorkspacesCurrentCustomizedSnippetsBySnippetIdError = DeleteWorkspacesCurrentCustomizedSnippetsBySnippetIdErrors[keyof DeleteWorkspacesCurrentCustomizedSnippetsBySnippetIdErrors] export type DeleteWorkspacesCurrentCustomizedSnippetsBySnippetIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteWorkspacesCurrentCustomizedSnippetsBySnippetIdResponse @@ -1026,7 +1024,7 @@ export type GetWorkspacesCurrentDefaultModelData = { body?: never path?: never query: { - model_type: string + model_type: ModelType } url: '/workspaces/current/default-model' } @@ -1393,7 +1391,7 @@ export type GetWorkspacesCurrentModelProvidersData = { body?: never path?: never query?: { - model_type?: string | null + model_type?: ModelType | null } url: '/workspaces/current/model-providers' } @@ -1435,9 +1433,7 @@ export type DeleteWorkspacesCurrentModelProvidersByProviderCredentialsData = { } export type DeleteWorkspacesCurrentModelProvidersByProviderCredentialsResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteWorkspacesCurrentModelProvidersByProviderCredentialsResponse @@ -1543,9 +1539,7 @@ export type DeleteWorkspacesCurrentModelProvidersByProviderModelsData = { } export type DeleteWorkspacesCurrentModelProvidersByProviderModelsResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteWorkspacesCurrentModelProvidersByProviderModelsResponse @@ -1597,9 +1591,7 @@ export type DeleteWorkspacesCurrentModelProvidersByProviderModelsCredentialsData } export type DeleteWorkspacesCurrentModelProvidersByProviderModelsCredentialsResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteWorkspacesCurrentModelProvidersByProviderModelsCredentialsResponse @@ -1614,7 +1606,7 @@ export type GetWorkspacesCurrentModelProvidersByProviderModelsCredentialsData = config_from?: string | null credential_id?: string | null model: string - model_type: string + model_type: ModelType } url: '/workspaces/current/model-providers/{provider}/models/credentials' } diff --git a/packages/contracts/generated/api/console/workspaces/zod.gen.ts b/packages/contracts/generated/api/console/workspaces/zod.gen.ts index 7818191daba..e1eab1a76c3 100644 --- a/packages/contracts/generated/api/console/workspaces/zod.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/zod.gen.ts @@ -16,6 +16,20 @@ export const zSnippetImportPayload = z.object({ yaml_url: z.string().nullish(), }) +/** + * ModelType + * + * Enum class for model type. + */ +export const zModelType = z.enum([ + 'llm', + 'moderation', + 'rerank', + 'speech2text', + 'text-embedding', + 'tts', +]) + /** * SimpleResultResponse */ @@ -189,6 +203,71 @@ export const zParserCredentialValidate = z.object({ credentials: z.record(z.string(), z.unknown()), }) +/** + * ParserDeleteModels + */ +export const zParserDeleteModels = z.object({ + model: z.string(), + model_type: zModelType, +}) + +/** + * ParserDeleteCredential + */ +export const zParserDeleteCredential = z.object({ + credential_id: z.string(), + model: z.string(), + model_type: zModelType, +}) + +/** + * ParserCreateCredential + */ +export const zParserCreateCredential = z.object({ + credentials: z.record(z.string(), z.unknown()), + model: z.string(), + model_type: zModelType, + name: z.string().max(30).nullish(), +}) + +/** + * ParserUpdateCredential + */ +export const zParserUpdateCredential = z.object({ + credential_id: z.string(), + credentials: z.record(z.string(), z.unknown()), + model: z.string(), + model_type: zModelType, + name: z.string().max(30).nullish(), +}) + +/** + * ParserSwitch + */ +export const zParserSwitch = z.object({ + credential_id: z.string(), + model: z.string(), + model_type: zModelType, +}) + +/** + * ParserValidate + */ +export const zParserValidate = z.object({ + credentials: z.record(z.string(), z.unknown()), + model: z.string(), + model_type: zModelType, +}) + +/** + * LoadBalancingCredentialPayload + */ +export const zLoadBalancingCredentialPayload = z.object({ + credentials: z.record(z.string(), z.unknown()), + model: z.string(), + model_type: zModelType, +}) + /** * ParserPreferredProviderType */ @@ -433,7 +512,7 @@ export const zWorkspaceCustomConfigResponse = z.object({ */ export const zTenantInfoResponse = z.object({ created_at: z.int().nullish(), - custom_config: zWorkspaceCustomConfigResponse.optional(), + custom_config: zWorkspaceCustomConfigResponse.nullish(), id: z.string(), in_trial: z.boolean().nullish(), name: z.string().nullish(), @@ -465,7 +544,7 @@ export const zIconInfo = z.object({ */ export const zUpdateSnippetPayload = z.object({ description: z.string().max(2000).nullish(), - icon_info: zIconInfo.optional(), + icon_info: zIconInfo.nullish(), name: z.string().min(1).max(255).nullish(), }) @@ -493,7 +572,7 @@ export const zInputFieldDefinition = z.object({ export const zCreateSnippetPayload = z.object({ description: z.string().max(2000).nullish(), graph: z.record(z.string(), z.unknown()).nullish(), - icon_info: zIconInfo.optional(), + icon_info: zIconInfo.nullish(), input_fields: z.array(zInputFieldDefinition).nullish(), name: z.string().min(1).max(255), type: z.enum(['group', 'node']).optional().default('node'), @@ -576,99 +655,6 @@ export const zAccountWithRoleList = z.object({ accounts: z.array(zAccountWithRole), }) -/** - * TenantAccountRole - */ -export const zTenantAccountRole = z.enum(['admin', 'dataset_operator', 'editor', 'normal', 'owner']) - -/** - * MemberInvitePayload - */ -export const zMemberInvitePayload = z.object({ - emails: z.array(z.string()).optional(), - language: z.string().nullish(), - role: zTenantAccountRole, -}) - -/** - * ModelType - * - * Enum class for model type. - */ -export const zModelType = z.enum([ - 'llm', - 'moderation', - 'rerank', - 'speech2text', - 'text-embedding', - 'tts', -]) - -/** - * ParserDeleteModels - */ -export const zParserDeleteModels = z.object({ - model: z.string(), - model_type: zModelType, -}) - -/** - * ParserDeleteCredential - */ -export const zParserDeleteCredential = z.object({ - credential_id: z.string(), - model: z.string(), - model_type: zModelType, -}) - -/** - * ParserCreateCredential - */ -export const zParserCreateCredential = z.object({ - credentials: z.record(z.string(), z.unknown()), - model: z.string(), - model_type: zModelType, - name: z.string().max(30).nullish(), -}) - -/** - * ParserUpdateCredential - */ -export const zParserUpdateCredential = z.object({ - credential_id: z.string(), - credentials: z.record(z.string(), z.unknown()), - model: z.string(), - model_type: zModelType, - name: z.string().max(30).nullish(), -}) - -/** - * ParserSwitch - */ -export const zParserSwitch = z.object({ - credential_id: z.string(), - model: z.string(), - model_type: zModelType, -}) - -/** - * ParserValidate - */ -export const zParserValidate = z.object({ - credentials: z.record(z.string(), z.unknown()), - model: z.string(), - model_type: zModelType, -}) - -/** - * LoadBalancingCredentialPayload - */ -export const zLoadBalancingCredentialPayload = z.object({ - credentials: z.record(z.string(), z.unknown()), - model: z.string(), - model_type: zModelType, -}) - /** * Inner */ @@ -685,6 +671,20 @@ export const zParserPostDefault = z.object({ model_settings: z.array(zInner), }) +/** + * TenantAccountRole + */ +export const zTenantAccountRole = z.enum(['admin', 'dataset_operator', 'editor', 'normal', 'owner']) + +/** + * MemberInvitePayload + */ +export const zMemberInvitePayload = z.object({ + emails: z.array(z.string()).optional(), + language: z.string().nullish(), + role: zTenantAccountRole, +}) + /** * LoadBalancingPayload */ @@ -699,7 +699,7 @@ export const zLoadBalancingPayload = z.object({ export const zParserPostModels = z.object({ config_from: z.string().nullish(), credential_id: z.string().nullish(), - load_balancing: zLoadBalancingPayload.optional(), + load_balancing: zLoadBalancingPayload.nullish(), model: z.string(), model_type: zModelType, }) @@ -726,8 +726,8 @@ export const zParserPermissionChange = z.object({ * PluginPermissionSettingsPayload */ export const zPluginPermissionSettingsPayload = z.object({ - debug_permission: zDebugPermission.optional(), - install_permission: zInstallPermission.optional(), + debug_permission: zDebugPermission.optional().default('everyone'), + install_permission: zInstallPermission.optional().default('everyone'), }) /** @@ -815,7 +815,7 @@ export const zMcpProviderCreatePayload = z.object({ icon: z.string(), icon_background: z.string().optional().default(''), icon_type: z.string(), - identity_mode: zIdentityMode.optional(), + identity_mode: zIdentityMode.nullish(), name: z.string(), server_identifier: z.string(), server_url: z.string(), @@ -831,7 +831,7 @@ export const zMcpProviderUpdatePayload = z.object({ icon: z.string(), icon_background: z.string().optional().default(''), icon_type: z.string(), - identity_mode: zIdentityMode.optional(), + identity_mode: zIdentityMode.nullish(), name: z.string(), provider_id: z.string(), server_identifier: z.string(), @@ -854,8 +854,8 @@ export const zUpgradeMode = z.enum(['all', 'exclude', 'partial']) export const zPluginAutoUpgradeSettingsPayload = z.object({ exclude_plugins: z.array(z.string()).optional(), include_plugins: z.array(z.string()).optional(), - strategy_setting: zStrategySetting.optional(), - upgrade_mode: zUpgradeMode.optional(), + strategy_setting: zStrategySetting.optional().default('fix_only'), + upgrade_mode: zUpgradeMode.optional().default('exclude'), upgrade_time_of_day: z.int().optional().default(0), }) @@ -987,10 +987,7 @@ export const zDeleteWorkspacesCurrentCustomizedSnippetsBySnippetIdPath = z.objec /** * Snippet deleted successfully */ -export const zDeleteWorkspacesCurrentCustomizedSnippetsBySnippetIdResponse = z.record( - z.string(), - z.never(), -) +export const zDeleteWorkspacesCurrentCustomizedSnippetsBySnippetIdResponse = z.void() export const zGetWorkspacesCurrentCustomizedSnippetsBySnippetIdPath = z.object({ snippet_id: z.string(), @@ -1052,7 +1049,7 @@ export const zPostWorkspacesCurrentCustomizedSnippetsBySnippetIdUseCountIncremen export const zGetWorkspacesCurrentDatasetOperatorsResponse = zAccountWithRoleList export const zGetWorkspacesCurrentDefaultModelQuery = z.object({ - model_type: z.string(), + model_type: zModelType, }) /** @@ -1104,7 +1101,7 @@ export const zPostWorkspacesCurrentEndpointsEnableResponse = zEndpointEnableResp export const zGetWorkspacesCurrentEndpointsListQuery = z.object({ page: z.int().gte(1), - page_size: z.int(), + page_size: z.int().gt(0), }) /** @@ -1114,7 +1111,7 @@ export const zGetWorkspacesCurrentEndpointsListResponse = zEndpointListResponse export const zGetWorkspacesCurrentEndpointsListPluginQuery = z.object({ page: z.int().gte(1), - page_size: z.int(), + page_size: z.int().gt(0), plugin_id: z.string(), }) @@ -1216,7 +1213,7 @@ export const zPutWorkspacesCurrentMembersByMemberIdUpdateRoleResponse = z.record ) export const zGetWorkspacesCurrentModelProvidersQuery = z.object({ - model_type: z.string().nullish(), + model_type: zModelType.nullish(), }) /** @@ -1246,10 +1243,7 @@ export const zDeleteWorkspacesCurrentModelProvidersByProviderCredentialsPath = z /** * Credential deleted successfully */ -export const zDeleteWorkspacesCurrentModelProvidersByProviderCredentialsResponse = z.record( - z.string(), - z.never(), -) +export const zDeleteWorkspacesCurrentModelProvidersByProviderCredentialsResponse = z.void() export const zGetWorkspacesCurrentModelProvidersByProviderCredentialsPath = z.object({ provider: z.string(), @@ -1332,10 +1326,7 @@ export const zDeleteWorkspacesCurrentModelProvidersByProviderModelsPath = z.obje /** * Model deleted successfully */ -export const zDeleteWorkspacesCurrentModelProvidersByProviderModelsResponse = z.record( - z.string(), - z.never(), -) +export const zDeleteWorkspacesCurrentModelProvidersByProviderModelsResponse = z.void() export const zGetWorkspacesCurrentModelProvidersByProviderModelsPath = z.object({ provider: z.string(), @@ -1373,10 +1364,7 @@ export const zDeleteWorkspacesCurrentModelProvidersByProviderModelsCredentialsPa /** * Credential deleted successfully */ -export const zDeleteWorkspacesCurrentModelProvidersByProviderModelsCredentialsResponse = z.record( - z.string(), - z.never(), -) +export const zDeleteWorkspacesCurrentModelProvidersByProviderModelsCredentialsResponse = z.void() export const zGetWorkspacesCurrentModelProvidersByProviderModelsCredentialsPath = z.object({ provider: z.string(), @@ -1386,7 +1374,7 @@ export const zGetWorkspacesCurrentModelProvidersByProviderModelsCredentialsQuery config_from: z.string().nullish(), credential_id: z.string().nullish(), model: z.string(), - model_type: z.string(), + model_type: zModelType, }) /** diff --git a/packages/contracts/generated/api/openapi/types.gen.ts b/packages/contracts/generated/api/openapi/types.gen.ts index 619eef42d2a..6d630639592 100644 --- a/packages/contracts/generated/api/openapi/types.gen.ts +++ b/packages/contracts/generated/api/openapi/types.gen.ts @@ -11,7 +11,7 @@ export type AccountPayload = { } export type AccountResponse = { - account?: AccountPayload + account?: AccountPayload | null default_workspace_id?: string | null subject_email?: string | null subject_issuer?: string | null @@ -36,7 +36,7 @@ export type AppDescribeQuery = { } export type AppDescribeResponse = { - info?: AppDescribeInfo + info?: AppDescribeInfo | null input_schema?: { [key: string]: unknown } | null @@ -77,7 +77,7 @@ export type AppInfoResponse = { export type AppListQuery = { limit?: number - mode?: AppMode + mode?: AppMode | null name?: string | null page?: number tag?: string | null @@ -177,7 +177,7 @@ export type ErrorBody = { } export type ErrorDetail = { - loc?: Array + loc?: Array msg: string type: string } @@ -242,7 +242,7 @@ export type Marketplace = { } export type MemberActionResponse = { - result?: string + result?: 'success' } export type MemberInvitePayload = { @@ -254,7 +254,7 @@ export type MemberInviteResponse = { email: string invite_url: string member_id: string - result?: string + result?: 'success' role: string tenant_id: string } @@ -289,7 +289,7 @@ export type MessageMetadata = { retriever_resources?: Array<{ [key: string]: unknown }> - usage?: UsageInfo + usage?: UsageInfo | null } export type OpenApiErrorCode @@ -330,7 +330,7 @@ export type Package = { export type PermittedExternalAppsListQuery = { limit?: number - mode?: AppMode + mode?: AppMode | null name?: string | null page?: number } @@ -346,7 +346,7 @@ export type PermittedExternalAppsListResponse = { export type PluginDependency = { current_identifier?: string | null type: Type - value: unknown + value: Github | Marketplace | Package } export type RevokeResponse = { @@ -386,7 +386,7 @@ export type TagItem = { } export type TaskStopResponse = { - result: string + result: 'success' } export type Type = 'github' | 'marketplace' | 'package' diff --git a/packages/contracts/generated/api/openapi/zod.gen.ts b/packages/contracts/generated/api/openapi/zod.gen.ts index 3c815ad2b6c..6379ffc62e2 100644 --- a/packages/contracts/generated/api/openapi/zod.gen.ts +++ b/packages/contracts/generated/api/openapi/zod.gen.ts @@ -79,7 +79,7 @@ export const zAppMode = z.enum([ */ export const zAppListQuery = z.object({ limit: z.int().gte(1).lte(200).optional().default(20), - mode: zAppMode.optional(), + mode: zAppMode.nullish(), name: z.string().max(200).nullish(), page: z.int().gte(1).optional().default(1), tag: z.string().max(100).nullish(), @@ -160,7 +160,10 @@ export const zDevicePollRequest = z.object({ * ErrorDetail */ export const zErrorDetail = z.object({ - loc: z.array(z.unknown()).optional().default([]), + loc: z + .array(z.union([z.string(), z.int()])) + .optional() + .default([]), msg: z.string(), type: z.string(), }) @@ -269,7 +272,7 @@ export const zMarketplace = z.object({ * MemberActionResponse */ export const zMemberActionResponse = z.object({ - result: z.string().optional().default('success'), + result: z.literal('success').optional().default('success'), }) /** @@ -287,7 +290,7 @@ export const zMemberInviteResponse = z.object({ email: z.string(), invite_url: z.string(), member_id: z.string(), - result: z.string().optional().default('success'), + result: z.literal('success').optional().default('success'), role: z.string(), tenant_id: z.string(), }) @@ -382,7 +385,7 @@ export const zPackage = z.object({ */ export const zPermittedExternalAppsListQuery = z.object({ limit: z.int().gte(1).lte(200).optional().default(20), - mode: zAppMode.optional(), + mode: zAppMode.nullish(), name: z.string().max(200).nullish(), page: z.int().gte(1).optional().default(1), }) @@ -464,7 +467,7 @@ export const zAppDescribeInfo = z.object({ * AppDescribeResponse */ export const zAppDescribeResponse = z.object({ - info: zAppDescribeInfo.optional(), + info: zAppDescribeInfo.nullish(), input_schema: z.record(z.string(), z.unknown()).nullish(), parameters: z.record(z.string(), z.unknown()).nullish(), }) @@ -526,7 +529,7 @@ export const zPermittedExternalAppsListResponse = z.object({ * types it as a required `'success'` rather than an optional field. */ export const zTaskStopResponse = z.object({ - result: z.string(), + result: z.literal('success'), }) /** @@ -540,7 +543,7 @@ export const zType = z.enum(['github', 'marketplace', 'package']) export const zPluginDependency = z.object({ current_identifier: z.string().nullish(), type: zType, - value: z.unknown(), + value: z.union([zGithub, zMarketplace, zPackage]), }) /** @@ -564,7 +567,7 @@ export const zUsageInfo = z.object({ */ export const zMessageMetadata = z.object({ retriever_resources: z.array(z.record(z.string(), z.unknown())).optional().default([]), - usage: zUsageInfo.optional(), + usage: zUsageInfo.nullish(), }) /** @@ -608,7 +611,7 @@ export const zWorkspacePayload = z.object({ * AccountResponse */ export const zAccountResponse = z.object({ - account: zAccountPayload.optional(), + account: zAccountPayload.nullish(), default_workspace_id: z.string().nullish(), subject_email: z.string().nullish(), subject_issuer: z.string().nullish(), diff --git a/packages/contracts/generated/api/service/types.gen.ts b/packages/contracts/generated/api/service/types.gen.ts index a702afe5498..f57d90bcfb7 100644 --- a/packages/contracts/generated/api/service/types.gen.ts +++ b/packages/contracts/generated/api/service/types.gen.ts @@ -132,7 +132,7 @@ export type Condition = { | '≤' | '≥' name: string - value?: unknown + value?: string | Array | number | number | null } export type ConversationListQuery = { @@ -190,9 +190,9 @@ export type DatasetCreatePayload = { external_knowledge_id?: string | null indexing_technique?: 'economy' | 'high_quality' | null name: string - permission?: PermissionEnum + permission?: PermissionEnum | null provider?: string - retrieval_model?: RetrievalModel + retrieval_model?: RetrievalModel | null summary_index_setting?: { [key: string]: unknown } | null @@ -215,7 +215,7 @@ export type DatasetDetailResponse = { embedding_model_provider: string | null enable_api: boolean external_knowledge_info?: DatasetExternalKnowledgeInfoResponse - external_retrieval_model: DatasetExternalRetrievalModelResponse + external_retrieval_model: DatasetExternalRetrievalModelResponse | null icon_info?: DatasetIconInfoResponse id: string indexing_technique: string | null @@ -253,7 +253,7 @@ export type DatasetDetailWithPartialMembersResponse = { embedding_model_provider: string | null enable_api: boolean external_knowledge_info?: DatasetExternalKnowledgeInfoResponse - external_retrieval_model: DatasetExternalRetrievalModelResponse + external_retrieval_model: DatasetExternalRetrievalModelResponse | null icon_info?: DatasetIconInfoResponse id: string indexing_technique: string | null @@ -365,7 +365,7 @@ export type DatasetRetrievalModelResponse = { score_threshold_enabled: boolean search_method: string top_k: number - weights?: DatasetWeightedScoreResponse + weights?: DatasetWeightedScoreResponse | null } export type DatasetSummaryIndexSettingResponse = { @@ -395,8 +395,8 @@ export type DatasetUpdatePayload = { partial_member_list?: Array<{ [key: string]: string }> | null - permission?: PermissionEnum - retrieval_model?: RetrievalModel + permission?: PermissionEnum | null + retrieval_model?: RetrievalModel | null } export type DatasetVectorSettingResponse = { @@ -454,7 +454,7 @@ export type DocumentMetadataResponse = { id: string name: string type: string - value?: unknown + value?: string | number | number | boolean | null } export type DocumentResponse = { @@ -511,8 +511,8 @@ export type DocumentTextCreatePayload = { indexing_technique?: string | null name: string original_document_id?: string | null - process_rule?: ProcessRule - retrieval_model?: RetrievalModel + process_rule?: ProcessRule | null + retrieval_model?: RetrievalModel | null text: string } @@ -520,8 +520,8 @@ export type DocumentTextUpdate = { doc_form?: string doc_language?: string name?: string | null - process_rule?: ProcessRule - retrieval_model?: RetrievalModel + process_rule?: ProcessRule | null + retrieval_model?: RetrievalModel | null text?: string | null } @@ -574,7 +574,7 @@ export type HitTestingChildChunk = { export type HitTestingDocument = { data_source_type: string - doc_metadata: unknown + doc_metadata: unknown | null doc_type: string | null id: string name: string @@ -595,7 +595,7 @@ export type HitTestingPayload = { [key: string]: unknown } | null query: string - retrieval_model?: RetrievalModel + retrieval_model?: RetrievalModel | null } export type HitTestingQuery = { @@ -608,7 +608,7 @@ export type HitTestingRecord = { score: number | null segment: HitTestingSegment summary: string | null - tsne_position: unknown + tsne_position: unknown | null } export type HitTestingResponse = { @@ -685,7 +685,7 @@ export type MetadataArgs = { export type MetadataDetail = { id: string name: string - value?: unknown + value?: string | number | number | null } export type MetadataFilteringCondition = { @@ -723,7 +723,7 @@ export type PreProcessingRule = { export type ProcessRule = { mode: ProcessRuleMode - rules?: Rule + rules?: Rule | null } export type ProcessRuleMode = 'automatic' | 'custom' | 'hierarchical' @@ -744,22 +744,22 @@ export type RetrievalMethod | 'semantic_search' export type RetrievalModel = { - metadata_filtering_conditions?: MetadataFilteringCondition + metadata_filtering_conditions?: MetadataFilteringCondition | null reranking_enable: boolean reranking_mode?: string | null - reranking_model?: RerankingModel + reranking_model?: RerankingModel | null score_threshold?: number | null score_threshold_enabled: boolean search_method: RetrievalMethod top_k: number - weights?: WeightModel + weights?: WeightModel | null } export type Rule = { parent_mode?: 'full-doc' | 'paragraph' | null pre_processing_rules?: Array | null - segmentation?: Segmentation - subchunk_segmentation?: Segmentation + segmentation?: Segmentation | null + subchunk_segmentation?: Segmentation | null } export type SegmentAttachmentResponse = { @@ -937,8 +937,8 @@ export type WeightKeywordSetting = { } export type WeightModel = { - keyword_setting?: WeightKeywordSetting - vector_setting?: WeightVectorSetting + keyword_setting?: WeightKeywordSetting | null + vector_setting?: WeightVectorSetting | null weight_type?: 'customized' | 'keyword_first' | 'semantic_first' | null } @@ -958,13 +958,22 @@ export type WorkflowAppLogPaginationResponse = { export type WorkflowAppLogPartialResponse = { created_at?: number | null - created_by_account?: SimpleAccount - created_by_end_user?: SimpleEndUser + created_by_account?: SimpleAccount | null + created_by_end_user?: SimpleEndUser | null created_by_role?: string | null created_from?: string | null - details?: unknown + details?: + | { + [key: string]: unknown + } + | Array + | string + | number + | number + | boolean + | null id: string - workflow_run?: WorkflowRunForLogResponse + workflow_run?: WorkflowRunForLogResponse | null } export type WorkflowLogQuery = { @@ -980,7 +989,7 @@ export type WorkflowLogQuery = { export type WorkflowRunForLogResponse = { created_at?: number | null - elapsed_time?: unknown + elapsed_time?: number | number | null error?: string | null exceptions_count?: number | null finished_at?: number | null @@ -1005,11 +1014,20 @@ export type WorkflowRunPayload = { export type WorkflowRunResponse = { created_at?: number | null - elapsed_time?: unknown + elapsed_time?: number | number | null error?: string | null finished_at?: number | null id: string - inputs?: unknown + inputs?: + | { + [key: string]: unknown + } + | Array + | string + | number + | number + | boolean + | null outputs?: { [key: string]: unknown } @@ -1205,9 +1223,7 @@ export type DeleteAppsAnnotationsByAnnotationIdError = DeleteAppsAnnotationsByAnnotationIdErrors[keyof DeleteAppsAnnotationsByAnnotationIdErrors] export type DeleteAppsAnnotationsByAnnotationIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteAppsAnnotationsByAnnotationIdResponse @@ -1456,9 +1472,7 @@ export type DeleteConversationsByCIdError = DeleteConversationsByCIdErrors[keyof DeleteConversationsByCIdErrors] export type DeleteConversationsByCIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteConversationsByCIdResponse @@ -1662,9 +1676,7 @@ export type DeleteDatasetsTagsErrors = { export type DeleteDatasetsTagsError = DeleteDatasetsTagsErrors[keyof DeleteDatasetsTagsErrors] export type DeleteDatasetsTagsResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteDatasetsTagsResponse @@ -1759,9 +1771,7 @@ export type PostDatasetsTagsBindingError = PostDatasetsTagsBindingErrors[keyof PostDatasetsTagsBindingErrors] export type PostDatasetsTagsBindingResponses = { - 204: { - [key: string]: never - } + 204: void } export type PostDatasetsTagsBindingResponse @@ -1787,9 +1797,7 @@ export type PostDatasetsTagsUnbindingError = PostDatasetsTagsUnbindingErrors[keyof PostDatasetsTagsUnbindingErrors] export type PostDatasetsTagsUnbindingResponses = { - 204: { - [key: string]: never - } + 204: void } export type PostDatasetsTagsUnbindingResponse @@ -1820,9 +1828,7 @@ export type DeleteDatasetsByDatasetIdError = DeleteDatasetsByDatasetIdErrors[keyof DeleteDatasetsByDatasetIdErrors] export type DeleteDatasetsByDatasetIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteDatasetsByDatasetIdResponse @@ -2192,9 +2198,7 @@ export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdError = DeleteDatasetsByDatasetIdDocumentsByDocumentIdErrors[keyof DeleteDatasetsByDatasetIdDocumentsByDocumentIdErrors] export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdResponse @@ -2388,9 +2392,7 @@ export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdErr = DeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdErrors[keyof DeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdErrors] export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdResponse @@ -2548,9 +2550,7 @@ export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChi export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksByChildChunkIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksByChildChunkIdResponse @@ -2873,9 +2873,7 @@ export type DeleteDatasetsByDatasetIdMetadataByMetadataIdError = DeleteDatasetsByDatasetIdMetadataByMetadataIdErrors[keyof DeleteDatasetsByDatasetIdMetadataByMetadataIdErrors] export type DeleteDatasetsByDatasetIdMetadataByMetadataIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteDatasetsByDatasetIdMetadataByMetadataIdResponse diff --git a/packages/contracts/generated/api/service/zod.gen.ts b/packages/contracts/generated/api/service/zod.gen.ts index b664eec6f27..4575e95b718 100644 --- a/packages/contracts/generated/api/service/zod.gen.ts +++ b/packages/contracts/generated/api/service/zod.gen.ts @@ -170,7 +170,7 @@ export const zCondition = z.object({ '≥', ]), name: z.string(), - value: z.unknown().optional(), + value: z.union([z.string(), z.array(z.string()), z.int(), z.number()]).nullish(), }) /** @@ -408,7 +408,7 @@ export const zDatasetRetrievalModelResponse = z.object({ score_threshold_enabled: z.boolean(), search_method: z.string(), top_k: z.int(), - weights: zDatasetWeightedScoreResponse.optional(), + weights: zDatasetWeightedScoreResponse.nullish(), }) /** @@ -431,7 +431,7 @@ export const zDatasetDetailResponse = z.object({ embedding_model_provider: z.string().nullable(), enable_api: z.boolean(), external_knowledge_info: zDatasetExternalKnowledgeInfoResponse.optional(), - external_retrieval_model: zDatasetExternalRetrievalModelResponse, + external_retrieval_model: zDatasetExternalRetrievalModelResponse.nullable(), icon_info: zDatasetIconInfoResponse.optional(), id: z.string(), indexing_technique: z.string().nullable(), @@ -472,7 +472,7 @@ export const zDatasetDetailWithPartialMembersResponse = z.object({ embedding_model_provider: z.string().nullable(), enable_api: z.boolean(), external_knowledge_info: zDatasetExternalKnowledgeInfoResponse.optional(), - external_retrieval_model: zDatasetExternalRetrievalModelResponse, + external_retrieval_model: zDatasetExternalRetrievalModelResponse.nullable(), icon_info: zDatasetIconInfoResponse.optional(), id: z.string(), indexing_technique: z.string().nullable(), @@ -541,7 +541,7 @@ export const zDocumentMetadataResponse = z.object({ id: z.string(), name: z.string(), type: z.string(), - value: z.unknown().optional(), + value: z.union([z.string(), z.int(), z.number(), z.boolean()]).nullish(), }) /** @@ -691,7 +691,7 @@ export const zHitTestingChildChunk = z.object({ */ export const zHitTestingDocument = z.object({ data_source_type: z.string(), - doc_metadata: z.unknown(), + doc_metadata: z.unknown().nullable(), doc_type: z.string().nullable(), id: z.string(), name: z.string(), @@ -754,7 +754,7 @@ export const zHitTestingRecord = z.object({ score: z.number().nullable(), segment: zHitTestingSegment, summary: z.string().nullable(), - tsne_position: z.unknown(), + tsne_position: z.unknown().nullable(), }) /** @@ -830,7 +830,7 @@ export const zMetadataArgs = z.object({ export const zMetadataDetail = z.object({ id: z.string(), name: z.string(), - value: z.unknown().optional(), + value: z.union([z.string(), z.int(), z.number()]).nullish(), }) /** @@ -1062,8 +1062,8 @@ export const zSegmentation = z.object({ export const zRule = z.object({ parent_mode: z.enum(['full-doc', 'paragraph']).nullish(), pre_processing_rules: z.array(zPreProcessingRule).nullish(), - segmentation: zSegmentation.optional(), - subchunk_segmentation: zSegmentation.optional(), + segmentation: zSegmentation.nullish(), + subchunk_segmentation: zSegmentation.nullish(), }) /** @@ -1071,7 +1071,7 @@ export const zRule = z.object({ */ export const zProcessRule = z.object({ mode: zProcessRuleMode, - rules: zRule.optional(), + rules: zRule.nullish(), }) /** @@ -1121,7 +1121,7 @@ export const zSite = z.object({ icon: z.string().nullish(), icon_background: z.string().nullish(), icon_type: z.string().nullish(), - icon_url: z.string().readonly().nullable(), + icon_url: z.string().nullable(), privacy_policy: z.string().nullish(), show_workflow_steps: z.boolean(), title: z.string(), @@ -1206,8 +1206,8 @@ export const zWeightVectorSetting = z.object({ * WeightModel */ export const zWeightModel = z.object({ - keyword_setting: zWeightKeywordSetting.optional(), - vector_setting: zWeightVectorSetting.optional(), + keyword_setting: zWeightKeywordSetting.nullish(), + vector_setting: zWeightVectorSetting.nullish(), weight_type: z.enum(['customized', 'keyword_first', 'semantic_first']).nullish(), }) @@ -1215,15 +1215,15 @@ export const zWeightModel = z.object({ * RetrievalModel */ export const zRetrievalModel = z.object({ - metadata_filtering_conditions: zMetadataFilteringCondition.optional(), + metadata_filtering_conditions: zMetadataFilteringCondition.nullish(), reranking_enable: z.boolean(), reranking_mode: z.string().nullish(), - reranking_model: zRerankingModel.optional(), + reranking_model: zRerankingModel.nullish(), score_threshold: z.number().nullish(), score_threshold_enabled: z.boolean(), search_method: zRetrievalMethod, top_k: z.int(), - weights: zWeightModel.optional(), + weights: zWeightModel.nullish(), }) /** @@ -1237,9 +1237,9 @@ export const zDatasetCreatePayload = z.object({ external_knowledge_id: z.string().nullish(), indexing_technique: z.enum(['economy', 'high_quality']).nullish(), name: z.string().min(1).max(40), - permission: zPermissionEnum.optional(), + permission: zPermissionEnum.nullish().default('only_me'), provider: z.string().optional().default('vendor'), - retrieval_model: zRetrievalModel.optional(), + retrieval_model: zRetrievalModel.nullish(), summary_index_setting: z.record(z.string(), z.unknown()).nullish(), }) @@ -1256,8 +1256,8 @@ export const zDatasetUpdatePayload = z.object({ indexing_technique: z.enum(['economy', 'high_quality']).nullish(), name: z.string().min(1).max(40).nullish(), partial_member_list: z.array(z.record(z.string(), z.string())).nullish(), - permission: zPermissionEnum.optional(), - retrieval_model: zRetrievalModel.optional(), + permission: zPermissionEnum.nullish(), + retrieval_model: zRetrievalModel.nullish(), }) /** @@ -1271,8 +1271,8 @@ export const zDocumentTextCreatePayload = z.object({ indexing_technique: z.string().nullish(), name: z.string(), original_document_id: z.string().nullish(), - process_rule: zProcessRule.optional(), - retrieval_model: zRetrievalModel.optional(), + process_rule: zProcessRule.nullish(), + retrieval_model: zRetrievalModel.nullish(), text: z.string(), }) @@ -1283,8 +1283,8 @@ export const zDocumentTextUpdate = z.object({ doc_form: z.string().optional().default('text_model'), doc_language: z.string().optional().default('English'), name: z.string().nullish(), - process_rule: zProcessRule.optional(), - retrieval_model: zRetrievalModel.optional(), + process_rule: zProcessRule.nullish(), + retrieval_model: zRetrievalModel.nullish(), text: z.string().nullish(), }) @@ -1295,7 +1295,7 @@ export const zHitTestingPayload = z.object({ attachment_ids: z.array(z.string()).nullish(), external_retrieval_model: z.record(z.string(), z.unknown()).nullish(), query: z.string().max(250), - retrieval_model: zRetrievalModel.optional(), + retrieval_model: zRetrievalModel.nullish(), }) /** @@ -1317,7 +1317,7 @@ export const zWorkflowLogQuery = z.object({ */ export const zWorkflowRunForLogResponse = z.object({ created_at: z.int().nullish(), - elapsed_time: z.unknown().optional(), + elapsed_time: z.union([z.number(), z.int()]).nullish(), error: z.string().nullish(), exceptions_count: z.int().nullish(), finished_at: z.int().nullish(), @@ -1334,13 +1334,22 @@ export const zWorkflowRunForLogResponse = z.object({ */ export const zWorkflowAppLogPartialResponse = z.object({ created_at: z.int().nullish(), - created_by_account: zSimpleAccount.optional(), - created_by_end_user: zSimpleEndUser.optional(), + created_by_account: zSimpleAccount.nullish(), + created_by_end_user: zSimpleEndUser.nullish(), created_by_role: z.string().nullish(), created_from: z.string().nullish(), - details: z.unknown().optional(), + details: z + .union([ + z.record(z.string(), z.unknown()), + z.array(z.unknown()), + z.string(), + z.int(), + z.number(), + z.boolean(), + ]) + .nullish(), id: z.string(), - workflow_run: zWorkflowRunForLogResponse.optional(), + workflow_run: zWorkflowRunForLogResponse.nullish(), }) /** @@ -1369,11 +1378,20 @@ export const zWorkflowRunPayload = z.object({ */ export const zWorkflowRunResponse = z.object({ created_at: z.int().nullish(), - elapsed_time: z.unknown().optional(), + elapsed_time: z.union([z.number(), z.int()]).nullish(), error: z.string().nullish(), finished_at: z.int().nullish(), id: z.string(), - inputs: z.unknown().optional(), + inputs: z + .union([ + z.record(z.string(), z.unknown()), + z.array(z.unknown()), + z.string(), + z.int(), + z.number(), + z.boolean(), + ]) + .nullish(), outputs: z.record(z.string(), z.unknown()).optional(), status: z.string(), total_steps: z.int().nullish(), @@ -1464,7 +1482,7 @@ export const zDeleteAppsAnnotationsByAnnotationIdPath = z.object({ /** * Annotation deleted successfully */ -export const zDeleteAppsAnnotationsByAnnotationIdResponse = z.record(z.string(), z.never()) +export const zDeleteAppsAnnotationsByAnnotationIdResponse = z.void() export const zPutAppsAnnotationsByAnnotationIdBody = zAnnotationCreatePayload @@ -1535,7 +1553,7 @@ export const zDeleteConversationsByCIdPath = z.object({ /** * Conversation deleted successfully */ -export const zDeleteConversationsByCIdResponse = z.record(z.string(), z.never()) +export const zDeleteConversationsByCIdResponse = z.void() export const zPostConversationsByCIdNameBody = zConversationRenamePayload @@ -1606,7 +1624,7 @@ export const zDeleteDatasetsTagsBody = zTagDeletePayload /** * Tag deleted successfully */ -export const zDeleteDatasetsTagsResponse = z.record(z.string(), z.never()) +export const zDeleteDatasetsTagsResponse = z.void() /** * Tags retrieved successfully @@ -1632,14 +1650,14 @@ export const zPostDatasetsTagsBindingBody = zTagBindingPayload /** * Tags bound successfully */ -export const zPostDatasetsTagsBindingResponse = z.record(z.string(), z.never()) +export const zPostDatasetsTagsBindingResponse = z.void() export const zPostDatasetsTagsUnbindingBody = zTagUnbindingPayload /** * Tags unbound successfully */ -export const zPostDatasetsTagsUnbindingResponse = z.record(z.string(), z.never()) +export const zPostDatasetsTagsUnbindingResponse = z.void() export const zDeleteDatasetsByDatasetIdPath = z.object({ dataset_id: z.string(), @@ -1648,7 +1666,7 @@ export const zDeleteDatasetsByDatasetIdPath = z.object({ /** * Dataset deleted successfully */ -export const zDeleteDatasetsByDatasetIdResponse = z.record(z.string(), z.never()) +export const zDeleteDatasetsByDatasetIdResponse = z.void() export const zGetDatasetsByDatasetIdPath = z.object({ dataset_id: z.string(), @@ -1790,10 +1808,7 @@ export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdPath = z.object({ /** * Document deleted successfully */ -export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdResponse = z.record( - z.string(), - z.never(), -) +export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdResponse = z.void() export const zGetDatasetsByDatasetIdDocumentsByDocumentIdPath = z.object({ dataset_id: z.string(), @@ -1872,10 +1887,7 @@ export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdP /** * Segment deleted successfully */ -export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdResponse = z.record( - z.string(), - z.never(), -) +export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdResponse = z.void() export const zGetDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdPath = z.object({ dataset_id: z.string(), @@ -1952,7 +1964,7 @@ export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdC * Child chunk deleted successfully */ export const zDeleteDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksByChildChunkIdResponse - = z.record(z.string(), z.never()) + = z.void() export const zPatchDatasetsByDatasetIdDocumentsByDocumentIdSegmentsBySegmentIdChildChunksByChildChunkIdBody = zChildChunkUpdatePayload @@ -2088,10 +2100,7 @@ export const zDeleteDatasetsByDatasetIdMetadataByMetadataIdPath = z.object({ /** * Metadata deleted successfully */ -export const zDeleteDatasetsByDatasetIdMetadataByMetadataIdResponse = z.record( - z.string(), - z.never(), -) +export const zDeleteDatasetsByDatasetIdMetadataByMetadataIdResponse = z.void() export const zPatchDatasetsByDatasetIdMetadataByMetadataIdBody = zMetadataUpdatePayload diff --git a/packages/contracts/generated/api/web/types.gen.ts b/packages/contracts/generated/api/web/types.gen.ts index d6adf720558..39c32702192 100644 --- a/packages/contracts/generated/api/web/types.gen.ts +++ b/packages/contracts/generated/api/web/types.gen.ts @@ -539,9 +539,7 @@ export type DeleteConversationsByCIdError = DeleteConversationsByCIdErrors[keyof DeleteConversationsByCIdErrors] export type DeleteConversationsByCIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteConversationsByCIdResponse @@ -1378,9 +1376,7 @@ export type DeleteSavedMessagesByMessageIdError = DeleteSavedMessagesByMessageIdErrors[keyof DeleteSavedMessagesByMessageIdErrors] export type DeleteSavedMessagesByMessageIdResponses = { - 204: { - [key: string]: never - } + 204: void } export type DeleteSavedMessagesByMessageIdResponse diff --git a/packages/contracts/generated/api/web/zod.gen.ts b/packages/contracts/generated/api/web/zod.gen.ts index baf367eb7ae..777237f5f58 100644 --- a/packages/contracts/generated/api/web/zod.gen.ts +++ b/packages/contracts/generated/api/web/zod.gen.ts @@ -213,8 +213,12 @@ export const zLicenseStatus = z.enum(['active', 'expired', 'expiring', 'inactive */ export const zLicenseModel = z.object({ expired_at: z.string().default(''), - status: zLicenseStatus, - workspaces: zLicenseLimitationModel, + status: zLicenseStatus.default('none'), + workspaces: zLicenseLimitationModel.default({ + enabled: false, + limit: 0, + size: 0, + }), }) /** @@ -271,7 +275,7 @@ export const zPluginInstallationScope = z.enum([ * PluginInstallationPermissionModel */ export const zPluginInstallationPermissionModel = z.object({ - plugin_installation_scope: zPluginInstallationScope, + plugin_installation_scope: zPluginInstallationScope.default('all'), restrict_to_marketplace_only: z.boolean().default(false), }) @@ -375,14 +379,20 @@ export const zWebAppAuthModel = z.object({ allow_email_password_login: z.boolean().default(false), allow_sso: z.boolean().default(false), enabled: z.boolean().default(false), - sso_config: zWebAppAuthSsoModel, + sso_config: zWebAppAuthSsoModel.default({ protocol: '' }), }) /** * SystemFeatureModel */ export const zSystemFeatureModel = z.object({ - branding: zBrandingModel, + branding: zBrandingModel.default({ + application_title: '', + enabled: false, + favicon: '', + login_page_logo: '', + workspace_logo: '', + }), enable_change_email: z.boolean().default(true), enable_collaboration_mode: z.boolean().default(true), enable_creators_platform: z.boolean().default(false), @@ -395,13 +405,30 @@ export const zSystemFeatureModel = z.object({ is_allow_create_workspace: z.boolean().default(false), is_allow_register: z.boolean().default(false), is_email_setup: z.boolean().default(false), - license: zLicenseModel, + license: zLicenseModel.default({ + expired_at: '', + status: 'none', + workspaces: { + enabled: false, + limit: 0, + size: 0, + }, + }), max_plugin_package_size: z.int().default(15728640), - plugin_installation_permission: zPluginInstallationPermissionModel, - plugin_manager: zPluginManagerModel, + plugin_installation_permission: zPluginInstallationPermissionModel.default({ + plugin_installation_scope: 'all', + restrict_to_marketplace_only: false, + }), + plugin_manager: zPluginManagerModel.default({ enabled: false }), sso_enforced_for_signin: z.boolean().default(false), sso_enforced_for_signin_protocol: z.string().default(''), - webapp_auth: zWebAppAuthModel, + webapp_auth: zWebAppAuthModel.default({ + allow_email_code_login: false, + allow_email_password_login: false, + allow_sso: false, + enabled: false, + sso_config: { protocol: '' }, + }), }) /** @@ -471,7 +498,7 @@ export const zDeleteConversationsByCIdPath = z.object({ /** * Conversation deleted successfully */ -export const zDeleteConversationsByCIdResponse = z.record(z.string(), z.never()) +export const zDeleteConversationsByCIdResponse = z.void() export const zPostConversationsByCIdNamePath = z.object({ c_id: z.string(), @@ -696,7 +723,7 @@ export const zDeleteSavedMessagesByMessageIdPath = z.object({ /** * Message removed successfully */ -export const zDeleteSavedMessagesByMessageIdResponse = z.record(z.string(), z.never()) +export const zDeleteSavedMessagesByMessageIdResponse = z.void() /** * Success diff --git a/packages/contracts/openapi-ts.api.config.ts b/packages/contracts/openapi-ts.api.config.ts index 445be5f06ed..fc6ff748111 100644 --- a/packages/contracts/openapi-ts.api.config.ts +++ b/packages/contracts/openapi-ts.api.config.ts @@ -7,23 +7,31 @@ import { $, defineConfig } from '@hey-api/openapi-ts' type JsonObject = Record type SwaggerSchema = JsonObject & { - '$defs'?: Record - '$ref'?: string - 'x-nullable'?: boolean - 'additionalProperties'?: unknown - 'allOf'?: SwaggerSchema[] - 'anyOf'?: SwaggerSchema[] - 'const'?: unknown - 'default'?: unknown - 'definitions'?: Record - 'description'?: string - 'enum'?: unknown[] - 'format'?: string - 'items'?: SwaggerSchema - 'oneOf'?: SwaggerSchema[] - 'properties'?: Record - 'required'?: string[] - 'type'?: string + $ref?: string + additionalProperties?: unknown + allOf?: SwaggerSchema[] + anyOf?: SwaggerSchema[] + description?: string + enum?: unknown[] + items?: SwaggerSchema + oneOf?: SwaggerSchema[] + properties?: Record + required?: string[] + type?: string +} + +type OpenApiMediaType = JsonObject & { + schema?: SwaggerSchema +} + +type OpenApiRequestBody = JsonObject & { + content?: Record + description?: string + required?: boolean +} + +type OpenApiComponents = JsonObject & { + schemas?: Record } type SwaggerParameter = JsonObject & { @@ -31,12 +39,11 @@ type SwaggerParameter = JsonObject & { name?: string required?: boolean schema?: SwaggerSchema - type?: string } type SwaggerResponse = JsonObject & { + content?: Record description?: string - schema?: SwaggerSchema } type SwaggerOperation = JsonObject & { @@ -44,11 +51,13 @@ type SwaggerOperation = JsonObject & { description?: string operationId?: string parameters?: SwaggerParameter[] + requestBody?: OpenApiRequestBody | null responses?: Record } type SwaggerDocument = JsonObject & { - definitions?: Record + components?: OpenApiComponents + openapi?: string paths?: Record> } @@ -97,10 +106,10 @@ const requestBodyMethods = new Set(['delete', 'patch', 'post', 'put']) const noBodyResponseStatuses = new Set(['204', '205', '304']) const apiSpecs: ApiSpec[] = [ - { filename: 'console-swagger.json', name: 'console' }, - { filename: 'web-swagger.json', name: 'web' }, - { filename: 'service-swagger.json', name: 'service' }, - { filename: 'openapi-swagger.json', name: 'openapi' }, + { filename: 'console-openapi.json', name: 'console' }, + { filename: 'web-openapi.json', name: 'web' }, + { filename: 'service-openapi.json', name: 'service' }, + { filename: 'openapi-openapi.json', name: 'openapi' }, ] const inaccurateGeneratedContractDescription = 'Generated contract types may be inaccurate because backend OpenAPI annotations are incomplete. Do not migrate callers until the generated contract is accurate.' @@ -115,13 +124,6 @@ const unknownObjectSchema = (): SwaggerSchema => ({ type: 'object', }) -const noContentSchema = (): SwaggerSchema => ({ - // Hey API's Swagger 2.0 pipeline currently needs a response schema symbol even for no-content responses. - additionalProperties: false, - properties: {}, - type: 'object', -}) - const toWords = (value: string) => { return value .replace(/[{}]/g, '') @@ -190,6 +192,48 @@ const clone = (value: T): T => { return JSON.parse(JSON.stringify(value)) as T } +const componentSchemaRefPrefix = '#/components/schemas/' + +const schemaNameFromRef = (ref: string) => { + if (ref.startsWith(componentSchemaRefPrefix)) + return ref.slice(componentSchemaRefPrefix.length) + return undefined +} + +const getDocumentSchemas = (document: SwaggerDocument) => { + const components = document.components ??= {} + return components.schemas ??= {} +} + +const firstContentSchema = ( + content: Record | undefined, + preferredMediaTypes: string[], +) => { + if (!isObject(content)) + return undefined + + for (const mediaType of preferredMediaTypes) { + const media = content[mediaType] + if (isObject(media?.schema)) + return media.schema + } + + for (const media of Object.values(content)) { + if (isObject(media?.schema)) + return media.schema + } + + return undefined +} + +const getRequestBodySchema = (operation: SwaggerOperation) => { + return firstContentSchema(operation.requestBody?.content, ['application/json']) +} + +const getResponseSchema = (response: SwaggerResponse) => { + return firstContentSchema(response.content, ['application/json']) +} + const apiOperationKey = (surface: string, method: string, routePath: string) => { return `${surface}:${method.toLowerCase()}:${routePath}` } @@ -358,7 +402,7 @@ const collectRuntimeBodyOperationKeys = () => { const runtimeBodyOperationKeys = collectRuntimeBodyOperationKeys() -const collectDefinitionRefs = (value: unknown, refs: Set, visited = new WeakSet()) => { +const collectSchemaRefs = (value: unknown, refs: Set, visited = new WeakSet()) => { if (!value || typeof value !== 'object') return @@ -368,120 +412,38 @@ const collectDefinitionRefs = (value: unknown, refs: Set, visited = new visited.add(value) if (Array.isArray(value)) { - value.forEach(item => collectDefinitionRefs(item, refs, visited)) + value.forEach(item => collectSchemaRefs(item, refs, visited)) return } const objectValue = value as JsonObject const ref = objectValue.$ref - if (typeof ref === 'string' && ref.startsWith('#/definitions/')) - refs.add(ref.slice('#/definitions/'.length)) - - Object.values(objectValue).forEach(item => collectDefinitionRefs(item, refs, visited)) -} - -const removeNullDefaults = (value: unknown, visited = new WeakSet()) => { - if (!value || typeof value !== 'object' || visited.has(value)) - return - - visited.add(value) - - if (Array.isArray(value)) { - value.forEach(item => removeNullDefaults(item, visited)) - return + if (typeof ref === 'string') { + const refName = schemaNameFromRef(ref) + if (refName) + refs.add(refName) } - const schema = value as SwaggerSchema - if (schema.default === null) - delete schema.default - - Object.values(schema).forEach(item => removeNullDefaults(item, visited)) + Object.values(objectValue).forEach(item => collectSchemaRefs(item, refs, visited)) } const isNullSchema = (schema: SwaggerSchema) => { return schema.type === 'null' } -const normalizeNullableAnyOf = (value: unknown, visited = new WeakSet()) => { - if (!value || typeof value !== 'object' || visited.has(value)) - return - - visited.add(value) - - if (Array.isArray(value)) { - value.forEach(item => normalizeNullableAnyOf(item, visited)) - return - } - - const schema = value as SwaggerSchema - - if (Array.isArray(schema.anyOf)) { - const nonNullSchemas = schema.anyOf.filter(item => !isNullSchema(item)) - const hasNullSchema = nonNullSchemas.length !== schema.anyOf.length - - if (hasNullSchema && nonNullSchemas.length === 1) { - const { anyOf: _anyOf, ...rest } = schema - Object.keys(schema).forEach(key => delete schema[key]) - Object.assign(schema, rest, nonNullSchemas[0], { 'x-nullable': true }) - } - } - - Object.values(schema).forEach(item => normalizeNullableAnyOf(item, visited)) -} - -const hoistNestedDefinitions = (definitions: Record) => { - const visited = new WeakSet() - - const visit = (value: unknown) => { - if (!value || typeof value !== 'object' || visited.has(value)) - return - - visited.add(value) - - if (Array.isArray(value)) { - value.forEach(visit) - return - } - - const schema = value as SwaggerSchema - for (const key of ['$defs', 'definitions'] as const) { - const nestedDefinitions = schema[key] - if (!isObject(nestedDefinitions)) - continue - - for (const [name, nestedSchema] of Object.entries(nestedDefinitions)) { - definitions[name] ??= nestedSchema - visit(nestedSchema) - } - - delete schema[key] - } - - Object.values(schema).forEach(visit) - } - - Object.values(definitions).forEach(visit) -} - -const ensureReferencedDefinitions = (document: SwaggerDocument) => { - const definitions = document.definitions ??= {} - const refs = new Set() - collectDefinitionRefs(document, refs) - - for (const refName of refs) - definitions[refName] ??= unknownObjectSchema() -} - -const resolveDefinitionRef = ( +const resolveSchemaRef = ( schema: SwaggerSchema | undefined, - definitions: Record, + schemas: Record, ): SwaggerSchema | undefined => { const ref = schema?.$ref - - if (!ref?.startsWith('#/definitions/')) + if (!ref) return schema - return definitions[ref.slice('#/definitions/'.length)] ?? schema + const refName = schemaNameFromRef(ref) + if (!refName) + return schema + + return schemas[refName] ?? schema } const withoutNullableWrapper = (schema: SwaggerSchema | undefined): SwaggerSchema => { @@ -499,22 +461,6 @@ const withoutNullableWrapper = (schema: SwaggerSchema | undefined): SwaggerSchem } } -const isNullEnumItem = (item: unknown) => { - return isObject(item) && (item.type === 'null' || item.const === null) -} - -const markNullableEnumSchema = (ctx: { schema: JsonObject }): undefined => { - const items = ctx.schema.items - - if (ctx.schema['x-nullable'] !== true || !Array.isArray(items) || items.some(isNullEnumItem)) - return undefined - - // Hey API's enum visitors infer nullable from a null enum item, not x-nullable. - ctx.schema.items = [...items, { const: null, type: 'null' }] - - return undefined -} - const queryParameterFromSchema = ( name: string, schema: SwaggerSchema | undefined, @@ -525,45 +471,12 @@ const queryParameterFromSchema = ( in: 'query', name, required, + schema: querySchema, } - if (querySchema.default !== undefined) - parameter.default = querySchema.default - if (querySchema.description) parameter.description = querySchema.description - if (querySchema.enum) - parameter.enum = querySchema.enum - - if (querySchema.format) - parameter.format = querySchema.format - - if (querySchema.items) - parameter.items = querySchema.items - - for (const key of [ - 'exclusiveMaximum', - 'exclusiveMinimum', - 'maxItems', - 'maxLength', - 'maximum', - 'minItems', - 'minLength', - 'minimum', - 'multipleOf', - 'pattern', - 'uniqueItems', - 'x-nullable', - ]) { - if (querySchema[key] !== undefined) - parameter[key] = querySchema[key] - } - - parameter.type = ['array', 'boolean', 'integer', 'number', 'string'].includes(querySchema.type ?? '') - ? querySchema.type - : 'string' - return parameter } @@ -596,15 +509,12 @@ const mergeQueryParameter = ( const normalizeGetBodyParameters = ( operation: SwaggerOperation, - definitions: Record, + schemas: Record, ) => { - if (!Array.isArray(operation.parameters)) - return - const bodyParameters: SwaggerParameter[] = [] const normalizedParameters: SwaggerParameter[] = [] - for (const parameter of operation.parameters) { + for (const parameter of operation.parameters ?? []) { if (parameter.in === 'body') { bodyParameters.push(parameter) continue @@ -613,8 +523,18 @@ const normalizeGetBodyParameters = ( normalizedParameters.push(parameter) } + const requestBodySchema = getRequestBodySchema(operation) + if (requestBodySchema) { + bodyParameters.push({ + in: 'body', + name: 'payload', + required: Boolean(operation.requestBody?.required), + schema: requestBodySchema, + }) + } + for (const parameter of bodyParameters) { - const schema = resolveDefinitionRef(parameter.schema, definitions) + const schema = resolveSchemaRef(parameter.schema, schemas) const properties = schema?.properties ?? {} const required = new Set(schema?.required ?? []) @@ -627,6 +547,7 @@ const normalizeGetBodyParameters = ( } operation.parameters = normalizedParameters + delete operation.requestBody } const normalizeResponses = (operation: SwaggerOperation) => { @@ -634,18 +555,26 @@ const normalizeResponses = (operation: SwaggerOperation) => { for (const [status, response] of Object.entries(responses)) { if (noBodyResponseStatuses.has(status)) { - response.schema = noContentSchema() + delete response.content continue } - if (!response.schema) - response.schema = unknownObjectSchema() + const schema = getResponseSchema(response) ?? unknownObjectSchema() + response.content = { + 'application/json': { + schema, + }, + } } if (!Object.keys(responses).some(status => /^2\d\d$/.test(status))) { responses['200'] = { + content: { + 'application/json': { + schema: unknownObjectSchema(), + }, + }, description: 'Success', - schema: unknownObjectSchema(), } } } @@ -678,7 +607,7 @@ const isLooseObjectSchema = (schema: SwaggerSchema) => { const hasLooseSchema = ( schema: SwaggerSchema | undefined, - definitions: Record, + schemas: Record, visitedRefs = new Set(), ): boolean => { if (!schema) @@ -688,37 +617,40 @@ const hasLooseSchema = ( return false const ref = schema?.$ref - if (ref?.startsWith('#/definitions/')) { - const refName = ref.slice('#/definitions/'.length) + if (ref) { + const refName = schemaNameFromRef(ref) + if (!refName) + return false + if (visitedRefs.has(refName)) return false - return hasLooseSchema(definitions[refName], definitions, new Set([...visitedRefs, refName])) + return hasLooseSchema(schemas[refName], schemas, new Set([...visitedRefs, refName])) } const normalizedSchema = withoutNullableWrapper(schema) for (const variants of [normalizedSchema.allOf, normalizedSchema.anyOf, normalizedSchema.oneOf]) { - if (Array.isArray(variants) && variants.some(item => !isNullSchema(item) && hasLooseSchema(item, definitions, visitedRefs))) + if (Array.isArray(variants) && variants.some(item => !isNullSchema(item) && hasLooseSchema(item, schemas, visitedRefs))) return true } if (normalizedSchema.type === 'array') - return hasLooseSchema(normalizedSchema.items, definitions, visitedRefs) + return hasLooseSchema(normalizedSchema.items, schemas, visitedRefs) if (isLooseObjectSchema(normalizedSchema)) return true - if (isObject(normalizedSchema.additionalProperties) && hasLooseSchema(normalizedSchema.additionalProperties, definitions, visitedRefs)) + if (isObject(normalizedSchema.additionalProperties) && hasLooseSchema(normalizedSchema.additionalProperties, schemas, visitedRefs)) return true return Object.values(normalizedSchema.properties ?? {}) - .some(property => hasLooseSchema(property, definitions, visitedRefs)) + .some(property => hasLooseSchema(property, schemas, visitedRefs)) } const hasPossiblyInaccurateGeneratedContractTypes = ( operation: SwaggerOperation, - definitions: Record, + schemas: Record, context: ApiOperationContext, ) => { const successResponses = Object.entries(operation.responses ?? {}) @@ -728,15 +660,17 @@ const hasPossiblyInaccurateGeneratedContractTypes = ( return true const successResponsesWithBody = successResponses.filter(([status]) => !noBodyResponseStatuses.has(status)) - if (successResponsesWithBody.some(([, response]) => hasLooseSchema(response.schema, definitions))) + if (successResponsesWithBody.some(([, response]) => hasLooseSchema(getResponseSchema(response), schemas))) return true - if (context.runtimeBodyRequired && !operation.parameters?.some(parameter => parameter.in === 'body')) + const requestBodySchema = getRequestBodySchema(operation) + const legacyBodyParameter = operation.parameters?.find(parameter => parameter.in === 'body') + + if (context.runtimeBodyRequired && !requestBodySchema && !legacyBodyParameter) return true - return operation.parameters?.some((parameter) => { - return parameter.in === 'body' && hasLooseSchema(parameter.schema, definitions) - }) ?? false + const bodySchema = requestBodySchema ?? legacyBodyParameter?.schema + return bodySchema ? hasLooseSchema(bodySchema, schemas) : false } const appendOperationDescription = (operation: SwaggerOperation, description: string) => { @@ -766,7 +700,7 @@ const formatPercent = (ready: number, total: number) => { } const normalizeOperations = (document: SwaggerDocument, surface: string) => { - const definitions = document.definitions ??= {} + const schemas = getDocumentSchemas(document) for (const [routePath, pathItem] of Object.entries(document.paths ?? {})) { for (const [method, operation] of Object.entries(pathItem)) { @@ -776,17 +710,16 @@ const normalizeOperations = (document: SwaggerDocument, surface: string) => { const swaggerOperation = operation as SwaggerOperation swaggerOperation.operationId = operationId(method, routePath) + if (method === 'get') + normalizeGetBodyParameters(swaggerOperation, schemas) normalizeResponses(swaggerOperation) - const hasPossiblyInaccurateTypes = hasPossiblyInaccurateGeneratedContractTypes(swaggerOperation, definitions, { + const hasPossiblyInaccurateTypes = hasPossiblyInaccurateGeneratedContractTypes(swaggerOperation, schemas, { method, routePath, runtimeBodyRequired: runtimeBodyOperationKeys.has(apiOperationKey(surface, method, routePath)), }) recordApiReadiness(surface, !hasPossiblyInaccurateTypes) - if (method === 'get') - normalizeGetBodyParameters(swaggerOperation, definitions) - if (hasPossiblyInaccurateTypes) markPossiblyInaccurateGeneratedContract(swaggerOperation) } @@ -794,14 +727,6 @@ const normalizeOperations = (document: SwaggerDocument, surface: string) => { } const normalizeApiSwagger = (document: SwaggerDocument, surface: string) => { - document.definitions ??= {} - - // Flask-RESTX emits Pydantic nested $defs inside individual schemas while - // refs point at the root Swagger 2.0 definitions object. - hoistNestedDefinitions(document.definitions) - ensureReferencedDefinitions(document) - normalizeNullableAnyOf(document) - removeNullDefaults(document) normalizeOperations(document, surface) return document @@ -836,13 +761,13 @@ const topLevelPathSegment = (routePath: string) => { return routePath.split('/').filter(Boolean)[0] ?? 'root' } -const selectReferencedDefinitions = ( - definitions: Record, +const selectReferencedSchemas = ( + schemas: Record, paths: Record>, ) => { - const selectedDefinitions: Record = {} + const selectedSchemas: Record = {} const pendingRefs = new Set() - collectDefinitionRefs(paths, pendingRefs) + collectSchemaRefs(paths, pendingRefs) while (pendingRefs.size > 0) { const refName = pendingRefs.values().next().value @@ -851,32 +776,36 @@ const selectReferencedDefinitions = ( pendingRefs.delete(refName) - if (selectedDefinitions[refName]) + if (selectedSchemas[refName]) continue - selectedDefinitions[refName] = definitions[refName] ?? unknownObjectSchema() + selectedSchemas[refName] = schemas[refName] ?? unknownObjectSchema() const nestedRefs = new Set() - collectDefinitionRefs(selectedDefinitions[refName], nestedRefs) + collectSchemaRefs(selectedSchemas[refName], nestedRefs) for (const nestedRef of nestedRefs) { - if (!selectedDefinitions[nestedRef]) + if (!selectedSchemas[nestedRef]) pendingRefs.add(nestedRef) } } - return selectedDefinitions + return selectedSchemas } const cloneDocumentWithPaths = ( document: SwaggerDocument, paths: Record>, ) => { - const { definitions: _definitions, paths: _paths, ...metadata } = document + const { components: _components, paths: _paths, ...metadata } = document const clonedPaths = clone(paths) + const components = clone(document.components ?? {}) + const sourceSchemas = getDocumentSchemas(document) + + components.schemas = selectReferencedSchemas(sourceSchemas, clonedPaths) return { ...clone(metadata), - definitions: selectReferencedDefinitions(document.definitions ?? {}, clonedPaths), + components, paths: clonedPaths, } satisfies SwaggerDocument } @@ -977,16 +906,12 @@ const createApiConfig = (job: ApiJob): UserConfig => ({ }, plugins: job.plugins ?? [ { - 'comments': false, - 'name': '@hey-api/typescript', - '~resolvers': { - enum: markNullableEnumSchema, - }, + comments: false, + name: '@hey-api/typescript', }, { 'name': 'zod', '~resolvers': { - enum: markNullableEnumSchema, string: (ctx) => { if (ctx.schema.format !== 'binary') return undefined From 1f6b7a3c350a9efe693208d65d6b2ff877dd5a48 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:08:40 +0800 Subject: [PATCH 046/122] docs(dify-ui): document scroll area content width (#37376) --- .../dify-ui/src/scroll-area/index.stories.tsx | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/dify-ui/src/scroll-area/index.stories.tsx b/packages/dify-ui/src/scroll-area/index.stories.tsx index bd3a1cef162..28c00bea2a7 100644 --- a/packages/dify-ui/src/scroll-area/index.stories.tsx +++ b/packages/dify-ui/src/scroll-area/index.stories.tsx @@ -17,7 +17,7 @@ const meta = { layout: 'padded', docs: { description: { - component: 'Compound scroll container built on Base UI Scroll Area. The examples mirror the upstream anatomy and focus patterns while applying Dify UI tokens, panel surfaces, and scrollbar spacing.', + component: 'Compound scroll container built on Base UI Scroll Area. The examples mirror the upstream anatomy and focus patterns while applying Dify UI tokens, panel surfaces, and scrollbar spacing. Base UI ScrollArea.Content defaults to min-width: fit-content, so vertical-only regions that should truncate long content must set min-width: 0 on the content slot.', }, }, }, @@ -70,6 +70,19 @@ const articleParagraphs = [ 'A scroll area follows the same principle in an interface. The viewport owns scrolling, the content stays inside its measured width, and the scrollbar remains a visual affordance rather than a second layout system.', ] as const +const fileRows = [ + 'agent-roster-skill-detail-dialog-preview-image.png', + 'workflow-agent-binding-source-of-truth-summary.md', + 'very-long-file-name-that-should-truncate-inside-a-vertical-scroll-area-without-creating-horizontal-scroll.json', + 'runtime-output-schema.ts', + 'knowledge-retrieval-notes.md', + 'composer-draft-original-state-diffing-notes.md', + 'generated-contract-console-query-options.ts', + 'agent-v2-workflow-node-config-schema.json', + 'selected-file-highlight-behavior.spec.tsx', + 'scroll-area-content-min-width-regression.md', +] as const + const gridCells = Array.from({ length: 100 }, (_, index) => index + 1) function StorySection({ @@ -176,6 +189,39 @@ export const Vertical: Story = { ), } +export const VerticalTruncation: Story = { + render: () => ( + +
+ + + + {fileRows.map(file => ( +
+ + + {file} + +
+ ))} +
+
+ + + +
+
+
+ ), +} + export const ScrollFade: Story = { render: () => ( Date: Fri, 12 Jun 2026 16:14:29 +0800 Subject: [PATCH 047/122] fix(ui): align infotip popover focus styles (#37377) --- packages/dify-ui/src/popover/__tests__/index.spec.tsx | 5 +++++ packages/dify-ui/src/popover/index.tsx | 1 + web/app/components/base/infotip/index.tsx | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/dify-ui/src/popover/__tests__/index.spec.tsx b/packages/dify-ui/src/popover/__tests__/index.spec.tsx index ba368c9ad37..64b3d76122b 100644 --- a/packages/dify-ui/src/popover/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/popover/__tests__/index.spec.tsx @@ -29,6 +29,11 @@ describe('PopoverContent', () => { await expect.element(screen.getByRole('group', { name: 'default positioner' })).toHaveAttribute('data-side', 'bottom') await expect.element(screen.getByRole('group', { name: 'default positioner' })).toHaveAttribute('data-align', 'center') await expect.element(screen.getByRole('dialog', { name: 'default popover' })).toHaveTextContent('Default content') + await expect.element(screen.getByRole('dialog', { name: 'default popover' })).toHaveClass( + 'outline-hidden', + 'focus:outline-hidden', + 'focus-visible:outline-hidden', + ) }) it('should apply parsed custom placement and custom offsets when placement props are provided', async () => { diff --git a/packages/dify-ui/src/popover/index.tsx b/packages/dify-ui/src/popover/index.tsx index c29c50d1fe1..aad0bde3d4a 100644 --- a/packages/dify-ui/src/popover/index.tsx +++ b/packages/dify-ui/src/popover/index.tsx @@ -57,6 +57,7 @@ export function PopoverContent({ From 5e8c182970d9fb044a81f2960870b39220913d30 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:20:29 +0800 Subject: [PATCH 048/122] fix(agent-v2): filter workflow invite options (#37368) Co-authored-by: Yansong Zhang <916125788@qq.com> --- ...d1a80_add_agent_active_config_has_model.py | 40 +++++ api/models/agent.py | 11 ++ api/services/agent/agent_soul_state.py | 6 + api/services/agent/composer_service.py | 7 + api/services/agent/roster_service.py | 48 +++++- .../services/agent/test_agent_services.py | 139 +++++++++++++++++- 6 files changed, 235 insertions(+), 16 deletions(-) create mode 100644 api/migrations/versions/2026_06_12_1600-9f4b7c2d1a80_add_agent_active_config_has_model.py create mode 100644 api/services/agent/agent_soul_state.py diff --git a/api/migrations/versions/2026_06_12_1600-9f4b7c2d1a80_add_agent_active_config_has_model.py b/api/migrations/versions/2026_06_12_1600-9f4b7c2d1a80_add_agent_active_config_has_model.py new file mode 100644 index 00000000000..6fbce0591e8 --- /dev/null +++ b/api/migrations/versions/2026_06_12_1600-9f4b7c2d1a80_add_agent_active_config_has_model.py @@ -0,0 +1,40 @@ +"""add agent active config has model + +Revision ID: 9f4b7c2d1a80 +Revises: 0b2f2c8a9d1e +Create Date: 2026-06-12 16:00:00.000000 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "9f4b7c2d1a80" +down_revision = "0b2f2c8a9d1e" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("agents", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "active_config_has_model", + sa.Boolean(), + server_default=sa.text("false"), + nullable=False, + ) + ) + + op.create_index( + "agent_tenant_invitable_idx", + "agents", + ["tenant_id", "scope", "status", "active_config_has_model", "updated_at"], + ) + + +def downgrade(): + op.drop_index("agent_tenant_invitable_idx", table_name="agents") + with op.batch_alter_table("agents", schema=None) as batch_op: + batch_op.drop_column("active_config_has_model") diff --git a/api/models/agent.py b/api/models/agent.py index 8487bc18962..669bcff6771 100644 --- a/api/models/agent.py +++ b/api/models/agent.py @@ -131,6 +131,14 @@ class Agent(DefaultFieldsMixin, Base): Index("agent_tenant_workflow_id_idx", "tenant_id", "workflow_id"), Index("agent_tenant_app_id_idx", "tenant_id", "app_id"), Index("agent_active_config_snapshot_id_idx", "active_config_snapshot_id"), + Index( + "agent_tenant_invitable_idx", + "tenant_id", + "scope", + "status", + "active_config_has_model", + "updated_at", + ), ) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) @@ -153,6 +161,9 @@ class Agent(DefaultFieldsMixin, Base): workflow_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True) workflow_node_id: Mapped[str | None] = mapped_column(String(255), nullable=True) active_config_snapshot_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True) + active_config_has_model: Mapped[bool] = mapped_column( + sa.Boolean, nullable=False, default=False, server_default=sa.text("false") + ) status: Mapped[AgentStatus] = mapped_column( EnumText(AgentStatus, length=32), nullable=False, default=AgentStatus.ACTIVE ) diff --git a/api/services/agent/agent_soul_state.py b/api/services/agent/agent_soul_state.py new file mode 100644 index 00000000000..dfc0a0335e4 --- /dev/null +++ b/api/services/agent/agent_soul_state.py @@ -0,0 +1,6 @@ +from models.agent_config_entities import AgentSoulConfig + + +def agent_soul_has_model(agent_soul: AgentSoulConfig) -> bool: + """Return whether the Agent Soul has the minimum model config required for runtime.""" + return agent_soul.model is not None diff --git a/api/services/agent/composer_service.py b/api/services/agent/composer_service.py index a2d73929035..b29fad37221 100644 --- a/api/services/agent/composer_service.py +++ b/api/services/agent/composer_service.py @@ -26,6 +26,7 @@ from models.agent_config_entities import ( effective_declared_outputs as _effective_declared_outputs, ) from models.workflow import Workflow +from services.agent.agent_soul_state import agent_soul_has_model from services.agent.composer_validator import ComposerConfigValidator from services.agent.errors import AgentNameConflictError, AgentNotFoundError, AgentVersionNotFoundError from services.entities.agent_entities import ( @@ -229,6 +230,7 @@ class AgentComposerService: version_note=payload.version_note, ) agent.active_config_snapshot_id = version.id + agent.active_config_has_model = agent_soul_has_model(payload.agent_soul) else: current_snapshot = cls._require_version( tenant_id=tenant_id, agent_id=agent.id, version_id=agent.active_config_snapshot_id @@ -241,6 +243,7 @@ class AgentComposerService: version_note=payload.version_note, ) agent.active_config_snapshot_id = version.id + agent.active_config_has_model = agent_soul_has_model(payload.agent_soul) agent.updated_by = account_id db.session.commit() @@ -605,6 +608,7 @@ class AgentComposerService: ) agent = cls._require_agent(tenant_id=tenant_id, agent_id=binding.agent_id) agent.active_config_snapshot_id = version.id + agent.active_config_has_model = agent_soul_has_model(payload.agent_soul) agent.updated_by = account_id binding.current_snapshot_id = version.id if payload.node_job is not None: @@ -634,6 +638,7 @@ class AgentComposerService: ) agent = cls._require_agent(tenant_id=tenant_id, agent_id=binding.agent_id) agent.active_config_snapshot_id = version.id + agent.active_config_has_model = agent_soul_has_model(payload.agent_soul) agent.updated_by = account_id binding.current_snapshot_id = version.id binding.updated_by = account_id @@ -753,6 +758,7 @@ class AgentComposerService: version_note=None, ) agent.active_config_snapshot_id = version.id + agent.active_config_has_model = agent_soul_has_model(agent_soul) return agent @classmethod @@ -792,6 +798,7 @@ class AgentComposerService: version_note=version_note, ) agent.active_config_snapshot_id = version.id + agent.active_config_has_model = agent_soul_has_model(agent_soul) return agent @classmethod diff --git a/api/services/agent/roster_service.py b/api/services/agent/roster_service.py index ab57e22268a..e54a0e9128e 100644 --- a/api/services/agent/roster_service.py +++ b/api/services/agent/roster_service.py @@ -21,6 +21,7 @@ from models.agent_config_entities import AgentSoulConfig from models.enums import AppStatus from models.model import App from models.workflow import Workflow +from services.agent.agent_soul_state import agent_soul_has_model from services.agent.composer_validator import ComposerConfigValidator from services.agent.errors import ( AgentArchivedError, @@ -95,9 +96,8 @@ class AgentRosterService: "created_at": to_timestamp(version.created_at), } - def list_roster_agents( - self, *, tenant_id: str, page: int = 1, limit: int = 20, keyword: str | None = None - ) -> dict[str, Any]: + @staticmethod + def _build_roster_agents_stmt(*, tenant_id: str, keyword: str | None = None): stmt = select(Agent).where( Agent.tenant_id == tenant_id, Agent.scope == AgentScope.ROSTER, @@ -108,7 +108,12 @@ class AgentRosterService: escaped_keyword = escape_like_pattern(keyword) stmt = stmt.where(Agent.name.ilike(f"%{escaped_keyword}%", escape="\\")) - stmt = stmt.order_by(Agent.updated_at.desc()) + return stmt.order_by(Agent.updated_at.desc()) + + def list_roster_agents( + self, *, tenant_id: str, page: int = 1, limit: int = 20, keyword: str | None = None + ) -> dict[str, Any]: + stmt = self._build_roster_agents_stmt(tenant_id=tenant_id, keyword=keyword) total = self._session.scalar(select(func.count()).select_from(stmt.subquery())) or 0 agents = list(self._session.scalars(stmt.offset((page - 1) * limit).limit(limit)).all()) @@ -144,7 +149,26 @@ class AgentRosterService: def list_invite_options( self, *, tenant_id: str, page: int = 1, limit: int = 20, keyword: str | None = None, app_id: str | None = None ) -> dict[str, Any]: - result = self.list_roster_agents(tenant_id=tenant_id, page=page, limit=limit, keyword=keyword) + stmt = self._build_roster_agents_stmt(tenant_id=tenant_id, keyword=keyword).where( + Agent.active_config_has_model.is_(True) + ) + total = self._session.scalar(select(func.count()).select_from(stmt.subquery())) or 0 + agents = list(self._session.scalars(stmt.offset((page - 1) * limit).limit(limit)).all()) + versions_by_id = self._load_versions_by_id( + [agent.active_config_snapshot_id for agent in agents if agent.active_config_snapshot_id] + ) + published_references_by_agent_id = self._load_published_references_by_agent_id( + tenant_id=tenant_id, + agent_ids=[agent.id for agent in agents], + ) + data = [ + self.serialize_agent( + agent, + versions_by_id.get(agent.active_config_snapshot_id) if agent.active_config_snapshot_id else None, + published_references_by_agent_id.get(agent.id, []), + ) + for agent in agents + ] usage_by_agent_id: dict[str, list[str]] = {} if app_id: draft_workflow = self._session.scalar( @@ -157,7 +181,7 @@ class AgentRosterService: .limit(1) ) if draft_workflow: - agent_ids = [item["id"] for item in result["data"]] + agent_ids = [item["id"] for item in data] if agent_ids: bindings = self._session.scalars( select(WorkflowAgentNodeBinding).where( @@ -170,12 +194,18 @@ class AgentRosterService: if binding.agent_id: usage_by_agent_id.setdefault(binding.agent_id, []).append(binding.node_id) - for item in result["data"]: + for item in data: existing_node_ids = usage_by_agent_id.get(item["id"], []) item["is_in_current_workflow"] = bool(existing_node_ids) item["in_current_workflow_count"] = len(existing_node_ids) item["existing_node_ids"] = existing_node_ids - return result + return { + "data": data, + "page": page, + "limit": limit, + "total": total, + "has_more": page * limit < total, + } def create_roster_agent( self, @@ -231,6 +261,7 @@ class AgentRosterService: ) self._session.add(revision) agent.active_config_snapshot_id = version.id + agent.active_config_has_model = agent_soul_has_model(payload.agent_soul) try: self._session.commit() @@ -302,6 +333,7 @@ class AgentRosterService: ) self._session.add(revision) agent.active_config_snapshot_id = version.id + agent.active_config_has_model = agent_soul_has_model(AgentSoulConfig()) self._session.flush() return agent diff --git a/api/tests/unit_tests/services/agent/test_agent_services.py b/api/tests/unit_tests/services/agent/test_agent_services.py index f7bcde44f50..b29aeccc8a0 100644 --- a/api/tests/unit_tests/services/agent/test_agent_services.py +++ b/api/tests/unit_tests/services/agent/test_agent_services.py @@ -17,6 +17,7 @@ from models.agent import ( from models.agent_config_entities import WorkflowNodeJobConfig from models.workflow import Workflow from services.agent import composer_service, roster_service +from services.agent.agent_soul_state import agent_soul_has_model from services.agent.composer_service import AgentComposerService from services.agent.composer_validator import ComposerConfigValidator from services.agent.errors import InvalidComposerConfigError @@ -72,6 +73,23 @@ class FakeSession: self.rollbacks += 1 +def _agent_soul_with_model() -> AgentSoulConfig: + return AgentSoulConfig.model_validate( + { + "model": { + "plugin_id": "langgenius/openai/openai", + "model_provider": "openai", + "model": "gpt-4o", + } + } + ) + + +def test_agent_soul_has_model(): + assert agent_soul_has_model(_agent_soul_with_model()) is True + assert agent_soul_has_model(AgentSoulConfig()) is False + + def test_load_workflow_composer_returns_empty_state(monkeypatch): monkeypatch.setattr(AgentComposerService, "_get_draft_workflow", lambda **kwargs: SimpleNamespace(id="workflow-1")) monkeypatch.setattr(AgentComposerService, "_get_workflow_binding", lambda **kwargs: None) @@ -217,13 +235,13 @@ def test_save_agent_app_composer_creates_agent_when_missing(monkeypatch): assert result == {"loaded": True} assert fake_session.added[0].name == "Analyst" assert fake_session.added[0].active_config_snapshot_id == "version-1" + assert fake_session.added[0].active_config_has_model is False assert fake_session.commits == 1 def test_save_agent_app_composer_updates_current_version(monkeypatch): - fake_session = FakeSession( - scalar=[SimpleNamespace(id="agent-1", active_config_snapshot_id="version-1", updated_by=None)] - ) + agent = SimpleNamespace(id="agent-1", active_config_snapshot_id="version-1", updated_by=None) + fake_session = FakeSession(scalar=[agent]) updated = {} monkeypatch.setattr(composer_service.db, "session", fake_session) @@ -239,7 +257,7 @@ def test_save_agent_app_composer_updates_current_version(monkeypatch): { "variant": ComposerVariant.AGENT_APP.value, "save_strategy": ComposerSaveStrategy.SAVE_TO_CURRENT_VERSION.value, - "agent_soul": {"prompt": {"system_prompt": "updated"}}, + "agent_soul": _agent_soul_with_model().model_dump(mode="json"), } ) @@ -250,6 +268,7 @@ def test_save_agent_app_composer_updates_current_version(monkeypatch): assert result.pop("validation") == {"warnings": [], "knowledge_retrieval_placeholder": []} assert result == {"loaded": True} assert updated["operation"].value == "save_current_version" + assert agent.active_config_has_model is True assert fake_session._scalar == [] assert fake_session.commits == 1 @@ -431,6 +450,38 @@ def test_composer_save_helpers_create_and_rebind_agents(monkeypatch): assert new_version_binding.current_snapshot_id == "new-version-1" +def test_composer_create_agents_syncs_active_config_has_model(monkeypatch): + fake_session = FakeSession() + monkeypatch.setattr(composer_service.db, "session", fake_session) + monkeypatch.setattr( + AgentComposerService, + "_create_config_version", + lambda **kwargs: SimpleNamespace(id="version-with-model"), + ) + + workflow_agent = AgentComposerService._create_workflow_only_agent( + tenant_id="tenant-1", + app_id="app-1", + workflow_id="workflow-1", + node_id="node-1", + account_id="account-1", + agent_soul=_agent_soul_with_model(), + ) + roster_agent = AgentComposerService._create_roster_agent_for_composer( + tenant_id="tenant-1", + account_id="account-1", + name="Ready Agent", + agent_soul=_agent_soul_with_model(), + operation=AgentConfigRevisionOperation.CREATE_VERSION, + version_note=None, + ) + + assert workflow_agent.active_config_snapshot_id == "version-with-model" + assert workflow_agent.active_config_has_model is True + assert roster_agent.active_config_snapshot_id == "version-with-model" + assert roster_agent.active_config_has_model is True + + def test_composer_version_helpers_and_lookup_errors(monkeypatch): fake_session = FakeSession( scalar=[ @@ -554,20 +605,50 @@ def test_roster_list_and_invite_options(monkeypatch): ) agent.created_at = created_at agent.updated_at = updated_at - version = AgentConfigSnapshot(id="version-1", agent_id="agent-1", version=1) + version = AgentConfigSnapshot( + id="version-1", agent_id="agent-1", version=1, config_snapshot=_agent_soul_with_model() + ) version.created_at = version_created_at agent.active_config_snapshot_id = "version-1" + agent.active_config_has_model = True + unconfigured_agent = Agent( + id="agent-2", + tenant_id="tenant-1", + name="Draft Agent", + description="", + role="draft", + agent_kind=AgentKind.DIFY_AGENT, + scope=AgentScope.ROSTER, + source=AgentSource.AGENT_APP, + status=AgentStatus.ACTIVE, + ) + unconfigured_agent.active_config_snapshot_id = "version-2" + unconfigured_agent.active_config_has_model = False + unconfigured_version = AgentConfigSnapshot( + id="version-2", agent_id="agent-2", version=1, config_snapshot=AgentSoulConfig() + ) fake_session = FakeSession( - scalar=[1, 1, SimpleNamespace(id="workflow-1")], - scalars=[[agent], [agent], [SimpleNamespace(agent_id="agent-1", node_id="node-1")]], + scalar=[2, 1, SimpleNamespace(id="workflow-1")], + scalars=[ + [agent, unconfigured_agent], + [agent], + [SimpleNamespace(agent_id="agent-1", node_id="node-1")], + ], ) service = AgentRosterService(fake_session) - monkeypatch.setattr(service, "_load_versions_by_id", lambda version_ids: {"version-1": version}) + monkeypatch.setattr( + service, + "_load_versions_by_id", + lambda version_ids: {"version-1": version, "version-2": unconfigured_version}, + ) monkeypatch.setattr(service, "_load_published_references_by_agent_id", lambda **kwargs: {}) listed = service.list_roster_agents(tenant_id="tenant-1", page=1, limit=20) invited = service.list_invite_options(tenant_id="tenant-1", page=1, limit=20, app_id="app-1") + assert [item["id"] for item in listed["data"]] == ["agent-1", "agent-2"] + assert [item["id"] for item in invited["data"]] == ["agent-1"] + assert invited["total"] == 1 assert listed["data"][0]["active_config_snapshot"]["id"] == "version-1" assert listed["data"][0]["role"] == "researcher" assert listed["data"][0]["created_at"] == int(created_at.timestamp()) @@ -577,6 +658,39 @@ def test_roster_list_and_invite_options(monkeypatch): assert invited["data"][0]["existing_node_ids"] == ["node-1"] +def test_invite_options_uses_db_filtered_pagination(monkeypatch): + configured_agent = Agent( + id="agent-2", + tenant_id="tenant-1", + name="Ready Agent", + description="", + agent_kind=AgentKind.DIFY_AGENT, + scope=AgentScope.ROSTER, + source=AgentSource.AGENT_APP, + status=AgentStatus.ACTIVE, + active_config_snapshot_id="version-2", + active_config_has_model=True, + ) + fake_session = FakeSession(scalar=[1], scalars=[[configured_agent]]) + service = AgentRosterService(fake_session) + monkeypatch.setattr( + service, + "_load_versions_by_id", + lambda version_ids: { + "version-2": AgentConfigSnapshot( + id="version-2", agent_id="agent-2", version=1, config_snapshot=_agent_soul_with_model() + ) + }, + ) + monkeypatch.setattr(service, "_load_published_references_by_agent_id", lambda **kwargs: {}) + + result = service.list_invite_options(tenant_id="tenant-1", page=1, limit=1) + + assert result["total"] == 1 + assert result["has_more"] is False + assert [item["id"] for item in result["data"]] == ["agent-2"] + + def test_roster_update_archive_versions_and_detail(monkeypatch): listed_version = AgentConfigSnapshot(id="version-2", agent_id="agent-1", version=2) listed_version_created_at = datetime(2026, 1, 5, 3, 4, 5, tzinfo=UTC) @@ -657,6 +771,12 @@ def test_roster_create_detail_and_lookup_helpers(monkeypatch): ) created = service.create_roster_agent(tenant_id="tenant-1", account_id="account-1", payload=payload) + backing_agent = service.create_backing_agent_for_app( + tenant_id="tenant-1", + account_id="account-1", + app_id="app-1", + name="Backing Agent", + ) found_agent = service._get_agent(tenant_id="tenant-1", agent_id="agent-1") with pytest.raises(roster_service.AgentNotFoundError): service._get_agent(tenant_id="tenant-1", agent_id="missing") @@ -668,6 +788,9 @@ def test_roster_create_detail_and_lookup_helpers(monkeypatch): assert created.name == "Analyst" assert created.active_config_snapshot_id is not None + assert created.active_config_has_model is False + assert backing_agent.active_config_snapshot_id is not None + assert backing_agent.active_config_has_model is False assert found_agent.id == "agent-1" assert found_version.id == "version-1" assert loaded_versions["version-1"].agent_id == "agent-1" From 800bfc988efba4eccc3c77044855d34d7d1707bc Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:55:57 +0800 Subject: [PATCH 049/122] fix(web): tighten start block preview card spacing (#37379) --- web/app/components/workflow/block-selector/start-blocks.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/app/components/workflow/block-selector/start-blocks.tsx b/web/app/components/workflow/block-selector/start-blocks.tsx index dbd0129fd18..98be2954312 100644 --- a/web/app/components/workflow/block-selector/start-blocks.tsx +++ b/web/app/components/workflow/block-selector/start-blocks.tsx @@ -210,7 +210,7 @@ function StartBlockPreviewCard({ ].includes(block.type) return ( - +
{showDifyTeamAuthor && ( -
+
{t('author', { ns: 'tools' })} {' '} {t('difyTeam', { ns: 'workflow' })} From e0c6ca9930544d384d85deb0f379d2553ed1ac87 Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Fri, 12 Jun 2026 17:01:22 +0800 Subject: [PATCH 050/122] fix: GET query parameter OpenAPI contracts (#37378) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/controllers/common/schema.py | 74 +++++- .../console/app/advanced_prompt_template.py | 4 +- api/controllers/console/app/agent.py | 4 +- api/controllers/console/app/annotation.py | 16 +- api/controllers/console/app/app.py | 11 +- api/controllers/console/app/audio.py | 4 +- api/controllers/console/app/conversation.py | 6 +- .../console/app/conversation_variables.py | 4 +- api/controllers/console/app/message.py | 6 +- api/controllers/console/app/ops_trace.py | 4 +- api/controllers/console/app/statistic.py | 18 +- api/controllers/console/app/workflow.py | 5 +- .../console/app/workflow_app_log.py | 6 +- .../console/app/workflow_draft_variable.py | 4 +- .../console/app/workflow_statistic.py | 10 +- .../console/app/workflow_trigger.py | 4 +- api/controllers/console/auth/activate.py | 4 +- api/controllers/console/billing/compliance.py | 3 +- api/controllers/console/datasets/website.py | 4 +- .../console/explore/conversation.py | 4 +- api/controllers/console/explore/message.py | 6 +- .../console/explore/saved_message.py | 4 +- .../console/snippets/snippet_workflow.py | 4 +- .../snippet_workflow_draft_variable.py | 3 +- api/controllers/console/workspace/account.py | 2 +- api/controllers/console/workspace/endpoint.py | 6 +- .../console/workspace/model_providers.py | 6 +- api/controllers/console/workspace/models.py | 13 +- api/controllers/console/workspace/plugin.py | 23 +- api/controllers/console/workspace/snippets.py | 4 +- .../console/workspace/workspace.py | 4 +- .../service_api/app/conversation.py | 6 +- .../service_api/app/file_preview.py | 4 +- api/controllers/service_api/app/message.py | 6 +- api/controllers/service_api/app/workflow.py | 4 +- api/controllers/web/message.py | 4 +- api/dev/generate_swagger_specs.py | 94 -------- api/openapi/markdown/console-openapi.md | 161 +++++++------- api/openapi/markdown/openapi-openapi.md | 4 +- api/openapi/markdown/service-openapi.md | 20 +- .../commands/test_generate_swagger_specs.py | 2 +- .../controllers/common/test_schema.py | 17 ++ .../unit_tests/controllers/test_swagger.py | 15 ++ cli/src/api/apps.ts | 4 +- cli/src/commands/get/app/index.ts | 3 +- cli/src/commands/get/app/run.ts | 2 +- .../api/console/activate/types.gen.ts | 4 +- .../generated/api/console/activate/zod.gen.ts | 4 +- .../generated/api/console/apps/types.gen.ts | 152 +++++++------ .../generated/api/console/apps/zod.gen.ts | 152 ++++++------- .../api/console/installed-apps/types.gen.ts | 8 +- .../api/console/installed-apps/zod.gen.ts | 8 +- .../generated/api/console/website/orpc.gen.ts | 2 +- .../api/console/website/types.gen.ts | 4 +- .../generated/api/console/website/zod.gen.ts | 2 +- .../api/console/workspaces/types.gen.ts | 26 +-- .../api/console/workspaces/zod.gen.ts | 210 +++++++++--------- .../generated/api/openapi/types.gen.ts | 20 +- .../generated/api/openapi/zod.gen.ts | 26 ++- .../generated/api/service/types.gen.ts | 20 +- .../generated/api/service/zod.gen.ts | 20 +- packages/contracts/openapi-ts.api.config.ts | 106 --------- 62 files changed, 678 insertions(+), 702 deletions(-) diff --git a/api/controllers/common/schema.py b/api/controllers/common/schema.py index e3172f81c73..0fb2f3c5884 100644 --- a/api/controllers/common/schema.py +++ b/api/controllers/common/schema.py @@ -27,12 +27,18 @@ QueryParamDoc = TypedDict( "description": NotRequired[str], "enum": NotRequired[list[object]], "default": NotRequired[object], + "format": NotRequired[str], "minimum": NotRequired[int | float], "maximum": NotRequired[int | float], + "exclusiveMinimum": NotRequired[int | float], + "exclusiveMaximum": NotRequired[int | float], "minLength": NotRequired[int], "maxLength": NotRequired[int], + "pattern": NotRequired[str], "minItems": NotRequired[int], "maxItems": NotRequired[int], + "uniqueItems": NotRequired[bool], + "multipleOf": NotRequired[int | float], }, ) @@ -129,6 +135,7 @@ def query_params_from_model(model: type[BaseModel]) -> dict[str, QueryParamDoc]: """ schema = model.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0) + definitions = _schema_definitions(schema) properties = schema.get("properties", {}) if not isinstance(properties, Mapping): return {} @@ -141,7 +148,11 @@ def query_params_from_model(model: type[BaseModel]) -> dict[str, QueryParamDoc]: if not isinstance(name, str) or not isinstance(property_schema, Mapping): continue - params[name] = _query_param_from_property(property_schema, required=name in required_names) + params[name] = _query_param_from_property( + property_schema, + required=name in required_names, + definitions=definitions, + ) return params @@ -198,8 +209,18 @@ def _drop_malformed_defaulted_integer_params(model: type[BaseModel], params: dic params.pop(name) -def _query_param_from_property(property_schema: Mapping[str, Any], *, required: bool) -> QueryParamDoc: - param_schema = _nullable_property_schema(property_schema) +def _schema_definitions(schema: Mapping[str, Any]) -> Mapping[str, Any]: + definitions = schema.get("$defs") + return definitions if isinstance(definitions, Mapping) else {} + + +def _query_param_from_property( + property_schema: Mapping[str, Any], + *, + required: bool, + definitions: Mapping[str, Any], +) -> QueryParamDoc: + param_schema = _resolve_schema_ref(_nullable_property_schema(property_schema), definitions) param_doc: QueryParamDoc = {"in": "query", "required": required} description = param_schema.get("description") @@ -212,9 +233,16 @@ def _query_param_from_property(property_schema: Mapping[str, Any], *, required: if schema_type == "array": items = param_schema.get("items") if isinstance(items, Mapping): - item_type = items.get("type") + item_schema = _resolve_schema_ref(items, definitions) + item_type = item_schema.get("type") if isinstance(item_type, str): param_doc["items"] = {"type": item_type} + item_enum = item_schema.get("enum") + if isinstance(item_enum, list): + param_doc.setdefault("items", {})["enum"] = item_enum + item_format = item_schema.get("format") + if isinstance(item_format, str): + param_doc.setdefault("items", {})["format"] = item_format enum = param_schema.get("enum") if isinstance(enum, list): @@ -224,6 +252,10 @@ def _query_param_from_property(property_schema: Mapping[str, Any], *, required: if default is not None: param_doc["default"] = default + schema_format = param_schema.get("format") + if isinstance(schema_format, str): + param_doc["format"] = schema_format + minimum = param_schema.get("minimum") if isinstance(minimum, int | float): param_doc["minimum"] = minimum @@ -232,6 +264,14 @@ def _query_param_from_property(property_schema: Mapping[str, Any], *, required: if isinstance(maximum, int | float): param_doc["maximum"] = maximum + exclusive_minimum = param_schema.get("exclusiveMinimum") + if isinstance(exclusive_minimum, int | float): + param_doc["exclusiveMinimum"] = exclusive_minimum + + exclusive_maximum = param_schema.get("exclusiveMaximum") + if isinstance(exclusive_maximum, int | float): + param_doc["exclusiveMaximum"] = exclusive_maximum + min_length = param_schema.get("minLength") if isinstance(min_length, int): param_doc["minLength"] = min_length @@ -240,6 +280,10 @@ def _query_param_from_property(property_schema: Mapping[str, Any], *, required: if isinstance(max_length, int): param_doc["maxLength"] = max_length + pattern = param_schema.get("pattern") + if isinstance(pattern, str): + param_doc["pattern"] = pattern + min_items = param_schema.get("minItems") if isinstance(min_items, int): param_doc["minItems"] = min_items @@ -248,9 +292,31 @@ def _query_param_from_property(property_schema: Mapping[str, Any], *, required: if isinstance(max_items, int): param_doc["maxItems"] = max_items + unique_items = param_schema.get("uniqueItems") + if isinstance(unique_items, bool): + param_doc["uniqueItems"] = unique_items + + multiple_of = param_schema.get("multipleOf") + if isinstance(multiple_of, int | float): + param_doc["multipleOf"] = multiple_of + return param_doc +def _resolve_schema_ref(property_schema: Mapping[str, Any], definitions: Mapping[str, Any]) -> Mapping[str, Any]: + ref = property_schema.get("$ref") + if not isinstance(ref, str): + return property_schema + + ref_name = ref.rsplit("/", 1)[-1] + resolved = definitions.get(ref_name) + if not isinstance(resolved, Mapping): + return property_schema + + property_without_ref = {key: value for key, value in property_schema.items() if key != "$ref"} + return {**resolved, **property_without_ref} + + def _nullable_property_schema(property_schema: Mapping[str, Any]) -> Mapping[str, Any]: any_of = property_schema.get("anyOf") if not isinstance(any_of, list): diff --git a/api/controllers/console/app/advanced_prompt_template.py b/api/controllers/console/app/advanced_prompt_template.py index 0ad7eee7cdf..ca8e7096822 100644 --- a/api/controllers/console/app/advanced_prompt_template.py +++ b/api/controllers/console/app/advanced_prompt_template.py @@ -2,7 +2,7 @@ from flask import request from flask_restx import Resource, fields from pydantic import BaseModel, Field -from controllers.common.schema import DEFAULT_REF_TEMPLATE_OPENAPI_3_0 +from controllers.common.schema import DEFAULT_REF_TEMPLATE_OPENAPI_3_0, query_params_from_model from controllers.console import console_ns from controllers.console.wraps import account_initialization_required, setup_required from libs.login import login_required @@ -26,7 +26,7 @@ console_ns.schema_model( class AdvancedPromptTemplateList(Resource): @console_ns.doc("get_advanced_prompt_templates") @console_ns.doc(description="Get advanced prompt templates based on app mode and model configuration") - @console_ns.expect(console_ns.models[AdvancedPromptTemplateQuery.__name__]) + @console_ns.doc(params=query_params_from_model(AdvancedPromptTemplateQuery)) @console_ns.response( 200, "Prompt templates retrieved successfully", fields.List(fields.Raw(description="Prompt template data")) ) diff --git a/api/controllers/console/app/agent.py b/api/controllers/console/app/agent.py index 2c3adb6a018..833ca97fa72 100644 --- a/api/controllers/console/app/agent.py +++ b/api/controllers/console/app/agent.py @@ -2,7 +2,7 @@ from flask import request from flask_restx import Resource, fields from pydantic import BaseModel, Field, field_validator -from controllers.common.schema import register_schema_models +from controllers.common.schema import query_params_from_model, register_schema_models from controllers.console import console_ns from controllers.console.app.wraps import get_app_model from controllers.console.wraps import account_initialization_required, setup_required, with_current_user @@ -36,7 +36,7 @@ class AgentLogApi(Resource): @console_ns.doc("get_agent_logs") @console_ns.doc(description="Get agent execution logs for an application") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.expect(console_ns.models[AgentLogQuery.__name__]) + @console_ns.doc(params=query_params_from_model(AgentLogQuery)) @console_ns.response( 200, "Agent logs retrieved successfully", fields.List(fields.Raw(description="Agent log entries")) ) diff --git a/api/controllers/console/app/annotation.py b/api/controllers/console/app/annotation.py index d066177df3c..497f3653633 100644 --- a/api/controllers/console/app/annotation.py +++ b/api/controllers/console/app/annotation.py @@ -6,7 +6,7 @@ from flask_restx import Resource from pydantic import BaseModel, Field, TypeAdapter, field_validator from controllers.common.errors import NoFileUploadedError, TooManyFilesError -from controllers.common.schema import register_schema_models +from controllers.common.schema import query_params_from_model, register_schema_models from controllers.console import console_ns from controllers.console.wraps import ( account_initialization_required, @@ -77,6 +77,11 @@ class AnnotationReplyStatusQuery(BaseModel): action: Literal["enable", "disable"] +class AnnotationHitHistoryListQuery(BaseModel): + page: int = Field(default=1, ge=1, description="Page number") + limit: int = Field(default=20, ge=1, description="Page size") + + class AnnotationFilePayload(BaseModel): message_id: str = Field(..., description="Message ID") @@ -99,6 +104,7 @@ register_schema_models( CreateAnnotationPayload, UpdateAnnotationPayload, AnnotationReplyStatusQuery, + AnnotationHitHistoryListQuery, AnnotationFilePayload, ) @@ -204,7 +210,7 @@ class AnnotationApi(Resource): @console_ns.doc("list_annotations") @console_ns.doc(description="Get annotations for an app with pagination") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.expect(console_ns.models[AnnotationListQuery.__name__]) + @console_ns.doc(params=query_params_from_model(AnnotationListQuery)) @console_ns.response(200, "Annotations retrieved successfully") @console_ns.response(403, "Insufficient permissions") @setup_required @@ -424,11 +430,7 @@ class AnnotationHitHistoryListApi(Resource): @console_ns.doc("list_annotation_hit_histories") @console_ns.doc(description="Get hit histories for an annotation") @console_ns.doc(params={"app_id": "Application ID", "annotation_id": "Annotation ID"}) - @console_ns.expect( - console_ns.parser() - .add_argument("page", type=int, location="args", default=1, help="Page number") - .add_argument("limit", type=int, location="args", default=20, help="Page size") - ) + @console_ns.doc(params=query_params_from_model(AnnotationHitHistoryListQuery)) @console_ns.response( 200, "Hit histories retrieved successfully", diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index 818da5553d5..598bb13577b 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -14,7 +14,12 @@ from werkzeug.exceptions import BadRequest from controllers.common.fields import RedirectUrlResponse, SimpleResultResponse from controllers.common.helpers import FileInfo -from controllers.common.schema import register_enum_models, register_response_schema_models, register_schema_models +from controllers.common.schema import ( + query_params_from_model, + register_enum_models, + register_response_schema_models, + register_schema_models, +) from controllers.console import console_ns from controllers.console.app.wraps import get_app_model, with_session from controllers.console.workspace.models import LoadBalancingPayload @@ -495,7 +500,7 @@ register_schema_models( class AppListApi(Resource): @console_ns.doc("list_apps") @console_ns.doc(description="Get list of applications with pagination and filtering") - @console_ns.expect(console_ns.models[AppListQuery.__name__]) + @console_ns.doc(params=query_params_from_model(AppListQuery)) @console_ns.response(200, "Success", console_ns.models[AppPagination.__name__]) @setup_required @login_required @@ -737,7 +742,7 @@ class AppExportApi(Resource): @console_ns.doc("export_app") @console_ns.doc(description="Export application configuration as DSL") @console_ns.doc(params={"app_id": "Application ID to export"}) - @console_ns.expect(console_ns.models[AppExportQuery.__name__]) + @console_ns.doc(params=query_params_from_model(AppExportQuery)) @console_ns.response(200, "App exported successfully", console_ns.models[AppExportResponse.__name__]) @console_ns.response(403, "Insufficient permissions") @get_app_model diff --git a/api/controllers/console/app/audio.py b/api/controllers/console/app/audio.py index acf2215e45b..f0d904faef2 100644 --- a/api/controllers/console/app/audio.py +++ b/api/controllers/console/app/audio.py @@ -6,7 +6,7 @@ from pydantic import BaseModel, Field from werkzeug.exceptions import InternalServerError import services -from controllers.common.schema import register_schema_models +from controllers.common.schema import query_params_from_model, register_schema_models from controllers.console import console_ns from controllers.console.app.error import ( AppUnavailableError, @@ -162,7 +162,7 @@ class TextModesApi(Resource): @console_ns.doc("get_text_to_speech_voices") @console_ns.doc(description="Get available TTS voices for a specific language") @console_ns.doc(params={"app_id": "App ID"}) - @console_ns.expect(console_ns.models[TextToSpeechVoiceQuery.__name__]) + @console_ns.doc(params=query_params_from_model(TextToSpeechVoiceQuery)) @console_ns.response( 200, "TTS voices retrieved successfully", fields.List(fields.Raw(description="Available voices")) ) diff --git a/api/controllers/console/app/conversation.py b/api/controllers/console/app/conversation.py index 7c20f025680..b9fcf2073d7 100644 --- a/api/controllers/console/app/conversation.py +++ b/api/controllers/console/app/conversation.py @@ -9,7 +9,7 @@ from sqlalchemy import func, or_ from sqlalchemy.orm import selectinload from werkzeug.exceptions import NotFound -from controllers.common.schema import register_schema_models +from controllers.common.schema import query_params_from_model, register_schema_models from controllers.console import console_ns from controllers.console.app.wraps import get_app_model from controllers.console.wraps import ( @@ -91,7 +91,7 @@ class CompletionConversationApi(Resource): @console_ns.doc("list_completion_conversations") @console_ns.doc(description="Get completion conversations with pagination and filtering") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.expect(console_ns.models[CompletionConversationQuery.__name__]) + @console_ns.doc(params=query_params_from_model(CompletionConversationQuery)) @console_ns.response(200, "Success", console_ns.models[ConversationPaginationResponse.__name__]) @console_ns.response(403, "Insufficient permissions") @setup_required @@ -206,7 +206,7 @@ class ChatConversationApi(Resource): @console_ns.doc("list_chat_conversations") @console_ns.doc(description="Get chat conversations with pagination, filtering and summary") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.expect(console_ns.models[ChatConversationQuery.__name__]) + @console_ns.doc(params=query_params_from_model(ChatConversationQuery)) @console_ns.response(200, "Success", console_ns.models[ConversationWithSummaryPaginationResponse.__name__]) @console_ns.response(403, "Insufficient permissions") @setup_required diff --git a/api/controllers/console/app/conversation_variables.py b/api/controllers/console/app/conversation_variables.py index beaef482756..9cf3f278eac 100644 --- a/api/controllers/console/app/conversation_variables.py +++ b/api/controllers/console/app/conversation_variables.py @@ -9,7 +9,7 @@ from pydantic import BaseModel, Field, field_validator from sqlalchemy import select from sqlalchemy.orm import sessionmaker -from controllers.common.schema import register_schema_models +from controllers.common.schema import query_params_from_model, register_schema_models from controllers.console import console_ns from controllers.console.app.wraps import get_app_model from controllers.console.wraps import account_initialization_required, setup_required @@ -84,7 +84,7 @@ class ConversationVariablesApi(Resource): @console_ns.doc("get_conversation_variables") @console_ns.doc(description="Get conversation variables for an application") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.expect(console_ns.models[ConversationVariablesQuery.__name__]) + @console_ns.doc(params=query_params_from_model(ConversationVariablesQuery)) @console_ns.response( 200, "Conversation variables retrieved successfully", diff --git a/api/controllers/console/app/message.py b/api/controllers/console/app/message.py index 90f30126f43..e698f7e234c 100644 --- a/api/controllers/console/app/message.py +++ b/api/controllers/console/app/message.py @@ -11,7 +11,7 @@ from werkzeug.exceptions import InternalServerError, NotFound from controllers.common.controller_schemas import MessageFeedbackPayload as _MessageFeedbackPayloadBase from controllers.common.fields import SimpleResultResponse -from controllers.common.schema import register_response_schema_models, register_schema_models +from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns from controllers.console.app.error import ( CompletionRequestError, @@ -174,7 +174,7 @@ class ChatMessageListApi(Resource): @console_ns.doc("list_chat_messages") @console_ns.doc(description="Get chat messages for a conversation with pagination") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.expect(console_ns.models[ChatMessagesQuery.__name__]) + @console_ns.doc(params=query_params_from_model(ChatMessagesQuery)) @console_ns.response(200, "Success", console_ns.models[MessageInfiniteScrollPaginationResponse.__name__]) @console_ns.response(404, "Conversation not found") @login_required @@ -372,7 +372,7 @@ class MessageFeedbackExportApi(Resource): @console_ns.doc("export_feedbacks") @console_ns.doc(description="Export user feedback data for Google Sheets") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.expect(console_ns.models[FeedbackExportQuery.__name__]) + @console_ns.doc(params=query_params_from_model(FeedbackExportQuery)) @console_ns.response(200, "Feedback data exported successfully") @console_ns.response(400, "Invalid parameters") @console_ns.response(500, "Internal server error") diff --git a/api/controllers/console/app/ops_trace.py b/api/controllers/console/app/ops_trace.py index a1123a580ea..2e20c3876a5 100644 --- a/api/controllers/console/app/ops_trace.py +++ b/api/controllers/console/app/ops_trace.py @@ -5,7 +5,7 @@ from flask_restx import Resource, fields from pydantic import BaseModel, Field from werkzeug.exceptions import BadRequest -from controllers.common.schema import register_schema_models +from controllers.common.schema import query_params_from_model, register_schema_models from controllers.console import console_ns from controllers.console.app.error import TracingConfigCheckError, TracingConfigIsExist, TracingConfigNotExist from controllers.console.app.wraps import get_app_model @@ -36,7 +36,7 @@ class TraceAppConfigApi(Resource): @console_ns.doc("get_trace_app_config") @console_ns.doc(description="Get tracing configuration for an application") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.expect(console_ns.models[TraceProviderQuery.__name__]) + @console_ns.doc(params=query_params_from_model(TraceProviderQuery)) @console_ns.response( 200, "Tracing configuration retrieved successfully", fields.Raw(description="Tracing configuration data") ) diff --git a/api/controllers/console/app/statistic.py b/api/controllers/console/app/statistic.py index 2595ed00813..61d871e885b 100644 --- a/api/controllers/console/app/statistic.py +++ b/api/controllers/console/app/statistic.py @@ -5,7 +5,7 @@ from flask import abort, jsonify, request from flask_restx import Resource, fields from pydantic import BaseModel, Field, field_validator -from controllers.common.schema import register_schema_models +from controllers.common.schema import query_params_from_model, register_schema_models from controllers.console import console_ns from controllers.console.app.wraps import get_app_model from controllers.console.wraps import account_initialization_required, setup_required, with_current_user @@ -39,7 +39,7 @@ class DailyMessageStatistic(Resource): @console_ns.doc("get_daily_message_statistics") @console_ns.doc(description="Get daily message statistics for an application") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.expect(console_ns.models[StatisticTimeRangeQuery.__name__]) + @console_ns.doc(params=query_params_from_model(StatisticTimeRangeQuery)) @console_ns.response( 200, "Daily message statistics retrieved successfully", @@ -99,7 +99,7 @@ class DailyConversationStatistic(Resource): @console_ns.doc("get_daily_conversation_statistics") @console_ns.doc(description="Get daily conversation statistics for an application") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.expect(console_ns.models[StatisticTimeRangeQuery.__name__]) + @console_ns.doc(params=query_params_from_model(StatisticTimeRangeQuery)) @console_ns.response( 200, "Daily conversation statistics retrieved successfully", @@ -158,7 +158,7 @@ class DailyTerminalsStatistic(Resource): @console_ns.doc("get_daily_terminals_statistics") @console_ns.doc(description="Get daily terminal/end-user statistics for an application") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.expect(console_ns.models[StatisticTimeRangeQuery.__name__]) + @console_ns.doc(params=query_params_from_model(StatisticTimeRangeQuery)) @console_ns.response( 200, "Daily terminal statistics retrieved successfully", @@ -218,7 +218,7 @@ class DailyTokenCostStatistic(Resource): @console_ns.doc("get_daily_token_cost_statistics") @console_ns.doc(description="Get daily token cost statistics for an application") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.expect(console_ns.models[StatisticTimeRangeQuery.__name__]) + @console_ns.doc(params=query_params_from_model(StatisticTimeRangeQuery)) @console_ns.response( 200, "Daily token cost statistics retrieved successfully", @@ -281,7 +281,7 @@ class AverageSessionInteractionStatistic(Resource): @console_ns.doc("get_average_session_interaction_statistics") @console_ns.doc(description="Get average session interaction statistics for an application") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.expect(console_ns.models[StatisticTimeRangeQuery.__name__]) + @console_ns.doc(params=query_params_from_model(StatisticTimeRangeQuery)) @console_ns.response( 200, "Average session interaction statistics retrieved successfully", @@ -360,7 +360,7 @@ class UserSatisfactionRateStatistic(Resource): @console_ns.doc("get_user_satisfaction_rate_statistics") @console_ns.doc(description="Get user satisfaction rate statistics for an application") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.expect(console_ns.models[StatisticTimeRangeQuery.__name__]) + @console_ns.doc(params=query_params_from_model(StatisticTimeRangeQuery)) @console_ns.response( 200, "User satisfaction rate statistics retrieved successfully", @@ -429,7 +429,7 @@ class AverageResponseTimeStatistic(Resource): @console_ns.doc("get_average_response_time_statistics") @console_ns.doc(description="Get average response time statistics for an application") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.expect(console_ns.models[StatisticTimeRangeQuery.__name__]) + @console_ns.doc(params=query_params_from_model(StatisticTimeRangeQuery)) @console_ns.response( 200, "Average response time statistics retrieved successfully", @@ -489,7 +489,7 @@ class TokensPerSecondStatistic(Resource): @console_ns.doc("get_tokens_per_second_statistics") @console_ns.doc(description="Get tokens per second statistics for an application") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.expect(console_ns.models[StatisticTimeRangeQuery.__name__]) + @console_ns.doc(params=query_params_from_model(StatisticTimeRangeQuery)) @console_ns.response( 200, "Tokens per second statistics retrieved successfully", diff --git a/api/controllers/console/app/workflow.py b/api/controllers/console/app/workflow.py index 6a14937ffa0..22eeac955cc 100644 --- a/api/controllers/console/app/workflow.py +++ b/api/controllers/console/app/workflow.py @@ -15,6 +15,7 @@ from controllers.common.controller_schemas import DefaultBlockConfigQuery, Workf from controllers.common.errors import InvalidArgumentError from controllers.common.fields import NewAppResponse, SimpleResultResponse from controllers.common.schema import ( + query_params_from_model, register_response_schema_model, register_response_schema_models, register_schema_models, @@ -1054,7 +1055,7 @@ class DefaultBlockConfigApi(Resource): @console_ns.doc(params={"app_id": "Application ID", "block_type": "Block type"}) @console_ns.response(200, "Default block configuration retrieved successfully") @console_ns.response(404, "Block type not found") - @console_ns.expect(console_ns.models[DefaultBlockConfigQuery.__name__]) + @console_ns.doc(params=query_params_from_model(DefaultBlockConfigQuery)) @setup_required @login_required @account_initialization_required @@ -1149,7 +1150,7 @@ class WorkflowFeaturesApi(Resource): @console_ns.route("/apps//workflows") class PublishedAllWorkflowApi(Resource): - @console_ns.expect(console_ns.models[WorkflowListQuery.__name__]) + @console_ns.doc(params=query_params_from_model(WorkflowListQuery)) @console_ns.doc("get_all_published_workflows") @console_ns.doc(description="Get all published workflows for an application") @console_ns.doc(params={"app_id": "Application ID"}) diff --git a/api/controllers/console/app/workflow_app_log.py b/api/controllers/console/app/workflow_app_log.py index dec183a3004..72bececd999 100644 --- a/api/controllers/console/app/workflow_app_log.py +++ b/api/controllers/console/app/workflow_app_log.py @@ -7,7 +7,7 @@ from flask_restx import Resource from pydantic import BaseModel, Field, field_validator from sqlalchemy.orm import sessionmaker -from controllers.common.schema import register_schema_models +from controllers.common.schema import query_params_from_model, register_schema_models from controllers.console import console_ns from controllers.console.app.wraps import get_app_model from controllers.console.wraps import account_initialization_required, setup_required @@ -166,7 +166,7 @@ class WorkflowAppLogApi(Resource): @console_ns.doc("get_workflow_app_logs") @console_ns.doc(description="Get workflow application execution logs") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.expect(console_ns.models[WorkflowAppLogQuery.__name__]) + @console_ns.doc(params=query_params_from_model(WorkflowAppLogQuery)) @console_ns.response( 200, "Workflow app logs retrieved successfully", @@ -209,7 +209,7 @@ class WorkflowArchivedLogApi(Resource): @console_ns.doc("get_workflow_archived_logs") @console_ns.doc(description="Get workflow archived execution logs") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.expect(console_ns.models[WorkflowAppLogQuery.__name__]) + @console_ns.doc(params=query_params_from_model(WorkflowAppLogQuery)) @console_ns.response( 200, "Workflow archived logs retrieved successfully", diff --git a/api/controllers/console/app/workflow_draft_variable.py b/api/controllers/console/app/workflow_draft_variable.py index 8ebd65eccf2..c23a0d63a62 100644 --- a/api/controllers/console/app/workflow_draft_variable.py +++ b/api/controllers/console/app/workflow_draft_variable.py @@ -10,7 +10,7 @@ from pydantic import BaseModel, Field from sqlalchemy.orm import sessionmaker from controllers.common.errors import InvalidArgumentError, NotFoundError -from controllers.common.schema import register_schema_models +from controllers.common.schema import query_params_from_model, register_schema_models from controllers.console import console_ns from controllers.console.app.error import ( DraftWorkflowNotExist, @@ -248,7 +248,7 @@ def _api_prerequisite[T, **P, R]( @console_ns.route("/apps//workflows/draft/variables") class WorkflowVariableCollectionApi(Resource): - @console_ns.expect(console_ns.models[WorkflowDraftVariableListQuery.__name__]) + @console_ns.doc(params=query_params_from_model(WorkflowDraftVariableListQuery)) @console_ns.doc("get_workflow_variables") @console_ns.doc(description="Get draft workflow variables") @console_ns.doc(params={"app_id": "Application ID"}) diff --git a/api/controllers/console/app/workflow_statistic.py b/api/controllers/console/app/workflow_statistic.py index 05d579527e7..8b3c9967293 100644 --- a/api/controllers/console/app/workflow_statistic.py +++ b/api/controllers/console/app/workflow_statistic.py @@ -3,7 +3,7 @@ from flask_restx import Resource from pydantic import BaseModel, Field, field_validator from sqlalchemy.orm import sessionmaker -from controllers.common.schema import register_schema_models +from controllers.common.schema import query_params_from_model, register_schema_models from controllers.console import console_ns from controllers.console.app.wraps import get_app_model from controllers.console.wraps import account_initialization_required, setup_required, with_current_user @@ -41,7 +41,7 @@ class WorkflowDailyRunsStatistic(Resource): @console_ns.doc("get_workflow_daily_runs_statistic") @console_ns.doc(description="Get workflow daily runs statistics") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.expect(console_ns.models[WorkflowStatisticQuery.__name__]) + @console_ns.doc(params=query_params_from_model(WorkflowStatisticQuery)) @console_ns.response(200, "Daily runs statistics retrieved successfully") @get_app_model @setup_required @@ -80,7 +80,7 @@ class WorkflowDailyTerminalsStatistic(Resource): @console_ns.doc("get_workflow_daily_terminals_statistic") @console_ns.doc(description="Get workflow daily terminals statistics") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.expect(console_ns.models[WorkflowStatisticQuery.__name__]) + @console_ns.doc(params=query_params_from_model(WorkflowStatisticQuery)) @console_ns.response(200, "Daily terminals statistics retrieved successfully") @get_app_model @setup_required @@ -119,7 +119,7 @@ class WorkflowDailyTokenCostStatistic(Resource): @console_ns.doc("get_workflow_daily_token_cost_statistic") @console_ns.doc(description="Get workflow daily token cost statistics") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.expect(console_ns.models[WorkflowStatisticQuery.__name__]) + @console_ns.doc(params=query_params_from_model(WorkflowStatisticQuery)) @console_ns.response(200, "Daily token cost statistics retrieved successfully") @get_app_model @setup_required @@ -158,7 +158,7 @@ class WorkflowAverageAppInteractionStatistic(Resource): @console_ns.doc("get_workflow_average_app_interaction_statistic") @console_ns.doc(description="Get workflow average app interaction statistics") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.expect(console_ns.models[WorkflowStatisticQuery.__name__]) + @console_ns.doc(params=query_params_from_model(WorkflowStatisticQuery)) @console_ns.response(200, "Average app interaction statistics retrieved successfully") @setup_required @login_required diff --git a/api/controllers/console/app/workflow_trigger.py b/api/controllers/console/app/workflow_trigger.py index 11c8b2ee553..6a1bd843ee2 100644 --- a/api/controllers/console/app/workflow_trigger.py +++ b/api/controllers/console/app/workflow_trigger.py @@ -9,7 +9,7 @@ from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import NotFound from configs import dify_config -from controllers.common.schema import register_schema_models +from controllers.common.schema import query_params_from_model, register_schema_models from extensions.ext_database import db from fields.base import ResponseModel from libs.login import login_required @@ -86,7 +86,7 @@ register_schema_models( class WebhookTriggerApi(Resource): """Webhook Trigger API""" - @console_ns.expect(console_ns.models[Parser.__name__]) + @console_ns.doc(params=query_params_from_model(Parser)) @setup_required @login_required @account_initialization_required diff --git a/api/controllers/console/auth/activate.py b/api/controllers/console/auth/activate.py index 65e278edb54..7e7810d86da 100644 --- a/api/controllers/console/auth/activate.py +++ b/api/controllers/console/auth/activate.py @@ -4,7 +4,7 @@ from pydantic import BaseModel, Field, field_validator from configs import dify_config from constants.languages import supported_language -from controllers.common.schema import register_schema_models +from controllers.common.schema import query_params_from_model, register_schema_models from controllers.console import console_ns from controllers.console.error import AccountInFreezeError, AlreadyActivateError from extensions.ext_database import db @@ -69,7 +69,7 @@ register_schema_models( class ActivateCheckApi(Resource): @console_ns.doc("check_activation_token") @console_ns.doc(description="Check if activation token is valid") - @console_ns.expect(console_ns.models[ActivateCheckQuery.__name__]) + @console_ns.doc(params=query_params_from_model(ActivateCheckQuery)) @console_ns.response( 200, "Success", diff --git a/api/controllers/console/billing/compliance.py b/api/controllers/console/billing/compliance.py index b0b364e54d2..8bc474964ab 100644 --- a/api/controllers/console/billing/compliance.py +++ b/api/controllers/console/billing/compliance.py @@ -2,6 +2,7 @@ from flask import request from flask_restx import Resource from pydantic import BaseModel, Field +from controllers.common.schema import query_params_from_model from libs.helper import extract_remote_ip from libs.login import login_required from models import Account @@ -30,7 +31,7 @@ console_ns.schema_model( @console_ns.route("/compliance/download") class ComplianceApi(Resource): - @console_ns.expect(console_ns.models[ComplianceDownloadQuery.__name__]) + @console_ns.doc(params=query_params_from_model(ComplianceDownloadQuery)) @console_ns.doc("download_compliance_document") @console_ns.doc(description="Get compliance document download link") @setup_required diff --git a/api/controllers/console/datasets/website.py b/api/controllers/console/datasets/website.py index 335c8f60308..9b0d5132b15 100644 --- a/api/controllers/console/datasets/website.py +++ b/api/controllers/console/datasets/website.py @@ -4,7 +4,7 @@ from flask import request from flask_restx import Resource from pydantic import BaseModel -from controllers.common.schema import register_schema_models +from controllers.common.schema import query_params_from_model, register_schema_models from controllers.console import console_ns from controllers.console.datasets.error import WebsiteCrawlError from controllers.console.wraps import account_initialization_required, setup_required @@ -57,7 +57,7 @@ class WebsiteCrawlStatusApi(Resource): @console_ns.doc("get_crawl_status") @console_ns.doc(description="Get website crawl status") @console_ns.doc(params={"job_id": "Crawl job ID", "provider": "Crawl provider (firecrawl/watercrawl/jinareader)"}) - @console_ns.expect(console_ns.models[WebsiteCrawlStatusQuery.__name__]) + @console_ns.doc(params=query_params_from_model(WebsiteCrawlStatusQuery)) @console_ns.response(200, "Crawl status retrieved successfully") @console_ns.response(404, "Crawl job not found") @console_ns.response(400, "Invalid provider") diff --git a/api/controllers/console/explore/conversation.py b/api/controllers/console/explore/conversation.py index 9cebba496b5..5b625debeae 100644 --- a/api/controllers/console/explore/conversation.py +++ b/api/controllers/console/explore/conversation.py @@ -7,7 +7,7 @@ from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import NotFound from controllers.common.controller_schemas import ConversationRenamePayload -from controllers.common.schema import register_response_schema_models, register_schema_models +from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console.app.error import AppUnavailableError from controllers.console.explore.error import NotChatAppError from controllers.console.explore.wraps import InstalledAppResource @@ -44,7 +44,7 @@ register_response_schema_models(console_ns, ResultResponse) endpoint="installed_app_conversations", ) class ConversationListApi(InstalledAppResource): - @console_ns.expect(console_ns.models[ConversationListQuery.__name__]) + @console_ns.doc(params=query_params_from_model(ConversationListQuery)) @with_current_user def get(self, current_user: Account, installed_app: InstalledApp): app_model = installed_app.app diff --git a/api/controllers/console/explore/message.py b/api/controllers/console/explore/message.py index a19355f90ba..88c0e679ad3 100644 --- a/api/controllers/console/explore/message.py +++ b/api/controllers/console/explore/message.py @@ -7,7 +7,7 @@ from pydantic import BaseModel, TypeAdapter from werkzeug.exceptions import InternalServerError, NotFound from controllers.common.controller_schemas import MessageFeedbackPayload, MessageListQuery -from controllers.common.schema import register_response_schema_models, register_schema_models +from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console.app.error import ( AppMoreLikeThisDisabledError, AppUnavailableError, @@ -60,7 +60,7 @@ register_response_schema_models(console_ns, ResultResponse, SuggestedQuestionsRe endpoint="installed_app_messages", ) class MessageListApi(InstalledAppResource): - @console_ns.expect(console_ns.models[MessageListQuery.__name__]) + @console_ns.doc(params=query_params_from_model(MessageListQuery)) @with_current_user def get(self, current_user: Account, installed_app: InstalledApp): app_model = installed_app.app @@ -129,7 +129,7 @@ class MessageFeedbackApi(InstalledAppResource): endpoint="installed_app_more_like_this", ) class MessageMoreLikeThisApi(InstalledAppResource): - @console_ns.expect(console_ns.models[MoreLikeThisQuery.__name__]) + @console_ns.doc(params=query_params_from_model(MoreLikeThisQuery)) @with_current_user def get(self, current_user: Account, installed_app: InstalledApp, message_id: UUID): app_model = installed_app.app diff --git a/api/controllers/console/explore/saved_message.py b/api/controllers/console/explore/saved_message.py index cf48eeea725..b8c0a9bb841 100644 --- a/api/controllers/console/explore/saved_message.py +++ b/api/controllers/console/explore/saved_message.py @@ -5,7 +5,7 @@ from pydantic import TypeAdapter from werkzeug.exceptions import NotFound from controllers.common.controller_schemas import SavedMessageCreatePayload, SavedMessageListQuery -from controllers.common.schema import register_response_schema_models, register_schema_models +from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns from controllers.console.app.error import AppUnavailableError from controllers.console.explore.error import NotCompletionAppError @@ -24,7 +24,7 @@ register_response_schema_models(console_ns, ResultResponse) @console_ns.route("/installed-apps//saved-messages", endpoint="installed_app_saved_messages") class SavedMessageListApi(InstalledAppResource): - @console_ns.expect(console_ns.models[SavedMessageListQuery.__name__]) + @console_ns.doc(params=query_params_from_model(SavedMessageListQuery)) @with_current_user def get(self, current_user: Account, installed_app: InstalledApp): app_model = installed_app.app diff --git a/api/controllers/console/snippets/snippet_workflow.py b/api/controllers/console/snippets/snippet_workflow.py index 59608afc554..c54d555686a 100644 --- a/api/controllers/console/snippets/snippet_workflow.py +++ b/api/controllers/console/snippets/snippet_workflow.py @@ -8,7 +8,7 @@ from pydantic import Field from sqlalchemy.orm import Session, sessionmaker from werkzeug.exceptions import BadRequest, InternalServerError, NotFound -from controllers.common.schema import register_response_schema_models, register_schema_models +from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns from controllers.console.app.error import DraftWorkflowNotExist, DraftWorkflowNotSync from controllers.console.app.workflow import ( @@ -283,7 +283,7 @@ class SnippetDefaultBlockConfigsApi(Resource): @console_ns.route("/snippets//workflows") class SnippetPublishedAllWorkflowApi(Resource): - @console_ns.expect(console_ns.models[SnippetWorkflowListQuery.__name__]) + @console_ns.doc(params=query_params_from_model(SnippetWorkflowListQuery)) @console_ns.doc("get_all_snippet_published_workflows") @console_ns.doc(description="Get all published workflows for a snippet") @console_ns.doc(params={"snippet_id": "Snippet ID"}) diff --git a/api/controllers/console/snippets/snippet_workflow_draft_variable.py b/api/controllers/console/snippets/snippet_workflow_draft_variable.py index 491a78a9b93..5c52287daab 100644 --- a/api/controllers/console/snippets/snippet_workflow_draft_variable.py +++ b/api/controllers/console/snippets/snippet_workflow_draft_variable.py @@ -19,6 +19,7 @@ from flask_restx import Resource, marshal, marshal_with from sqlalchemy.orm import Session, sessionmaker from controllers.common.errors import InvalidArgumentError, NotFoundError +from controllers.common.schema import query_params_from_model from controllers.console import console_ns from controllers.console.app.error import DraftWorkflowNotExist from controllers.console.app.workflow_draft_variable import ( @@ -90,7 +91,7 @@ def _snippet_draft_var_prerequisite[T, **P, R]( @console_ns.route("/snippets//workflows/draft/variables") class SnippetWorkflowVariableCollectionApi(Resource): - @console_ns.expect(console_ns.models[WorkflowDraftVariableListQuery.__name__]) + @console_ns.doc(params=query_params_from_model(WorkflowDraftVariableListQuery)) @console_ns.doc("get_snippet_workflow_variables") @console_ns.doc(description="List draft workflow variables without values (paginated, snippet scope)") @console_ns.response( diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index e58f34dc3b9..1b83e9c4824 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -585,7 +585,7 @@ class EducationApi(Resource): @console_ns.route("/account/education/autocomplete") class EducationAutoCompleteApi(Resource): - @console_ns.expect(console_ns.models[EducationAutocompleteQuery.__name__]) + @console_ns.doc(params=query_params_from_model(EducationAutocompleteQuery)) @setup_required @login_required @account_initialization_required diff --git a/api/controllers/console/workspace/endpoint.py b/api/controllers/console/workspace/endpoint.py index c539debf087..f69c5273177 100644 --- a/api/controllers/console/workspace/endpoint.py +++ b/api/controllers/console/workspace/endpoint.py @@ -12,7 +12,7 @@ from flask import request from flask_restx import Resource from pydantic import BaseModel, Field -from controllers.common.schema import register_schema_models +from controllers.common.schema import query_params_from_model, register_schema_models from controllers.console import console_ns from controllers.console.wraps import ( account_initialization_required, @@ -201,7 +201,7 @@ class DeprecatedEndpointCreateApi(Resource): class EndpointListApi(Resource): @console_ns.doc("list_endpoints") @console_ns.doc(description="List plugin endpoints with pagination") - @console_ns.expect(console_ns.models[EndpointListQuery.__name__]) + @console_ns.doc(params=query_params_from_model(EndpointListQuery)) @console_ns.response( 200, "Success", @@ -234,7 +234,7 @@ class EndpointListApi(Resource): class EndpointListForSinglePluginApi(Resource): @console_ns.doc("list_plugin_endpoints") @console_ns.doc(description="List endpoints for a specific plugin") - @console_ns.expect(console_ns.models[EndpointListForPluginQuery.__name__]) + @console_ns.doc(params=query_params_from_model(EndpointListForPluginQuery)) @console_ns.response( 200, "Success", diff --git a/api/controllers/console/workspace/model_providers.py b/api/controllers/console/workspace/model_providers.py index e77f17b2d0e..dc05b73436e 100644 --- a/api/controllers/console/workspace/model_providers.py +++ b/api/controllers/console/workspace/model_providers.py @@ -6,7 +6,7 @@ from flask_restx import Resource from pydantic import BaseModel, Field, field_validator from controllers.common.fields import SimpleResultResponse -from controllers.common.schema import register_response_schema_models, register_schema_models +from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns from controllers.console.wraps import ( account_initialization_required, @@ -98,7 +98,7 @@ register_response_schema_models(console_ns, SimpleResultResponse) @console_ns.route("/workspaces/current/model-providers") class ModelProviderListApi(Resource): - @console_ns.expect(console_ns.models[ParserModelList.__name__]) + @console_ns.doc(params=query_params_from_model(ParserModelList)) @setup_required @login_required @account_initialization_required @@ -115,7 +115,7 @@ class ModelProviderListApi(Resource): @console_ns.route("/workspaces/current/model-providers//credentials") class ModelProviderCredentialApi(Resource): - @console_ns.expect(console_ns.models[ParserCredentialId.__name__]) + @console_ns.doc(params=query_params_from_model(ParserCredentialId)) @setup_required @login_required @account_initialization_required diff --git a/api/controllers/console/workspace/models.py b/api/controllers/console/workspace/models.py index 19e3fc60bbf..860acce443c 100644 --- a/api/controllers/console/workspace/models.py +++ b/api/controllers/console/workspace/models.py @@ -6,7 +6,12 @@ from flask_restx import Resource from pydantic import BaseModel, Field, field_validator from controllers.common.fields import SimpleResultResponse -from controllers.common.schema import register_enum_models, register_response_schema_models, register_schema_models +from controllers.common.schema import ( + query_params_from_model, + register_enum_models, + register_response_schema_models, + register_schema_models, +) from controllers.console import console_ns from controllers.console.wraps import ( account_initialization_required, @@ -141,7 +146,7 @@ register_enum_models(console_ns, ModelType) @console_ns.route("/workspaces/current/default-model") class DefaultModelApi(Resource): - @console_ns.expect(console_ns.models[ParserGetDefault.__name__]) + @console_ns.doc(params=query_params_from_model(ParserGetDefault)) @setup_required @login_required @account_initialization_required @@ -267,7 +272,7 @@ class ModelProviderModelApi(Resource): @console_ns.route("/workspaces/current/model-providers//models/credentials") class ModelProviderModelCredentialApi(Resource): - @console_ns.expect(console_ns.models[ParserGetCredentials.__name__]) + @console_ns.doc(params=query_params_from_model(ParserGetCredentials)) @setup_required @login_required @account_initialization_required @@ -515,7 +520,7 @@ class ModelProviderModelValidateApi(Resource): @console_ns.route("/workspaces/current/model-providers//models/parameter-rules") class ModelProviderModelParameterRuleApi(Resource): - @console_ns.expect(console_ns.models[ParserParameter.__name__]) + @console_ns.doc(params=query_params_from_model(ParserParameter)) @setup_required @login_required @account_initialization_required diff --git a/api/controllers/console/workspace/plugin.py b/api/controllers/console/workspace/plugin.py index d0538f2de22..1f9867f4587 100644 --- a/api/controllers/console/workspace/plugin.py +++ b/api/controllers/console/workspace/plugin.py @@ -10,7 +10,12 @@ from werkzeug.exceptions import Forbidden from configs import dify_config from controllers.common.fields import SuccessResponse -from controllers.common.schema import register_enum_models, register_response_schema_models, register_schema_models +from controllers.common.schema import ( + query_params_from_model, + register_enum_models, + register_response_schema_models, + register_schema_models, +) from controllers.console import console_ns from controllers.console.workspace import plugin_permission_required from controllers.console.wraps import ( @@ -221,7 +226,7 @@ class PluginDebuggingKeyApi(Resource): @console_ns.route("/workspaces/current/plugin/list") class PluginListApi(Resource): - @console_ns.expect(console_ns.models[ParserList.__name__]) + @console_ns.doc(params=query_params_from_model(ParserList)) @setup_required @login_required @account_initialization_required @@ -274,7 +279,7 @@ class PluginListInstallationsFromIdsApi(Resource): @console_ns.route("/workspaces/current/plugin/icon") class PluginIconApi(Resource): - @console_ns.expect(console_ns.models[ParserIcon.__name__]) + @console_ns.doc(params=query_params_from_model(ParserIcon)) @setup_required def get(self): args = ParserIcon.model_validate(request.args.to_dict(flat=True)) @@ -290,7 +295,7 @@ class PluginIconApi(Resource): @console_ns.route("/workspaces/current/plugin/asset") class PluginAssetApi(Resource): - @console_ns.expect(console_ns.models[ParserAsset.__name__]) + @console_ns.doc(params=query_params_from_model(ParserAsset)) @setup_required @login_required @account_initialization_required @@ -425,7 +430,7 @@ class PluginInstallFromMarketplaceApi(Resource): @console_ns.route("/workspaces/current/plugin/marketplace/pkg") class PluginFetchMarketplacePkgApi(Resource): - @console_ns.expect(console_ns.models[ParserPluginIdentifierQuery.__name__]) + @console_ns.doc(params=query_params_from_model(ParserPluginIdentifierQuery)) @setup_required @login_required @account_initialization_required @@ -449,7 +454,7 @@ class PluginFetchMarketplacePkgApi(Resource): @console_ns.route("/workspaces/current/plugin/fetch-manifest") class PluginFetchManifestApi(Resource): - @console_ns.expect(console_ns.models[ParserPluginIdentifierQuery.__name__]) + @console_ns.doc(params=query_params_from_model(ParserPluginIdentifierQuery)) @setup_required @login_required @account_initialization_required @@ -468,7 +473,7 @@ class PluginFetchManifestApi(Resource): @console_ns.route("/workspaces/current/plugin/tasks") class PluginFetchInstallTasksApi(Resource): - @console_ns.expect(console_ns.models[ParserTasks.__name__]) + @console_ns.doc(params=query_params_from_model(ParserTasks)) @setup_required @login_required @account_initialization_required @@ -655,7 +660,7 @@ class PluginFetchPermissionApi(Resource): @console_ns.route("/workspaces/current/plugin/parameters/dynamic-options") class PluginFetchDynamicSelectOptionsApi(Resource): - @console_ns.expect(console_ns.models[ParserDynamicOptions.__name__]) + @console_ns.doc(params=query_params_from_model(ParserDynamicOptions)) @setup_required @login_required @is_admin_or_owner_required @@ -817,7 +822,7 @@ class PluginAutoUpgradeExcludePluginApi(Resource): @console_ns.route("/workspaces/current/plugin/readme") class PluginReadmeApi(Resource): - @console_ns.expect(console_ns.models[ParserReadme.__name__]) + @console_ns.doc(params=query_params_from_model(ParserReadme)) @setup_required @login_required @account_initialization_required diff --git a/api/controllers/console/workspace/snippets.py b/api/controllers/console/workspace/snippets.py index 4bec22e091d..ca27066ef12 100644 --- a/api/controllers/console/workspace/snippets.py +++ b/api/controllers/console/workspace/snippets.py @@ -8,7 +8,7 @@ from sqlalchemy.orm import Session, sessionmaker from werkzeug.datastructures import MultiDict from werkzeug.exceptions import NotFound -from controllers.common.schema import register_schema_models +from controllers.common.schema import query_params_from_model, register_schema_models from controllers.console import console_ns from controllers.console.snippets.payloads import ( CreateSnippetPayload, @@ -89,7 +89,7 @@ snippet_pagination_model = console_ns.model("SnippetPagination", snippet_paginat @console_ns.route("/workspaces/current/customized-snippets") class CustomizedSnippetsApi(Resource): @console_ns.doc("list_customized_snippets") - @console_ns.expect(console_ns.models.get(SnippetListQuery.__name__)) + @console_ns.doc(params=query_params_from_model(SnippetListQuery)) @console_ns.response(200, "Snippets retrieved successfully", snippet_pagination_model) @setup_required @login_required diff --git a/api/controllers/console/workspace/workspace.py b/api/controllers/console/workspace/workspace.py index 60ecaa16bdb..ad41290987a 100644 --- a/api/controllers/console/workspace/workspace.py +++ b/api/controllers/console/workspace/workspace.py @@ -16,7 +16,7 @@ from controllers.common.errors import ( TooManyFilesError, UnsupportedFileTypeError, ) -from controllers.common.schema import register_response_schema_models, register_schema_models +from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns from controllers.console.admin import admin_required from controllers.console.error import AccountNotLinkTenantError @@ -201,7 +201,7 @@ class TenantListApi(Resource): @console_ns.route("/all-workspaces") class WorkspaceListApi(Resource): - @console_ns.expect(console_ns.models[WorkspaceListQuery.__name__]) + @console_ns.doc(params=query_params_from_model(WorkspaceListQuery)) @setup_required @admin_required def get(self): diff --git a/api/controllers/service_api/app/conversation.py b/api/controllers/service_api/app/conversation.py index b298801ca02..f121374ca9a 100644 --- a/api/controllers/service_api/app/conversation.py +++ b/api/controllers/service_api/app/conversation.py @@ -10,7 +10,7 @@ from werkzeug.exceptions import BadRequest, NotFound import services from controllers.common.controller_schemas import ConversationRenamePayload -from controllers.common.schema import register_schema_models +from controllers.common.schema import query_params_from_model, register_schema_models from controllers.service_api import service_api_ns from controllers.service_api.app.error import NotChatAppError from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token @@ -138,7 +138,7 @@ register_schema_models( @service_api_ns.route("/conversations") class ConversationApi(Resource): - @service_api_ns.expect(service_api_ns.models[ConversationListQuery.__name__]) + @service_api_ns.doc(params=query_params_from_model(ConversationListQuery)) @service_api_ns.doc("list_conversations") @service_api_ns.doc(description="List all conversations for the current user") @service_api_ns.doc( @@ -250,7 +250,7 @@ class ConversationRenameApi(Resource): @service_api_ns.route("/conversations//variables") class ConversationVariablesApi(Resource): - @service_api_ns.expect(service_api_ns.models[ConversationVariablesQuery.__name__]) + @service_api_ns.doc(params=query_params_from_model(ConversationVariablesQuery)) @service_api_ns.doc("list_conversation_variables") @service_api_ns.doc(description="List all variables for a conversation") @service_api_ns.doc(params={"c_id": "Conversation ID"}) diff --git a/api/controllers/service_api/app/file_preview.py b/api/controllers/service_api/app/file_preview.py index 44f765d866d..11317016df3 100644 --- a/api/controllers/service_api/app/file_preview.py +++ b/api/controllers/service_api/app/file_preview.py @@ -8,7 +8,7 @@ from pydantic import BaseModel, Field from sqlalchemy import select from controllers.common.file_response import enforce_download_for_html -from controllers.common.schema import register_schema_model +from controllers.common.schema import query_params_from_model, register_schema_model from controllers.service_api import service_api_ns from controllers.service_api.app.error import ( FileAccessDeniedError, @@ -38,7 +38,7 @@ class FilePreviewApi(Resource): Files can only be accessed if they belong to messages within the requesting app's context. """ - @service_api_ns.expect(service_api_ns.models[FilePreviewQuery.__name__]) + @service_api_ns.doc(params=query_params_from_model(FilePreviewQuery)) @service_api_ns.doc("preview_file") @service_api_ns.doc(description="Preview or download a file uploaded via Service API") @service_api_ns.doc(params={"file_id": "UUID of the file to preview"}) diff --git a/api/controllers/service_api/app/message.py b/api/controllers/service_api/app/message.py index a77c4fb6608..bdb16794efe 100644 --- a/api/controllers/service_api/app/message.py +++ b/api/controllers/service_api/app/message.py @@ -9,7 +9,7 @@ from werkzeug.exceptions import BadRequest, InternalServerError, NotFound import services from controllers.common.controller_schemas import MessageFeedbackPayload, MessageListQuery from controllers.common.fields import SimpleResultStringListResponse -from controllers.common.schema import register_response_schema_models, register_schema_models +from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.service_api import service_api_ns from controllers.service_api.app.error import NotChatAppError from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token @@ -39,7 +39,7 @@ register_response_schema_models(service_api_ns, ResultResponse, SimpleResultStri @service_api_ns.route("/messages") class MessageListApi(Resource): - @service_api_ns.expect(service_api_ns.models[MessageListQuery.__name__]) + @service_api_ns.doc(params=query_params_from_model(MessageListQuery)) @service_api_ns.doc("list_messages") @service_api_ns.doc(description="List messages in a conversation") @service_api_ns.doc( @@ -120,7 +120,7 @@ class MessageFeedbackApi(Resource): @service_api_ns.route("/app/feedbacks") class AppGetFeedbacksApi(Resource): - @service_api_ns.expect(service_api_ns.models[FeedbackListQuery.__name__]) + @service_api_ns.doc(params=query_params_from_model(FeedbackListQuery)) @service_api_ns.doc("get_app_feedbacks") @service_api_ns.doc(description="Get all feedbacks for the application") @service_api_ns.doc( diff --git a/api/controllers/service_api/app/workflow.py b/api/controllers/service_api/app/workflow.py index 975fdf0cd92..04fb9900a2f 100644 --- a/api/controllers/service_api/app/workflow.py +++ b/api/controllers/service_api/app/workflow.py @@ -12,7 +12,7 @@ from werkzeug.exceptions import BadRequest, InternalServerError, NotFound from controllers.common.controller_schemas import WorkflowRunPayload as WorkflowRunPayloadBase from controllers.common.fields import SimpleResultResponse -from controllers.common.schema import register_response_schema_models, register_schema_models +from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.service_api import service_api_ns from controllers.service_api.app.error import ( CompletionRequestError, @@ -407,7 +407,7 @@ class WorkflowTaskStopApi(Resource): @service_api_ns.route("/workflows/logs") class WorkflowAppLogApi(Resource): - @service_api_ns.expect(service_api_ns.models[WorkflowLogQuery.__name__]) + @service_api_ns.doc(params=query_params_from_model(WorkflowLogQuery)) @service_api_ns.doc("get_workflow_logs") @service_api_ns.doc(description="Get workflow execution logs") @service_api_ns.doc( diff --git a/api/controllers/web/message.py b/api/controllers/web/message.py index e40e57c4367..ea941401129 100644 --- a/api/controllers/web/message.py +++ b/api/controllers/web/message.py @@ -7,7 +7,7 @@ from pydantic import BaseModel, Field, TypeAdapter from werkzeug.exceptions import InternalServerError, NotFound from controllers.common.controller_schemas import MessageFeedbackPayload, MessageListQuery -from controllers.common.schema import register_response_schema_models, register_schema_models +from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.web import web_ns from controllers.web.error import ( AppMoreLikeThisDisabledError, @@ -156,7 +156,7 @@ class MessageFeedbackApi(WebApiResource): class MessageMoreLikeThisApi(WebApiResource): @web_ns.doc("Generate More Like This") @web_ns.doc(description="Generate a new completion similar to an existing message (completion apps only).") - @web_ns.expect(web_ns.models[MessageMoreLikeThisQuery.__name__]) + @web_ns.doc(params=query_params_from_model(MessageMoreLikeThisQuery)) @web_ns.doc( responses={ 200: "Success", diff --git a/api/dev/generate_swagger_specs.py b/api/dev/generate_swagger_specs.py index 6b959b6d17e..d3b62511ea6 100644 --- a/api/dev/generate_swagger_specs.py +++ b/api/dev/generate_swagger_specs.py @@ -330,99 +330,6 @@ def _replace_legacy_refs(value: object) -> object: HTTP_METHODS = {"delete", "get", "head", "options", "patch", "post", "put", "trace"} -def _resolve_component_schema(payload: dict[str, object], schema: object) -> dict[str, object] | None: - if not isinstance(schema, dict): - return None - - ref = schema.get("$ref") - if isinstance(ref, str) and ref.startswith("#/components/schemas/"): - name = ref.removeprefix("#/components/schemas/") - components = payload.get("components") - if not isinstance(components, dict): - return None - schemas = components.get("schemas") - if not isinstance(schemas, dict): - return None - resolved = schemas.get(name) - return resolved if isinstance(resolved, dict) else None - - return schema - - -def _request_body_schema(request_body: object) -> object | None: - if not isinstance(request_body, dict): - return None - content = request_body.get("content") - if not isinstance(content, dict): - return None - media_type = content.get("application/json") - if not isinstance(media_type, dict): - return None - return media_type.get("schema") - - -def _query_parameters_from_schema(schema: dict[str, object]) -> list[dict[str, object]]: - properties = schema.get("properties") - if not isinstance(properties, dict): - return [] - - required = schema.get("required") - required_names = set(required) if isinstance(required, list) else set() - parameters: list[dict[str, object]] = [] - - for name, property_schema in sorted(properties.items()): - if not isinstance(name, str) or not isinstance(property_schema, dict): - continue - schema_copy = dict(property_schema) - description = schema_copy.get("description") - parameter: dict[str, object] = { - "name": name, - "in": "query", - "required": name in required_names, - "schema": schema_copy, - } - if isinstance(description, str): - parameter["description"] = description - parameters.append(parameter) - - return parameters - - -def _move_get_request_bodies_to_query_parameters(payload: dict[str, object]) -> dict[str, object]: - """Represent GET request bodies as query parameters in exported specs.""" - - paths = payload.get("paths") - if not isinstance(paths, dict): - return payload - - for path_item in paths.values(): - if not isinstance(path_item, dict): - continue - operation = path_item.get("get") - if not isinstance(operation, dict) or "requestBody" not in operation: - continue - - schema = _resolve_component_schema(payload, _request_body_schema(operation.get("requestBody"))) - existing_parameters = operation.get("parameters") - parameters = list(existing_parameters) if isinstance(existing_parameters, list) else [] - existing_query_names = { - parameter.get("name") - for parameter in parameters - if isinstance(parameter, dict) and parameter.get("in") == "query" - } - - if schema is not None: - for parameter in _query_parameters_from_schema(schema): - if parameter["name"] not in existing_query_names: - parameters.append(parameter) - - if parameters: - operation["parameters"] = parameters - operation.pop("requestBody", None) - - return payload - - def _deduplicate_operation_ids(payload: dict[str, object]) -> dict[str, object]: """Make operationId values unique while preserving already-unique IDs.""" @@ -497,7 +404,6 @@ def generate_specs(output_dir: Path) -> list[Path]: if not isinstance(payload, dict): raise RuntimeError(f"unexpected response payload for {target.route}") payload = _merge_registered_schemas(payload, target.namespace) - payload = _move_get_request_bodies_to_query_parameters(payload) payload = _deduplicate_operation_ids(payload) payload = drop_null_values(payload) payload = sort_openapi_arrays(payload) diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index c0d9fb653a3..cada682d092 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -283,9 +283,9 @@ Check if activation token is valid | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| email | query | | No | | +| email | query | | No | string | | token | query | | Yes | string | -| workspace_id | query | | No | | +| workspace_id | query | | No | string | #### Responses @@ -570,13 +570,13 @@ Get list of applications with pagination and filtering | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| creator_ids | query | Filter by creator account IDs | No | | -| is_created_by_me | query | Filter by creator | No | | +| creator_ids | query | Filter by creator account IDs | No | [ string ] | +| is_created_by_me | query | Filter by creator | No | boolean | | limit | query | Page size (1-100) | No | integer,
**Default:** 20 | | mode | query | App mode filter | No | string,
**Available values:** "advanced-chat", "agent", "agent-chat", "all", "channel", "chat", "completion", "workflow",
**Default:** all | -| name | query | Filter by app name | No | | +| name | query | Filter by app name | No | string | | page | query | Page number (1-99999) | No | integer,
**Default:** 1 | -| tag_ids | query | Filter by tag IDs | No | | +| tag_ids | query | Filter by tag IDs | No | [ string ] | #### Responses @@ -1406,12 +1406,12 @@ Get chat conversations with pagination, filtering and summary | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | annotation_status | query | Annotation status filter | No | string,
**Available values:** "all", "annotated", "not_annotated",
**Default:** all | -| end | query | End date (YYYY-MM-DD HH:MM) | No | | -| keyword | query | Search keyword | No | | +| end | query | End date (YYYY-MM-DD HH:MM) | No | string | +| keyword | query | Search keyword | No | string | | limit | query | Page size (1-100) | No | integer,
**Default:** 20 | | page | query | Page number | No | integer,
**Default:** 1 | | sort_by | query | Sort field and direction | No | string,
**Available values:** "-created_at", "-updated_at", "created_at", "updated_at",
**Default:** -updated_at | -| start | query | Start date (YYYY-MM-DD HH:MM) | No | | +| start | query | Start date (YYYY-MM-DD HH:MM) | No | string | #### Responses @@ -1465,7 +1465,7 @@ Get chat messages for a conversation with pagination | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | conversation_id | query | Conversation ID | Yes | string | -| first_id | query | First message ID for pagination | No | | +| first_id | query | First message ID for pagination | No | string | | limit | query | Number of messages to return (1-100) | No | integer,
**Default:** 20 | #### Responses @@ -1517,11 +1517,11 @@ Get completion conversations with pagination and filtering | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | annotation_status | query | Annotation status filter | No | string,
**Available values:** "all", "annotated", "not_annotated",
**Default:** all | -| end | query | End date (YYYY-MM-DD HH:MM) | No | | -| keyword | query | Search keyword | No | | +| end | query | End date (YYYY-MM-DD HH:MM) | No | string | +| keyword | query | Search keyword | No | string | | limit | query | Page size (1-100) | No | integer,
**Default:** 20 | | page | query | Page number | No | integer,
**Default:** 1 | -| start | query | Start date (YYYY-MM-DD HH:MM) | No | | +| start | query | Start date (YYYY-MM-DD HH:MM) | No | string | #### Responses @@ -1683,7 +1683,7 @@ Export application configuration as DSL | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID to export | Yes | string | | include_secret | query | Include secrets in export | No | boolean | -| workflow_id | query | Specific workflow ID to export | No | | +| workflow_id | query | Specific workflow ID to export | No | string | #### Responses @@ -1723,12 +1723,12 @@ Export user feedback data for Google Sheets | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -| end_date | query | End date (YYYY-MM-DD) | No | | +| end_date | query | End date (YYYY-MM-DD) | No | string | | format | query | Export format | No | string,
**Available values:** "csv", "json",
**Default:** csv | -| from_source | query | Filter by feedback source | No | | -| has_comment | query | Only include feedback with comments | No | | -| rating | query | Filter by rating | No | | -| start_date | query | Start date (YYYY-MM-DD) | No | | +| from_source | query | Filter by feedback source | No | string,
**Available values:** "admin", "user" | +| has_comment | query | Only include feedback with comments | No | boolean | +| rating | query | Filter by rating | No | string,
**Available values:** "dislike", "like" | +| start_date | query | Start date (YYYY-MM-DD) | No | string | #### Responses @@ -1968,8 +1968,8 @@ Get average response time statistics for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -| end | query | End date (YYYY-MM-DD HH:MM) | No | | -| start | query | Start date (YYYY-MM-DD HH:MM) | No | | +| end | query | End date (YYYY-MM-DD HH:MM) | No | string | +| start | query | Start date (YYYY-MM-DD HH:MM) | No | string | #### Responses @@ -1985,8 +1985,8 @@ Get average session interaction statistics for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -| end | query | End date (YYYY-MM-DD HH:MM) | No | | -| start | query | Start date (YYYY-MM-DD HH:MM) | No | | +| end | query | End date (YYYY-MM-DD HH:MM) | No | string | +| start | query | Start date (YYYY-MM-DD HH:MM) | No | string | #### Responses @@ -2002,8 +2002,8 @@ Get daily conversation statistics for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -| end | query | End date (YYYY-MM-DD HH:MM) | No | | -| start | query | Start date (YYYY-MM-DD HH:MM) | No | | +| end | query | End date (YYYY-MM-DD HH:MM) | No | string | +| start | query | Start date (YYYY-MM-DD HH:MM) | No | string | #### Responses @@ -2019,8 +2019,8 @@ Get daily terminal/end-user statistics for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -| end | query | End date (YYYY-MM-DD HH:MM) | No | | -| start | query | Start date (YYYY-MM-DD HH:MM) | No | | +| end | query | End date (YYYY-MM-DD HH:MM) | No | string | +| start | query | Start date (YYYY-MM-DD HH:MM) | No | string | #### Responses @@ -2036,8 +2036,8 @@ Get daily message statistics for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -| end | query | End date (YYYY-MM-DD HH:MM) | No | | -| start | query | Start date (YYYY-MM-DD HH:MM) | No | | +| end | query | End date (YYYY-MM-DD HH:MM) | No | string | +| start | query | Start date (YYYY-MM-DD HH:MM) | No | string | #### Responses @@ -2053,8 +2053,8 @@ Get daily token cost statistics for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -| end | query | End date (YYYY-MM-DD HH:MM) | No | | -| start | query | Start date (YYYY-MM-DD HH:MM) | No | | +| end | query | End date (YYYY-MM-DD HH:MM) | No | string | +| start | query | Start date (YYYY-MM-DD HH:MM) | No | string | #### Responses @@ -2070,8 +2070,8 @@ Get tokens per second statistics for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -| end | query | End date (YYYY-MM-DD HH:MM) | No | | -| start | query | Start date (YYYY-MM-DD HH:MM) | No | | +| end | query | End date (YYYY-MM-DD HH:MM) | No | string | +| start | query | Start date (YYYY-MM-DD HH:MM) | No | string | #### Responses @@ -2087,8 +2087,8 @@ Get user satisfaction rate statistics for an application | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -| end | query | End date (YYYY-MM-DD HH:MM) | No | | -| start | query | Start date (YYYY-MM-DD HH:MM) | No | | +| end | query | End date (YYYY-MM-DD HH:MM) | No | string | +| start | query | Start date (YYYY-MM-DD HH:MM) | No | string | #### Responses @@ -2309,15 +2309,15 @@ Get workflow application execution logs | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -| created_at__after | query | Filter logs created after this timestamp | No | | -| created_at__before | query | Filter logs created before this timestamp | No | | -| created_by_account | query | Filter by account | No | | -| created_by_end_user_session_id | query | Filter by end user session ID | No | | +| created_at__after | query | Filter logs created after this timestamp | No | dateTime | +| created_at__before | query | Filter logs created before this timestamp | No | dateTime | +| created_by_account | query | Filter by account | No | string | +| created_by_end_user_session_id | query | Filter by end user session ID | No | string | | detail | query | Whether to return detailed logs | No | boolean | -| keyword | query | Search keyword for filtering logs | No | | +| keyword | query | Search keyword for filtering logs | No | string | | limit | query | Number of items per page (1-100) | No | integer,
**Default:** 20 | | page | query | Page number (1-99999) | No | integer,
**Default:** 1 | -| status | query | Execution status filter (succeeded, failed, stopped, partial-succeeded) | No | | +| status | query | Execution status filter (succeeded, failed, stopped, partial-succeeded) | No | string,
**Available values:** "failed", "partial-succeeded", "paused", "running", "scheduled", "stopped", "succeeded" | #### Responses @@ -2335,15 +2335,15 @@ Get workflow archived execution logs | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -| created_at__after | query | Filter logs created after this timestamp | No | | -| created_at__before | query | Filter logs created before this timestamp | No | | -| created_by_account | query | Filter by account | No | | -| created_by_end_user_session_id | query | Filter by end user session ID | No | | +| created_at__after | query | Filter logs created after this timestamp | No | dateTime | +| created_at__before | query | Filter logs created before this timestamp | No | dateTime | +| created_by_account | query | Filter by account | No | string | +| created_by_end_user_session_id | query | Filter by end user session ID | No | string | | detail | query | Whether to return detailed logs | No | boolean | -| keyword | query | Search keyword for filtering logs | No | | +| keyword | query | Search keyword for filtering logs | No | string | | limit | query | Number of items per page (1-100) | No | integer,
**Default:** 20 | | page | query | Page number (1-99999) | No | integer,
**Default:** 1 | -| status | query | Execution status filter (succeeded, failed, stopped, partial-succeeded) | No | | +| status | query | Execution status filter (succeeded, failed, stopped, partial-succeeded) | No | string,
**Available values:** "failed", "partial-succeeded", "paused", "running", "scheduled", "stopped", "succeeded" | #### Responses @@ -2710,8 +2710,8 @@ Get workflow average app interaction statistics | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -| end | query | End date and time (YYYY-MM-DD HH:MM) | No | | -| start | query | Start date and time (YYYY-MM-DD HH:MM) | No | | +| end | query | End date and time (YYYY-MM-DD HH:MM) | No | string | +| start | query | Start date and time (YYYY-MM-DD HH:MM) | No | string | #### Responses @@ -2727,8 +2727,8 @@ Get workflow daily runs statistics | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -| end | query | End date and time (YYYY-MM-DD HH:MM) | No | | -| start | query | Start date and time (YYYY-MM-DD HH:MM) | No | | +| end | query | End date and time (YYYY-MM-DD HH:MM) | No | string | +| start | query | Start date and time (YYYY-MM-DD HH:MM) | No | string | #### Responses @@ -2744,8 +2744,8 @@ Get workflow daily terminals statistics | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -| end | query | End date and time (YYYY-MM-DD HH:MM) | No | | -| start | query | Start date and time (YYYY-MM-DD HH:MM) | No | | +| end | query | End date and time (YYYY-MM-DD HH:MM) | No | string | +| start | query | Start date and time (YYYY-MM-DD HH:MM) | No | string | #### Responses @@ -2761,8 +2761,8 @@ Get workflow daily token cost statistics | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -| end | query | End date and time (YYYY-MM-DD HH:MM) | No | | -| start | query | Start date and time (YYYY-MM-DD HH:MM) | No | | +| end | query | End date and time (YYYY-MM-DD HH:MM) | No | string | +| start | query | Start date and time (YYYY-MM-DD HH:MM) | No | string | #### Responses @@ -2783,7 +2783,7 @@ Get all published workflows for an application | limit | query | | No | integer,
**Default:** 10 | | named_only | query | | No | boolean | | page | query | | No | integer,
**Default:** 1 | -| user_id | query | | No | | +| user_id | query | | No | string | #### Responses @@ -2819,7 +2819,7 @@ Get default block configuration by type | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | | block_type | path | Block type | Yes | string | -| q | query | | No | | +| q | query | | No | string | #### Responses @@ -3467,8 +3467,8 @@ Get draft workflow variables | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | Application ID | Yes | string | -| limit | query | Number of items per page (1-100) | No | string | -| page | query | Page number (1-100000) | No | string | +| limit | query | Items per page | No | integer,
**Default:** 20 | +| page | query | Page number | No | integer,
**Default:** 1 | #### Responses @@ -3667,9 +3667,7 @@ Full value for one declared output of a published run. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| credential_id | query | | No | | -| datasource_type | query | | Yes | string | -| inputs | query | | Yes | object | +| node_id | query | | Yes | string | | app_id | path | | Yes | string | #### Responses @@ -5762,9 +5760,9 @@ Request body: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| last_id | query | | No | | +| last_id | query | | No | string | | limit | query | | No | integer,
**Default:** 20 | -| pinned | query | | No | | +| pinned | query | | No | boolean | | installed_app_id | path | | Yes | string | #### Responses @@ -5841,7 +5839,7 @@ Request body: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | conversation_id | query | Conversation UUID | Yes | string | -| first_id | query | First message ID for pagination | No | | +| first_id | query | First message ID for pagination | No | string | | limit | query | Number of messages to return (1-100) | No | integer,
**Default:** 20 | | installed_app_id | path | | Yes | string | @@ -5935,7 +5933,7 @@ Request body: | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| last_id | query | | No | | +| last_id | query | | No | string | | limit | query | | No | integer,
**Default:** 20 | | installed_app_id | path | | Yes | string | @@ -8155,7 +8153,7 @@ Get website crawl status | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | job_id | path | Crawl job ID | Yes | string | -| provider | query | Crawl provider (firecrawl/watercrawl/jinareader) | No | string | +| provider | query | Crawl provider (firecrawl/watercrawl/jinareader) | Yes | string,
**Available values:** "firecrawl", "jinareader", "watercrawl" | #### Responses @@ -8267,12 +8265,12 @@ Get list of available agent providers | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| creators | query | Filter by creator account IDs | No | | -| is_published | query | Filter by published status | No | | -| keyword | query | | No | | +| creators | query | Filter by creator account IDs | No | [ string ] | +| is_published | query | Filter by published status | No | boolean | +| keyword | query | | No | string | | limit | query | | No | integer,
**Default:** 20 | | page | query | | No | integer,
**Default:** 1 | -| tag_ids | query | Filter by tag IDs | No | | +| tag_ids | query | Filter by tag IDs | No | [ string ] | #### Responses @@ -8448,7 +8446,7 @@ Increment snippet use count by 1 | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| model_type | query | | Yes | [ModelType](#modeltype) | +| model_type | query | Enum class for model type. | Yes | string,
**Available values:** "llm", "moderation", "rerank", "speech2text", "text-embedding", "tts" | #### Responses @@ -8747,7 +8745,7 @@ Update a plugin endpoint | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| model_type | query | | No | | +| model_type | query | Enum class for model type. | No | string,
**Available values:** "llm", "moderation", "rerank", "speech2text", "text-embedding", "tts" | #### Responses @@ -8792,7 +8790,7 @@ Update a plugin endpoint | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| credential_id | query | | No | | +| credential_id | query | | No | string | | provider | path | | Yes | string | #### Responses @@ -8952,10 +8950,10 @@ Update a plugin endpoint | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| config_from | query | | No | | -| credential_id | query | | No | | +| config_from | query | | No | string | +| credential_id | query | | No | string | | model | query | | Yes | string | -| model_type | query | | Yes | [ModelType](#modeltype) | +| model_type | query | Enum class for model type. | Yes | string,
**Available values:** "llm", "moderation", "rerank", "speech2text", "text-embedding", "tts" | | provider | path | | Yes | string | #### Responses @@ -9320,7 +9318,7 @@ Returns permission flags that control workspace features like member invitations | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | action | query | | Yes | string | -| credential_id | query | | No | | +| credential_id | query | | No | string | | parameter | query | | Yes | string | | plugin_id | query | | Yes | string | | provider | query | | Yes | string | @@ -11466,6 +11464,13 @@ Soft lifecycle state for Agent records. | page | integer | | Yes | | total | integer | | Yes | +#### AnnotationHitHistoryListQuery + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| limit | integer,
**Default:** 20 | Page size | No | +| page | integer,
**Default:** 1 | Page number | No | + #### AnnotationList | Name | Type | Description | Required | diff --git a/api/openapi/markdown/openapi-openapi.md b/api/openapi/markdown/openapi-openapi.md index f0394f6cc56..625699c0619 100644 --- a/api/openapi/markdown/openapi-openapi.md +++ b/api/openapi/markdown/openapi-openapi.md @@ -81,7 +81,7 @@ User-scoped operations | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | limit | query | | No | integer,
**Default:** 20 | -| mode | query | | No | string | +| mode | query | | No | string,
**Available values:** "advanced-chat", "agent", "agent-chat", "channel", "chat", "completion", "rag-pipeline", "workflow" | | name | query | | No | string | | page | query | | No | integer,
**Default:** 1 | | tag | query | | No | string | @@ -318,7 +318,7 @@ Upload a file to use as an input variable when running the app | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | limit | query | | No | integer,
**Default:** 20 | -| mode | query | | No | string | +| mode | query | | No | string,
**Available values:** "advanced-chat", "agent", "agent-chat", "channel", "chat", "completion", "rag-pipeline", "workflow" | | name | query | | No | string | | page | query | | No | integer,
**Default:** 1 | diff --git a/api/openapi/markdown/service-openapi.md b/api/openapi/markdown/service-openapi.md index 47350fa8479..5c0738062d6 100644 --- a/api/openapi/markdown/service-openapi.md +++ b/api/openapi/markdown/service-openapi.md @@ -264,7 +264,7 @@ Supports pagination using last_id and limit parameters. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| last_id | query | Last conversation ID for pagination | No | | +| last_id | query | Last conversation ID for pagination | No | string | | limit | query | Number of conversations to return | No | integer,
**Default:** 20 | | sort_by | query | Sort order for conversations | No | string,
**Available values:** "-created_at", "-updated_at", "created_at", "updated_at",
**Default:** -updated_at | @@ -327,9 +327,9 @@ Conversational variables are only available for chat applications. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | c_id | path | Conversation ID | Yes | string | -| last_id | query | Last variable ID for pagination | No | | +| last_id | query | Last variable ID for pagination | No | string | | limit | query | Number of variables to return | No | integer,
**Default:** 20 | -| variable_name | query | Filter variables by name | No | | +| variable_name | query | Filter variables by name | No | string | #### Responses @@ -1570,7 +1570,7 @@ Retrieves messages with pagination support using first_id. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | conversation_id | query | Conversation UUID | Yes | string | -| first_id | query | First message ID for pagination | No | | +| first_id | query | First message ID for pagination | No | string | | limit | query | Number of messages to return (1-100) | No | integer,
**Default:** 20 | #### Responses @@ -1722,14 +1722,14 @@ Returns paginated workflow execution logs with filtering options. | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | -| created_at__after | query | | No | | -| created_at__before | query | | No | | -| created_by_account | query | | No | | -| created_by_end_user_session_id | query | | No | | -| keyword | query | | No | | +| created_at__after | query | | No | string | +| created_at__before | query | | No | string | +| created_by_account | query | | No | string | +| created_by_end_user_session_id | query | | No | string | +| keyword | query | | No | string | | limit | query | | No | integer,
**Default:** 20 | | page | query | | No | integer,
**Default:** 1 | -| status | query | | No | | +| status | query | | No | string,
**Available values:** "failed", "stopped", "succeeded" | #### Responses diff --git a/api/tests/unit_tests/commands/test_generate_swagger_specs.py b/api/tests/unit_tests/commands/test_generate_swagger_specs.py index bfc98f5f517..03af7643f3c 100644 --- a/api/tests/unit_tests/commands/test_generate_swagger_specs.py +++ b/api/tests/unit_tests/commands/test_generate_swagger_specs.py @@ -95,7 +95,7 @@ def test_generate_specs_writes_unique_operation_ids(tmp_path): assert len(operation_ids) == len(set(operation_ids)) -def test_generate_specs_moves_get_request_bodies_to_query_parameters(tmp_path): +def test_generate_specs_writes_get_operations_without_request_bodies(tmp_path): module = _load_generate_swagger_specs_module() written_paths = module.generate_specs(tmp_path) diff --git a/api/tests/unit_tests/controllers/common/test_schema.py b/api/tests/unit_tests/controllers/common/test_schema.py index 9cd83ad54db..b8d99327872 100644 --- a/api/tests/unit_tests/controllers/common/test_schema.py +++ b/api/tests/unit_tests/controllers/common/test_schema.py @@ -1,4 +1,5 @@ import sys +from datetime import datetime from enum import StrEnum from typing import Literal from unittest.mock import MagicMock, patch @@ -43,6 +44,8 @@ class QueryModel(BaseModel): page: int = Field(default=1, ge=1, le=100, description="Page number") keyword: str | None = Field(default=None, min_length=1, max_length=50, description="Search keyword") status: Literal["active", "inactive"] | None = Field(default=None, description="Status filter") + enum_status: StatusEnum | None = Field(default=None, description="Enum status filter") + created_at: datetime | None = Field(default=None, description="Creation time") app_id: str = Field(..., alias="appId", description="Application ID") tag_ids: list[str] = Field(default_factory=list, min_length=1, max_length=3, description="Tag IDs") ambiguous: int | str | None = Field(default=None, description="Ambiguous query parameter") @@ -303,6 +306,20 @@ def test_query_params_from_model_builds_flask_restx_doc_params(): "type": "string", "enum": ["active", "inactive"], } + assert params["enum_status"] == { + "in": "query", + "required": False, + "description": "Enum status filter", + "type": "string", + "enum": ["active", "inactive"], + } + assert params["created_at"] == { + "in": "query", + "required": False, + "description": "Creation time", + "type": "string", + "format": "date-time", + } assert params["appId"] == { "in": "query", "required": True, diff --git a/api/tests/unit_tests/controllers/test_swagger.py b/api/tests/unit_tests/controllers/test_swagger.py index fae4268210d..8ad590c4dd0 100644 --- a/api/tests/unit_tests/controllers/test_swagger.py +++ b/api/tests/unit_tests/controllers/test_swagger.py @@ -1,5 +1,7 @@ """OpenAPI JSON rendering tests for Flask-RESTX API blueprints.""" +from collections.abc import Iterator + import pytest from flask import Flask @@ -31,6 +33,17 @@ def _parameters_by_name(operation: dict[str, object]) -> dict[str, dict[str, obj return result +def _get_operations(payload: dict[str, object]) -> Iterator[tuple[str, dict[str, object]]]: + paths = payload["paths"] + assert isinstance(paths, dict) + for path, path_item in paths.items(): + if not isinstance(path, str) or not isinstance(path_item, dict): + continue + operation = path_item.get("get") + if isinstance(operation, dict): + yield path, operation + + def _multipart_form_schema(operation: dict[str, object]) -> dict[str, object]: request_body = operation.get("requestBody") assert isinstance(request_body, dict) @@ -93,6 +106,8 @@ def test_openapi_json_endpoints_render(monkeypatch: pytest.MonkeyPatch): assert isinstance(payload["components"]["schemas"], dict) missing_refs = _schema_refs(payload) - set(payload["components"]["schemas"]) assert not missing_refs + get_request_body_paths = [path for path, operation in _get_operations(payload) if "requestBody" in operation] + assert not get_request_body_paths assert app.config["RESTX_INCLUDE_ALL_MODELS"] is True diff --git a/cli/src/api/apps.ts b/cli/src/api/apps.ts index 40bb5c80053..ea0e41c252f 100644 --- a/cli/src/api/apps.ts +++ b/cli/src/api/apps.ts @@ -1,4 +1,4 @@ -import type { AppDescribeResponse, AppListResponse } from '@dify/contracts/api/openapi/types.gen' +import type { AppDescribeResponse, AppListResponse, AppMode } from '@dify/contracts/api/openapi/types.gen' import type { OpenApiClient } from '@/http/orpc' import type { HttpClient } from '@/http/types' import { createOpenApiClient } from '@/http/orpc' @@ -7,7 +7,7 @@ export type ListQuery = { readonly workspaceId: string readonly page?: number readonly limit?: number - readonly mode?: string + readonly mode?: AppMode | '' readonly name?: string readonly tag?: string } diff --git a/cli/src/commands/get/app/index.ts b/cli/src/commands/get/app/index.ts index 9b60ab8e956..47594813704 100644 --- a/cli/src/commands/get/app/index.ts +++ b/cli/src/commands/get/app/index.ts @@ -8,6 +8,7 @@ import { runGetApp } from './run' const APP_MODE_VALUES: readonly AppMode[] = [ 'advanced-chat', + 'agent', 'agent-chat', 'channel', 'chat', @@ -56,7 +57,7 @@ export default class GetApp extends DifyCommand { allWorkspaces: flags['all-workspaces'], page: flags.page, limitRaw: flags.limit, - mode: flags.mode, + mode: flags.mode as AppMode | undefined, name: flags.name, tag: flags.tag, format, diff --git a/cli/src/commands/get/app/run.ts b/cli/src/commands/get/app/run.ts index 5c061d2548f..102cf066499 100644 --- a/cli/src/commands/get/app/run.ts +++ b/cli/src/commands/get/app/run.ts @@ -17,7 +17,7 @@ export type GetAppOptions = { readonly allWorkspaces?: boolean readonly page?: number readonly limitRaw?: string - readonly mode?: string + readonly mode?: AppMode readonly name?: string readonly tag?: string readonly format?: string diff --git a/packages/contracts/generated/api/console/activate/types.gen.ts b/packages/contracts/generated/api/console/activate/types.gen.ts index 5160896b75c..aff306f3211 100644 --- a/packages/contracts/generated/api/console/activate/types.gen.ts +++ b/packages/contracts/generated/api/console/activate/types.gen.ts @@ -53,9 +53,9 @@ export type GetActivateCheckData = { body?: never path?: never query: { - email?: string | null + email?: string token: string - workspace_id?: string | null + workspace_id?: string } url: '/activate/check' } diff --git a/packages/contracts/generated/api/console/activate/zod.gen.ts b/packages/contracts/generated/api/console/activate/zod.gen.ts index 4f877fcd7e0..00f85767b7c 100644 --- a/packages/contracts/generated/api/console/activate/zod.gen.ts +++ b/packages/contracts/generated/api/console/activate/zod.gen.ts @@ -46,9 +46,9 @@ export const zPostActivateBody = zActivatePayload export const zPostActivateResponse = zActivationResponse export const zGetActivateCheckQuery = z.object({ - email: z.string().nullish(), + email: z.string().optional(), token: z.string(), - workspace_id: z.string().nullish(), + workspace_id: z.string().optional(), }) /** diff --git a/packages/contracts/generated/api/console/apps/types.gen.ts b/packages/contracts/generated/api/console/apps/types.gen.ts index 42fd165ba05..08cb46ef329 100644 --- a/packages/contracts/generated/api/console/apps/types.gen.ts +++ b/packages/contracts/generated/api/console/apps/types.gen.ts @@ -602,15 +602,6 @@ export type WorkflowTriggerListResponse = { data: Array } -export type WorkflowExecutionStatus - = | 'failed' - | 'partial-succeeded' - | 'paused' - | 'running' - | 'scheduled' - | 'stopped' - | 'succeeded' - export type WorkflowAppLogPaginationResponse = { data: Array has_more: boolean @@ -1567,6 +1558,15 @@ export type AgentComposerImpactBindingResponse = { workflow_id: string } +export type WorkflowExecutionStatus + = | 'failed' + | 'partial-succeeded' + | 'paused' + | 'running' + | 'scheduled' + | 'stopped' + | 'succeeded' + export type NodeStatus = 'failed' | 'idle' | 'ready' | 'running' export type NodeOutputView = { @@ -2250,8 +2250,8 @@ export type GetAppsData = { body?: never path?: never query?: { - creator_ids?: Array | null - is_created_by_me?: boolean | null + creator_ids?: Array + is_created_by_me?: boolean limit?: number mode?: | 'advanced-chat' @@ -2262,9 +2262,9 @@ export type GetAppsData = { | 'chat' | 'completion' | 'workflow' - name?: string | null + name?: string page?: number - tag_ids?: Array | null + tag_ids?: Array } url: '/apps' } @@ -3288,12 +3288,12 @@ export type GetAppsByAppIdChatConversationsData = { } query?: { annotation_status?: 'all' | 'annotated' | 'not_annotated' - end?: string | null - keyword?: string | null + end?: string + keyword?: string limit?: number page?: number sort_by?: '-created_at' | '-updated_at' | 'created_at' | 'updated_at' - start?: string | null + start?: string } url: '/apps/{app_id}/chat-conversations' } @@ -3379,7 +3379,7 @@ export type GetAppsByAppIdChatMessagesData = { } query: { conversation_id: string - first_id?: string | null + first_id?: string limit?: number } url: '/apps/{app_id}/chat-messages' @@ -3451,11 +3451,11 @@ export type GetAppsByAppIdCompletionConversationsData = { } query?: { annotation_status?: 'all' | 'annotated' | 'not_annotated' - end?: string | null - keyword?: string | null + end?: string + keyword?: string limit?: number page?: number - start?: string | null + start?: string } url: '/apps/{app_id}/completion-conversations' } @@ -3658,7 +3658,7 @@ export type GetAppsByAppIdExportData = { } query?: { include_secret?: boolean - workflow_id?: string | null + workflow_id?: string } url: '/apps/{app_id}/export' } @@ -3712,12 +3712,12 @@ export type GetAppsByAppIdFeedbacksExportData = { app_id: string } query?: { - end_date?: string | null + end_date?: string format?: 'csv' | 'json' - from_source?: 'admin' | 'user' | null - has_comment?: boolean | null - rating?: 'dislike' | 'like' | null - start_date?: string | null + from_source?: 'admin' | 'user' + has_comment?: boolean + rating?: 'dislike' | 'like' + start_date?: string } url: '/apps/{app_id}/feedbacks/export' } @@ -4011,8 +4011,8 @@ export type GetAppsByAppIdStatisticsAverageResponseTimeData = { app_id: string } query?: { - end?: string | null - start?: string | null + end?: string + start?: string } url: '/apps/{app_id}/statistics/average-response-time' } @@ -4032,8 +4032,8 @@ export type GetAppsByAppIdStatisticsAverageSessionInteractionsData = { app_id: string } query?: { - end?: string | null - start?: string | null + end?: string + start?: string } url: '/apps/{app_id}/statistics/average-session-interactions' } @@ -4053,8 +4053,8 @@ export type GetAppsByAppIdStatisticsDailyConversationsData = { app_id: string } query?: { - end?: string | null - start?: string | null + end?: string + start?: string } url: '/apps/{app_id}/statistics/daily-conversations' } @@ -4074,8 +4074,8 @@ export type GetAppsByAppIdStatisticsDailyEndUsersData = { app_id: string } query?: { - end?: string | null - start?: string | null + end?: string + start?: string } url: '/apps/{app_id}/statistics/daily-end-users' } @@ -4095,8 +4095,8 @@ export type GetAppsByAppIdStatisticsDailyMessagesData = { app_id: string } query?: { - end?: string | null - start?: string | null + end?: string + start?: string } url: '/apps/{app_id}/statistics/daily-messages' } @@ -4116,8 +4116,8 @@ export type GetAppsByAppIdStatisticsTokenCostsData = { app_id: string } query?: { - end?: string | null - start?: string | null + end?: string + start?: string } url: '/apps/{app_id}/statistics/token-costs' } @@ -4137,8 +4137,8 @@ export type GetAppsByAppIdStatisticsTokensPerSecondData = { app_id: string } query?: { - end?: string | null - start?: string | null + end?: string + start?: string } url: '/apps/{app_id}/statistics/tokens-per-second' } @@ -4158,8 +4158,8 @@ export type GetAppsByAppIdStatisticsUserSatisfactionRateData = { app_id: string } query?: { - end?: string | null - start?: string | null + end?: string + start?: string } url: '/apps/{app_id}/statistics/user-satisfaction-rate' } @@ -4417,15 +4417,22 @@ export type GetAppsByAppIdWorkflowAppLogsData = { app_id: string } query?: { - created_at__after?: string | null - created_at__before?: string | null - created_by_account?: string | null - created_by_end_user_session_id?: string | null + created_at__after?: string + created_at__before?: string + created_by_account?: string + created_by_end_user_session_id?: string detail?: boolean - keyword?: string | null + keyword?: string limit?: number page?: number - status?: WorkflowExecutionStatus | null + status?: + | 'failed' + | 'partial-succeeded' + | 'paused' + | 'running' + | 'scheduled' + | 'stopped' + | 'succeeded' } url: '/apps/{app_id}/workflow-app-logs' } @@ -4443,15 +4450,22 @@ export type GetAppsByAppIdWorkflowArchivedLogsData = { app_id: string } query?: { - created_at__after?: string | null - created_at__before?: string | null - created_by_account?: string | null - created_by_end_user_session_id?: string | null + created_at__after?: string + created_at__before?: string + created_by_account?: string + created_by_end_user_session_id?: string detail?: boolean - keyword?: string | null + keyword?: string limit?: number page?: number - status?: WorkflowExecutionStatus | null + status?: + | 'failed' + | 'partial-succeeded' + | 'paused' + | 'running' + | 'scheduled' + | 'stopped' + | 'succeeded' } url: '/apps/{app_id}/workflow-archived-logs' } @@ -4838,8 +4852,8 @@ export type GetAppsByAppIdWorkflowStatisticsAverageAppInteractionsData = { app_id: string } query?: { - end?: string | null - start?: string | null + end?: string + start?: string } url: '/apps/{app_id}/workflow/statistics/average-app-interactions' } @@ -4859,8 +4873,8 @@ export type GetAppsByAppIdWorkflowStatisticsDailyConversationsData = { app_id: string } query?: { - end?: string | null - start?: string | null + end?: string + start?: string } url: '/apps/{app_id}/workflow/statistics/daily-conversations' } @@ -4880,8 +4894,8 @@ export type GetAppsByAppIdWorkflowStatisticsDailyTerminalsData = { app_id: string } query?: { - end?: string | null - start?: string | null + end?: string + start?: string } url: '/apps/{app_id}/workflow/statistics/daily-terminals' } @@ -4901,8 +4915,8 @@ export type GetAppsByAppIdWorkflowStatisticsTokenCostsData = { app_id: string } query?: { - end?: string | null - start?: string | null + end?: string + start?: string } url: '/apps/{app_id}/workflow/statistics/token-costs' } @@ -4925,7 +4939,7 @@ export type GetAppsByAppIdWorkflowsData = { limit?: number named_only?: boolean page?: number - user_id?: string | null + user_id?: string } url: '/apps/{app_id}/workflows' } @@ -4962,7 +4976,7 @@ export type GetAppsByAppIdWorkflowsDefaultWorkflowBlockConfigsByBlockTypeData = block_type: string } query?: { - q?: string | null + q?: string } url: '/apps/{app_id}/workflows/default-workflow-block-configs/{block_type}' } @@ -5721,8 +5735,8 @@ export type GetAppsByAppIdWorkflowsDraftVariablesData = { app_id: string } query?: { - limit?: string - page?: string + limit?: number + page?: number } url: '/apps/{app_id}/workflows/draft/variables' } @@ -5991,11 +6005,7 @@ export type GetAppsByAppIdWorkflowsTriggersWebhookData = { app_id: string } query: { - credential_id?: string | null - datasource_type: string - inputs: { - [key: string]: unknown - } + node_id: string } url: '/apps/{app_id}/workflows/triggers/webhook' } diff --git a/packages/contracts/generated/api/console/apps/zod.gen.ts b/packages/contracts/generated/api/console/apps/zod.gen.ts index 14a8d50f4e0..12d9dab3c77 100644 --- a/packages/contracts/generated/api/console/apps/zod.gen.ts +++ b/packages/contracts/generated/api/console/apps/zod.gen.ts @@ -394,19 +394,6 @@ export const zWorkflowTriggerListResponse = z.object({ data: z.array(zWorkflowTriggerResponse), }) -/** - * WorkflowExecutionStatus - */ -export const zWorkflowExecutionStatus = z.enum([ - 'failed', - 'partial-succeeded', - 'paused', - 'running', - 'scheduled', - 'stopped', - 'succeeded', -]) - /** * WorkflowRunExportResponse */ @@ -1430,6 +1417,19 @@ export const zAgentComposerImpactResponse = z.object({ workflow_node_count: z.int(), }) +/** + * WorkflowExecutionStatus + */ +export const zWorkflowExecutionStatus = z.enum([ + 'failed', + 'partial-succeeded', + 'paused', + 'running', + 'scheduled', + 'stopped', + 'succeeded', +]) + /** * NodeStatus * @@ -3010,8 +3010,8 @@ export const zWorkflowCommentDetailWritable = z.object({ }) export const zGetAppsQuery = z.object({ - creator_ids: z.array(z.string()).nullish(), - is_created_by_me: z.boolean().nullish(), + creator_ids: z.array(z.string()).optional(), + is_created_by_me: z.boolean().optional(), limit: z.int().gte(1).lte(100).optional().default(20), mode: z .enum([ @@ -3026,9 +3026,9 @@ export const zGetAppsQuery = z.object({ ]) .optional() .default('all'), - name: z.string().nullish(), + name: z.string().optional(), page: z.int().gte(1).lte(99999).optional().default(1), - tag_ids: z.array(z.string()).nullish(), + tag_ids: z.array(z.string()).optional(), }) /** @@ -3494,8 +3494,8 @@ export const zGetAppsByAppIdAnnotationsByAnnotationIdHitHistoriesPath = z.object }) export const zGetAppsByAppIdAnnotationsByAnnotationIdHitHistoriesQuery = z.object({ - limit: z.int().optional().default(20), - page: z.int().optional().default(1), + limit: z.int().gte(1).optional().default(20), + page: z.int().gte(1).optional().default(1), }) /** @@ -3530,15 +3530,15 @@ export const zGetAppsByAppIdChatConversationsPath = z.object({ export const zGetAppsByAppIdChatConversationsQuery = z.object({ annotation_status: z.enum(['all', 'annotated', 'not_annotated']).optional().default('all'), - end: z.string().nullish(), - keyword: z.string().nullish(), + end: z.string().optional(), + keyword: z.string().optional(), limit: z.int().gte(1).lte(100).optional().default(20), page: z.int().gte(1).lte(99999).optional().default(1), sort_by: z .enum(['-created_at', '-updated_at', 'created_at', 'updated_at']) .optional() .default('-updated_at'), - start: z.string().nullish(), + start: z.string().optional(), }) /** @@ -3572,7 +3572,7 @@ export const zGetAppsByAppIdChatMessagesPath = z.object({ export const zGetAppsByAppIdChatMessagesQuery = z.object({ conversation_id: z.string(), - first_id: z.string().nullish(), + first_id: z.string().optional(), limit: z.int().gte(1).lte(100).optional().default(20), }) @@ -3608,11 +3608,11 @@ export const zGetAppsByAppIdCompletionConversationsPath = z.object({ export const zGetAppsByAppIdCompletionConversationsQuery = z.object({ annotation_status: z.enum(['all', 'annotated', 'not_annotated']).optional().default('all'), - end: z.string().nullish(), - keyword: z.string().nullish(), + end: z.string().optional(), + keyword: z.string().optional(), limit: z.int().gte(1).lte(100).optional().default(20), page: z.int().gte(1).lte(99999).optional().default(1), - start: z.string().nullish(), + start: z.string().optional(), }) /** @@ -3703,7 +3703,7 @@ export const zGetAppsByAppIdExportPath = z.object({ export const zGetAppsByAppIdExportQuery = z.object({ include_secret: z.boolean().optional().default(false), - workflow_id: z.string().nullish(), + workflow_id: z.string().optional(), }) /** @@ -3727,12 +3727,12 @@ export const zGetAppsByAppIdFeedbacksExportPath = z.object({ }) export const zGetAppsByAppIdFeedbacksExportQuery = z.object({ - end_date: z.string().nullish(), + end_date: z.string().optional(), format: z.enum(['csv', 'json']).optional().default('csv'), - from_source: z.enum(['admin', 'user']).nullish(), - has_comment: z.boolean().nullish(), - rating: z.enum(['dislike', 'like']).nullish(), - start_date: z.string().nullish(), + from_source: z.enum(['admin', 'user']).optional(), + has_comment: z.boolean().optional(), + rating: z.enum(['dislike', 'like']).optional(), + start_date: z.string().optional(), }) /** @@ -3859,8 +3859,8 @@ export const zGetAppsByAppIdStatisticsAverageResponseTimePath = z.object({ }) export const zGetAppsByAppIdStatisticsAverageResponseTimeQuery = z.object({ - end: z.string().nullish(), - start: z.string().nullish(), + end: z.string().optional(), + start: z.string().optional(), }) /** @@ -3875,8 +3875,8 @@ export const zGetAppsByAppIdStatisticsAverageSessionInteractionsPath = z.object( }) export const zGetAppsByAppIdStatisticsAverageSessionInteractionsQuery = z.object({ - end: z.string().nullish(), - start: z.string().nullish(), + end: z.string().optional(), + start: z.string().optional(), }) /** @@ -3891,8 +3891,8 @@ export const zGetAppsByAppIdStatisticsDailyConversationsPath = z.object({ }) export const zGetAppsByAppIdStatisticsDailyConversationsQuery = z.object({ - end: z.string().nullish(), - start: z.string().nullish(), + end: z.string().optional(), + start: z.string().optional(), }) /** @@ -3907,8 +3907,8 @@ export const zGetAppsByAppIdStatisticsDailyEndUsersPath = z.object({ }) export const zGetAppsByAppIdStatisticsDailyEndUsersQuery = z.object({ - end: z.string().nullish(), - start: z.string().nullish(), + end: z.string().optional(), + start: z.string().optional(), }) /** @@ -3923,8 +3923,8 @@ export const zGetAppsByAppIdStatisticsDailyMessagesPath = z.object({ }) export const zGetAppsByAppIdStatisticsDailyMessagesQuery = z.object({ - end: z.string().nullish(), - start: z.string().nullish(), + end: z.string().optional(), + start: z.string().optional(), }) /** @@ -3939,8 +3939,8 @@ export const zGetAppsByAppIdStatisticsTokenCostsPath = z.object({ }) export const zGetAppsByAppIdStatisticsTokenCostsQuery = z.object({ - end: z.string().nullish(), - start: z.string().nullish(), + end: z.string().optional(), + start: z.string().optional(), }) /** @@ -3955,8 +3955,8 @@ export const zGetAppsByAppIdStatisticsTokensPerSecondPath = z.object({ }) export const zGetAppsByAppIdStatisticsTokensPerSecondQuery = z.object({ - end: z.string().nullish(), - start: z.string().nullish(), + end: z.string().optional(), + start: z.string().optional(), }) /** @@ -3971,8 +3971,8 @@ export const zGetAppsByAppIdStatisticsUserSatisfactionRatePath = z.object({ }) export const zGetAppsByAppIdStatisticsUserSatisfactionRateQuery = z.object({ - end: z.string().nullish(), - start: z.string().nullish(), + end: z.string().optional(), + start: z.string().optional(), }) /** @@ -4097,15 +4097,17 @@ export const zGetAppsByAppIdWorkflowAppLogsPath = z.object({ }) export const zGetAppsByAppIdWorkflowAppLogsQuery = z.object({ - created_at__after: z.iso.datetime().nullish(), - created_at__before: z.iso.datetime().nullish(), - created_by_account: z.string().nullish(), - created_by_end_user_session_id: z.string().nullish(), + created_at__after: z.iso.datetime().optional(), + created_at__before: z.iso.datetime().optional(), + created_by_account: z.string().optional(), + created_by_end_user_session_id: z.string().optional(), detail: z.boolean().optional().default(false), - keyword: z.string().nullish(), + keyword: z.string().optional(), limit: z.int().gte(1).lte(100).optional().default(20), page: z.int().gte(1).lte(99999).optional().default(1), - status: zWorkflowExecutionStatus.nullish(), + status: z + .enum(['failed', 'partial-succeeded', 'paused', 'running', 'scheduled', 'stopped', 'succeeded']) + .optional(), }) /** @@ -4118,15 +4120,17 @@ export const zGetAppsByAppIdWorkflowArchivedLogsPath = z.object({ }) export const zGetAppsByAppIdWorkflowArchivedLogsQuery = z.object({ - created_at__after: z.iso.datetime().nullish(), - created_at__before: z.iso.datetime().nullish(), - created_by_account: z.string().nullish(), - created_by_end_user_session_id: z.string().nullish(), + created_at__after: z.iso.datetime().optional(), + created_at__before: z.iso.datetime().optional(), + created_by_account: z.string().optional(), + created_by_end_user_session_id: z.string().optional(), detail: z.boolean().optional().default(false), - keyword: z.string().nullish(), + keyword: z.string().optional(), limit: z.int().gte(1).lte(100).optional().default(20), page: z.int().gte(1).lte(99999).optional().default(1), - status: zWorkflowExecutionStatus.nullish(), + status: z + .enum(['failed', 'partial-succeeded', 'paused', 'running', 'scheduled', 'stopped', 'succeeded']) + .optional(), }) /** @@ -4376,8 +4380,8 @@ export const zGetAppsByAppIdWorkflowStatisticsAverageAppInteractionsPath = z.obj }) export const zGetAppsByAppIdWorkflowStatisticsAverageAppInteractionsQuery = z.object({ - end: z.string().nullish(), - start: z.string().nullish(), + end: z.string().optional(), + start: z.string().optional(), }) /** @@ -4393,8 +4397,8 @@ export const zGetAppsByAppIdWorkflowStatisticsDailyConversationsPath = z.object( }) export const zGetAppsByAppIdWorkflowStatisticsDailyConversationsQuery = z.object({ - end: z.string().nullish(), - start: z.string().nullish(), + end: z.string().optional(), + start: z.string().optional(), }) /** @@ -4410,8 +4414,8 @@ export const zGetAppsByAppIdWorkflowStatisticsDailyTerminalsPath = z.object({ }) export const zGetAppsByAppIdWorkflowStatisticsDailyTerminalsQuery = z.object({ - end: z.string().nullish(), - start: z.string().nullish(), + end: z.string().optional(), + start: z.string().optional(), }) /** @@ -4427,8 +4431,8 @@ export const zGetAppsByAppIdWorkflowStatisticsTokenCostsPath = z.object({ }) export const zGetAppsByAppIdWorkflowStatisticsTokenCostsQuery = z.object({ - end: z.string().nullish(), - start: z.string().nullish(), + end: z.string().optional(), + start: z.string().optional(), }) /** @@ -4444,7 +4448,7 @@ export const zGetAppsByAppIdWorkflowsQuery = z.object({ limit: z.int().gte(1).lte(100).optional().default(10), named_only: z.boolean().optional().default(false), page: z.int().gte(1).lte(99999).optional().default(1), - user_id: z.string().nullish(), + user_id: z.string().optional(), }) /** @@ -4470,7 +4474,7 @@ export const zGetAppsByAppIdWorkflowsDefaultWorkflowBlockConfigsByBlockTypePath }) export const zGetAppsByAppIdWorkflowsDefaultWorkflowBlockConfigsByBlockTypeQuery = z.object({ - q: z.string().nullish(), + q: z.string().optional(), }) /** @@ -4880,8 +4884,8 @@ export const zGetAppsByAppIdWorkflowsDraftVariablesPath = z.object({ }) export const zGetAppsByAppIdWorkflowsDraftVariablesQuery = z.object({ - limit: z.string().optional(), - page: z.string().optional(), + limit: z.int().gte(1).lte(100).optional().default(20), + page: z.int().gte(1).lte(100000).optional().default(1), }) /** @@ -5007,9 +5011,7 @@ export const zGetAppsByAppIdWorkflowsTriggersWebhookPath = z.object({ }) export const zGetAppsByAppIdWorkflowsTriggersWebhookQuery = z.object({ - credential_id: z.string().nullish(), - datasource_type: z.string(), - inputs: z.record(z.string(), z.unknown()), + node_id: z.string(), }) /** diff --git a/packages/contracts/generated/api/console/installed-apps/types.gen.ts b/packages/contracts/generated/api/console/installed-apps/types.gen.ts index 355b24eeb6f..875c2492443 100644 --- a/packages/contracts/generated/api/console/installed-apps/types.gen.ts +++ b/packages/contracts/generated/api/console/installed-apps/types.gen.ts @@ -259,9 +259,9 @@ export type GetInstalledAppsByInstalledAppIdConversationsData = { installed_app_id: string } query?: { - last_id?: string | null + last_id?: string limit?: number - pinned?: boolean | null + pinned?: boolean } url: '/installed-apps/{installed_app_id}/conversations' } @@ -352,7 +352,7 @@ export type GetInstalledAppsByInstalledAppIdMessagesData = { } query: { conversation_id: string - first_id?: string | null + first_id?: string limit?: number } url: '/installed-apps/{installed_app_id}/messages' @@ -464,7 +464,7 @@ export type GetInstalledAppsByInstalledAppIdSavedMessagesData = { installed_app_id: string } query?: { - last_id?: string | null + last_id?: string limit?: number } url: '/installed-apps/{installed_app_id}/saved-messages' diff --git a/packages/contracts/generated/api/console/installed-apps/zod.gen.ts b/packages/contracts/generated/api/console/installed-apps/zod.gen.ts index bfa75c9d997..31d6189d9ce 100644 --- a/packages/contracts/generated/api/console/installed-apps/zod.gen.ts +++ b/packages/contracts/generated/api/console/installed-apps/zod.gen.ts @@ -234,9 +234,9 @@ export const zGetInstalledAppsByInstalledAppIdConversationsPath = z.object({ }) export const zGetInstalledAppsByInstalledAppIdConversationsQuery = z.object({ - last_id: z.string().nullish(), + last_id: z.string().optional(), limit: z.int().gte(1).lte(100).optional().default(20), - pinned: z.boolean().nullish(), + pinned: z.boolean().optional(), }) /** @@ -299,7 +299,7 @@ export const zGetInstalledAppsByInstalledAppIdMessagesPath = z.object({ export const zGetInstalledAppsByInstalledAppIdMessagesQuery = z.object({ conversation_id: z.string(), - first_id: z.string().nullish(), + first_id: z.string().optional(), limit: z.int().gte(1).lte(100).optional().default(20), }) @@ -373,7 +373,7 @@ export const zGetInstalledAppsByInstalledAppIdSavedMessagesPath = z.object({ }) export const zGetInstalledAppsByInstalledAppIdSavedMessagesQuery = z.object({ - last_id: z.string().nullish(), + last_id: z.string().optional(), limit: z.int().gte(1).lte(100).optional().default(20), }) diff --git a/packages/contracts/generated/api/console/website/orpc.gen.ts b/packages/contracts/generated/api/console/website/orpc.gen.ts index 3632d32121a..5ed1fcd8abf 100644 --- a/packages/contracts/generated/api/console/website/orpc.gen.ts +++ b/packages/contracts/generated/api/console/website/orpc.gen.ts @@ -32,7 +32,7 @@ export const get = oc .input( z.object({ params: zGetWebsiteCrawlStatusByJobIdPath, - query: zGetWebsiteCrawlStatusByJobIdQuery.optional(), + query: zGetWebsiteCrawlStatusByJobIdQuery, }), ) .output(zGetWebsiteCrawlStatusByJobIdResponse) diff --git a/packages/contracts/generated/api/console/website/types.gen.ts b/packages/contracts/generated/api/console/website/types.gen.ts index 8dba2c1370c..c8ea0de7b65 100644 --- a/packages/contracts/generated/api/console/website/types.gen.ts +++ b/packages/contracts/generated/api/console/website/types.gen.ts @@ -40,8 +40,8 @@ export type GetWebsiteCrawlStatusByJobIdData = { path: { job_id: string } - query?: { - provider?: string + query: { + provider: 'firecrawl' | 'jinareader' | 'watercrawl' } url: '/website/crawl/status/{job_id}' } diff --git a/packages/contracts/generated/api/console/website/zod.gen.ts b/packages/contracts/generated/api/console/website/zod.gen.ts index 13038ac0b1a..88f1d4a7c81 100644 --- a/packages/contracts/generated/api/console/website/zod.gen.ts +++ b/packages/contracts/generated/api/console/website/zod.gen.ts @@ -23,7 +23,7 @@ export const zGetWebsiteCrawlStatusByJobIdPath = z.object({ }) export const zGetWebsiteCrawlStatusByJobIdQuery = z.object({ - provider: z.string().optional(), + provider: z.enum(['firecrawl', 'jinareader', 'watercrawl']), }) /** diff --git a/packages/contracts/generated/api/console/workspaces/types.gen.ts b/packages/contracts/generated/api/console/workspaces/types.gen.ts index 335634279a5..5597046c629 100644 --- a/packages/contracts/generated/api/console/workspaces/types.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/types.gen.ts @@ -85,8 +85,6 @@ export type AccountWithRoleList = { accounts: Array } -export type ModelType = 'llm' | 'moderation' | 'rerank' | 'speech2text' | 'text-embedding' | 'tts' - export type ParserPostDefault = { model_settings: Array } @@ -643,6 +641,8 @@ export type Inner = { export type TenantAccountRole = 'admin' | 'dataset_operator' | 'editor' | 'normal' | 'owner' +export type ModelType = 'llm' | 'moderation' | 'rerank' | 'speech2text' | 'text-embedding' | 'tts' + export type LoadBalancingPayload = { configs?: Array<{ [key: string]: unknown @@ -752,12 +752,12 @@ export type GetWorkspacesCurrentCustomizedSnippetsData = { body?: never path?: never query?: { - creators?: Array | null - is_published?: boolean | null - keyword?: string | null + creators?: Array + is_published?: boolean + keyword?: string limit?: number page?: number - tag_ids?: Array | null + tag_ids?: Array } url: '/workspaces/current/customized-snippets' } @@ -1024,7 +1024,7 @@ export type GetWorkspacesCurrentDefaultModelData = { body?: never path?: never query: { - model_type: ModelType + model_type: 'llm' | 'moderation' | 'rerank' | 'speech2text' | 'text-embedding' | 'tts' } url: '/workspaces/current/default-model' } @@ -1391,7 +1391,7 @@ export type GetWorkspacesCurrentModelProvidersData = { body?: never path?: never query?: { - model_type?: ModelType | null + model_type?: 'llm' | 'moderation' | 'rerank' | 'speech2text' | 'text-embedding' | 'tts' } url: '/workspaces/current/model-providers' } @@ -1445,7 +1445,7 @@ export type GetWorkspacesCurrentModelProvidersByProviderCredentialsData = { provider: string } query?: { - credential_id?: string | null + credential_id?: string } url: '/workspaces/current/model-providers/{provider}/credentials' } @@ -1603,10 +1603,10 @@ export type GetWorkspacesCurrentModelProvidersByProviderModelsCredentialsData = provider: string } query: { - config_from?: string | null - credential_id?: string | null + config_from?: string + credential_id?: string model: string - model_type: ModelType + model_type: 'llm' | 'moderation' | 'rerank' | 'speech2text' | 'text-embedding' | 'tts' } url: '/workspaces/current/model-providers/{provider}/models/credentials' } @@ -2023,7 +2023,7 @@ export type GetWorkspacesCurrentPluginParametersDynamicOptionsData = { path?: never query: { action: string - credential_id?: string | null + credential_id?: string parameter: string plugin_id: string provider: string diff --git a/packages/contracts/generated/api/console/workspaces/zod.gen.ts b/packages/contracts/generated/api/console/workspaces/zod.gen.ts index e1eab1a76c3..3c46c777b28 100644 --- a/packages/contracts/generated/api/console/workspaces/zod.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/zod.gen.ts @@ -16,20 +16,6 @@ export const zSnippetImportPayload = z.object({ yaml_url: z.string().nullish(), }) -/** - * ModelType - * - * Enum class for model type. - */ -export const zModelType = z.enum([ - 'llm', - 'moderation', - 'rerank', - 'speech2text', - 'text-embedding', - 'tts', -]) - /** * SimpleResultResponse */ @@ -203,71 +189,6 @@ export const zParserCredentialValidate = z.object({ credentials: z.record(z.string(), z.unknown()), }) -/** - * ParserDeleteModels - */ -export const zParserDeleteModels = z.object({ - model: z.string(), - model_type: zModelType, -}) - -/** - * ParserDeleteCredential - */ -export const zParserDeleteCredential = z.object({ - credential_id: z.string(), - model: z.string(), - model_type: zModelType, -}) - -/** - * ParserCreateCredential - */ -export const zParserCreateCredential = z.object({ - credentials: z.record(z.string(), z.unknown()), - model: z.string(), - model_type: zModelType, - name: z.string().max(30).nullish(), -}) - -/** - * ParserUpdateCredential - */ -export const zParserUpdateCredential = z.object({ - credential_id: z.string(), - credentials: z.record(z.string(), z.unknown()), - model: z.string(), - model_type: zModelType, - name: z.string().max(30).nullish(), -}) - -/** - * ParserSwitch - */ -export const zParserSwitch = z.object({ - credential_id: z.string(), - model: z.string(), - model_type: zModelType, -}) - -/** - * ParserValidate - */ -export const zParserValidate = z.object({ - credentials: z.record(z.string(), z.unknown()), - model: z.string(), - model_type: zModelType, -}) - -/** - * LoadBalancingCredentialPayload - */ -export const zLoadBalancingCredentialPayload = z.object({ - credentials: z.record(z.string(), z.unknown()), - model: z.string(), - model_type: zModelType, -}) - /** * ParserPreferredProviderType */ @@ -655,6 +576,99 @@ export const zAccountWithRoleList = z.object({ accounts: z.array(zAccountWithRole), }) +/** + * TenantAccountRole + */ +export const zTenantAccountRole = z.enum(['admin', 'dataset_operator', 'editor', 'normal', 'owner']) + +/** + * MemberInvitePayload + */ +export const zMemberInvitePayload = z.object({ + emails: z.array(z.string()).optional(), + language: z.string().nullish(), + role: zTenantAccountRole, +}) + +/** + * ModelType + * + * Enum class for model type. + */ +export const zModelType = z.enum([ + 'llm', + 'moderation', + 'rerank', + 'speech2text', + 'text-embedding', + 'tts', +]) + +/** + * ParserDeleteModels + */ +export const zParserDeleteModels = z.object({ + model: z.string(), + model_type: zModelType, +}) + +/** + * ParserDeleteCredential + */ +export const zParserDeleteCredential = z.object({ + credential_id: z.string(), + model: z.string(), + model_type: zModelType, +}) + +/** + * ParserCreateCredential + */ +export const zParserCreateCredential = z.object({ + credentials: z.record(z.string(), z.unknown()), + model: z.string(), + model_type: zModelType, + name: z.string().max(30).nullish(), +}) + +/** + * ParserUpdateCredential + */ +export const zParserUpdateCredential = z.object({ + credential_id: z.string(), + credentials: z.record(z.string(), z.unknown()), + model: z.string(), + model_type: zModelType, + name: z.string().max(30).nullish(), +}) + +/** + * ParserSwitch + */ +export const zParserSwitch = z.object({ + credential_id: z.string(), + model: z.string(), + model_type: zModelType, +}) + +/** + * ParserValidate + */ +export const zParserValidate = z.object({ + credentials: z.record(z.string(), z.unknown()), + model: z.string(), + model_type: zModelType, +}) + +/** + * LoadBalancingCredentialPayload + */ +export const zLoadBalancingCredentialPayload = z.object({ + credentials: z.record(z.string(), z.unknown()), + model: z.string(), + model_type: zModelType, +}) + /** * Inner */ @@ -671,20 +685,6 @@ export const zParserPostDefault = z.object({ model_settings: z.array(zInner), }) -/** - * TenantAccountRole - */ -export const zTenantAccountRole = z.enum(['admin', 'dataset_operator', 'editor', 'normal', 'owner']) - -/** - * MemberInvitePayload - */ -export const zMemberInvitePayload = z.object({ - emails: z.array(z.string()).optional(), - language: z.string().nullish(), - role: zTenantAccountRole, -}) - /** * LoadBalancingPayload */ @@ -941,12 +941,12 @@ export const zGetWorkspacesCurrentAgentProvidersResponse = z.array( ) export const zGetWorkspacesCurrentCustomizedSnippetsQuery = z.object({ - creators: z.array(z.string()).nullish(), - is_published: z.boolean().nullish(), - keyword: z.string().nullish(), + creators: z.array(z.string()).optional(), + is_published: z.boolean().optional(), + keyword: z.string().optional(), limit: z.int().gte(1).lte(100).optional().default(20), page: z.int().gte(1).lte(99999).optional().default(1), - tag_ids: z.array(z.string()).nullish(), + tag_ids: z.array(z.string()).optional(), }) /** @@ -1049,7 +1049,7 @@ export const zPostWorkspacesCurrentCustomizedSnippetsBySnippetIdUseCountIncremen export const zGetWorkspacesCurrentDatasetOperatorsResponse = zAccountWithRoleList export const zGetWorkspacesCurrentDefaultModelQuery = z.object({ - model_type: zModelType, + model_type: z.enum(['llm', 'moderation', 'rerank', 'speech2text', 'text-embedding', 'tts']), }) /** @@ -1213,7 +1213,9 @@ export const zPutWorkspacesCurrentMembersByMemberIdUpdateRoleResponse = z.record ) export const zGetWorkspacesCurrentModelProvidersQuery = z.object({ - model_type: zModelType.nullish(), + model_type: z + .enum(['llm', 'moderation', 'rerank', 'speech2text', 'text-embedding', 'tts']) + .optional(), }) /** @@ -1250,7 +1252,7 @@ export const zGetWorkspacesCurrentModelProvidersByProviderCredentialsPath = z.ob }) export const zGetWorkspacesCurrentModelProvidersByProviderCredentialsQuery = z.object({ - credential_id: z.string().nullish(), + credential_id: z.string().optional(), }) /** @@ -1371,10 +1373,10 @@ export const zGetWorkspacesCurrentModelProvidersByProviderModelsCredentialsPath }) export const zGetWorkspacesCurrentModelProvidersByProviderModelsCredentialsQuery = z.object({ - config_from: z.string().nullish(), - credential_id: z.string().nullish(), + config_from: z.string().optional(), + credential_id: z.string().optional(), model: z.string(), - model_type: zModelType, + model_type: z.enum(['llm', 'moderation', 'rerank', 'speech2text', 'text-embedding', 'tts']), }) /** @@ -1641,7 +1643,7 @@ export const zGetWorkspacesCurrentPluginMarketplacePkgResponse = z.record(z.stri export const zGetWorkspacesCurrentPluginParametersDynamicOptionsQuery = z.object({ action: z.string(), - credential_id: z.string().nullish(), + credential_id: z.string().optional(), parameter: z.string(), plugin_id: z.string(), provider: z.string(), diff --git a/packages/contracts/generated/api/openapi/types.gen.ts b/packages/contracts/generated/api/openapi/types.gen.ts index 6d630639592..2dccca42e63 100644 --- a/packages/contracts/generated/api/openapi/types.gen.ts +++ b/packages/contracts/generated/api/openapi/types.gen.ts @@ -569,7 +569,15 @@ export type GetAppsData = { path?: never query: { limit?: number - mode?: string + mode?: + | 'advanced-chat' + | 'agent' + | 'agent-chat' + | 'channel' + | 'chat' + | 'completion' + | 'rag-pipeline' + | 'workflow' name?: string page?: number tag?: string @@ -891,7 +899,15 @@ export type GetPermittedExternalAppsData = { path?: never query?: { limit?: number - mode?: string + mode?: + | 'advanced-chat' + | 'agent' + | 'agent-chat' + | 'channel' + | 'chat' + | 'completion' + | 'rag-pipeline' + | 'workflow' name?: string page?: number } diff --git a/packages/contracts/generated/api/openapi/zod.gen.ts b/packages/contracts/generated/api/openapi/zod.gen.ts index 6379ffc62e2..e8077b321af 100644 --- a/packages/contracts/generated/api/openapi/zod.gen.ts +++ b/packages/contracts/generated/api/openapi/zod.gen.ts @@ -678,7 +678,18 @@ export const zDeleteAccountSessionsBySessionIdResponse = zRevokeResponse export const zGetAppsQuery = z.object({ limit: z.int().gte(1).lte(200).optional().default(20), - mode: z.string().optional(), + mode: z + .enum([ + 'advanced-chat', + 'agent', + 'agent-chat', + 'channel', + 'chat', + 'completion', + 'rag-pipeline', + 'workflow', + ]) + .optional(), name: z.string().max(200).optional(), page: z.int().gte(1).optional().default(1), tag: z.string().max(100).optional(), @@ -827,7 +838,18 @@ export const zPostOauthDeviceTokenResponse = z.record(z.string(), z.unknown()) export const zGetPermittedExternalAppsQuery = z.object({ limit: z.int().gte(1).lte(200).optional().default(20), - mode: z.string().optional(), + mode: z + .enum([ + 'advanced-chat', + 'agent', + 'agent-chat', + 'channel', + 'chat', + 'completion', + 'rag-pipeline', + 'workflow', + ]) + .optional(), name: z.string().max(200).optional(), page: z.int().gte(1).optional().default(1), }) diff --git a/packages/contracts/generated/api/service/types.gen.ts b/packages/contracts/generated/api/service/types.gen.ts index f57d90bcfb7..f3310887fff 100644 --- a/packages/contracts/generated/api/service/types.gen.ts +++ b/packages/contracts/generated/api/service/types.gen.ts @@ -1424,7 +1424,7 @@ export type GetConversationsData = { body?: never path?: never query?: { - last_id?: string | null + last_id?: string limit?: number sort_by?: '-created_at' | '-updated_at' | 'created_at' | 'updated_at' } @@ -1514,9 +1514,9 @@ export type GetConversationsByCIdVariablesData = { c_id: string } query?: { - last_id?: string | null + last_id?: string limit?: number - variable_name?: string | null + variable_name?: string } url: '/conversations/{c_id}/variables' } @@ -3236,7 +3236,7 @@ export type GetMessagesData = { path?: never query: { conversation_id: string - first_id?: string | null + first_id?: string limit?: number } url: '/messages' @@ -3466,14 +3466,14 @@ export type GetWorkflowsLogsData = { body?: never path?: never query?: { - created_at__after?: string | null - created_at__before?: string | null - created_by_account?: string | null - created_by_end_user_session_id?: string | null - keyword?: string | null + created_at__after?: string + created_at__before?: string + created_by_account?: string + created_by_end_user_session_id?: string + keyword?: string limit?: number page?: number - status?: 'failed' | 'stopped' | 'succeeded' | null + status?: 'failed' | 'stopped' | 'succeeded' } url: '/workflows/logs' } diff --git a/packages/contracts/generated/api/service/zod.gen.ts b/packages/contracts/generated/api/service/zod.gen.ts index 4575e95b718..93eae180806 100644 --- a/packages/contracts/generated/api/service/zod.gen.ts +++ b/packages/contracts/generated/api/service/zod.gen.ts @@ -1533,7 +1533,7 @@ export const zPostCompletionMessagesByTaskIdStopPath = z.object({ export const zPostCompletionMessagesByTaskIdStopResponse = zSimpleResultResponse export const zGetConversationsQuery = z.object({ - last_id: z.string().nullish(), + last_id: z.string().optional(), limit: z.int().gte(1).lte(100).optional().default(20), sort_by: z .enum(['-created_at', '-updated_at', 'created_at', 'updated_at']) @@ -1571,9 +1571,9 @@ export const zGetConversationsByCIdVariablesPath = z.object({ }) export const zGetConversationsByCIdVariablesQuery = z.object({ - last_id: z.string().nullish(), + last_id: z.string().optional(), limit: z.int().gte(1).lte(100).optional().default(20), - variable_name: z.string().min(1).max(255).nullish(), + variable_name: z.string().min(1).max(255).optional(), }) /** @@ -2226,7 +2226,7 @@ export const zGetInfoResponse = zAppInfoResponse export const zGetMessagesQuery = z.object({ conversation_id: z.string(), - first_id: z.string().nullish(), + first_id: z.string().optional(), limit: z.int().gte(1).lte(100).optional().default(20), }) @@ -2293,14 +2293,14 @@ export const zGetWorkflowByTaskIdEventsQuery = z.object({ export const zGetWorkflowByTaskIdEventsResponse = z.record(z.string(), z.unknown()) export const zGetWorkflowsLogsQuery = z.object({ - created_at__after: z.string().nullish(), - created_at__before: z.string().nullish(), - created_by_account: z.string().nullish(), - created_by_end_user_session_id: z.string().nullish(), - keyword: z.string().nullish(), + created_at__after: z.string().optional(), + created_at__before: z.string().optional(), + created_by_account: z.string().optional(), + created_by_end_user_session_id: z.string().optional(), + keyword: z.string().optional(), limit: z.int().gte(1).lte(100).optional().default(20), page: z.int().gte(1).lte(99999).optional().default(1), - status: z.enum(['failed', 'stopped', 'succeeded']).nullish(), + status: z.enum(['failed', 'stopped', 'succeeded']).optional(), }) /** diff --git a/packages/contracts/openapi-ts.api.config.ts b/packages/contracts/openapi-ts.api.config.ts index fc6ff748111..9aae8ab0042 100644 --- a/packages/contracts/openapi-ts.api.config.ts +++ b/packages/contracts/openapi-ts.api.config.ts @@ -431,21 +431,6 @@ const isNullSchema = (schema: SwaggerSchema) => { return schema.type === 'null' } -const resolveSchemaRef = ( - schema: SwaggerSchema | undefined, - schemas: Record, -): SwaggerSchema | undefined => { - const ref = schema?.$ref - if (!ref) - return schema - - const refName = schemaNameFromRef(ref) - if (!refName) - return schema - - return schemas[refName] ?? schema -} - const withoutNullableWrapper = (schema: SwaggerSchema | undefined): SwaggerSchema => { if (!schema) return {} @@ -461,95 +446,6 @@ const withoutNullableWrapper = (schema: SwaggerSchema | undefined): SwaggerSchem } } -const queryParameterFromSchema = ( - name: string, - schema: SwaggerSchema | undefined, - required: boolean, -): SwaggerParameter => { - const querySchema = withoutNullableWrapper(schema) - const parameter: SwaggerParameter = { - in: 'query', - name, - required, - schema: querySchema, - } - - if (querySchema.description) - parameter.description = querySchema.description - - return parameter -} - -const mergeQueryParameter = ( - parameters: SwaggerParameter[], - queryParameter: SwaggerParameter, -) => { - const existingIndex = parameters.findIndex((parameter) => { - return parameter.in === 'query' && parameter.name === queryParameter.name - }) - - if (existingIndex === -1) { - parameters.push(queryParameter) - return - } - - const existingParameter = parameters[existingIndex] - if (!existingParameter) { - parameters.push(queryParameter) - return - } - - parameters[existingIndex] = { - ...existingParameter, - ...queryParameter, - description: queryParameter.description ?? existingParameter.description, - required: Boolean(existingParameter.required) || Boolean(queryParameter.required), - } -} - -const normalizeGetBodyParameters = ( - operation: SwaggerOperation, - schemas: Record, -) => { - const bodyParameters: SwaggerParameter[] = [] - const normalizedParameters: SwaggerParameter[] = [] - - for (const parameter of operation.parameters ?? []) { - if (parameter.in === 'body') { - bodyParameters.push(parameter) - continue - } - - normalizedParameters.push(parameter) - } - - const requestBodySchema = getRequestBodySchema(operation) - if (requestBodySchema) { - bodyParameters.push({ - in: 'body', - name: 'payload', - required: Boolean(operation.requestBody?.required), - schema: requestBodySchema, - }) - } - - for (const parameter of bodyParameters) { - const schema = resolveSchemaRef(parameter.schema, schemas) - const properties = schema?.properties ?? {} - const required = new Set(schema?.required ?? []) - - for (const [name, propertySchema] of Object.entries(properties)) { - mergeQueryParameter( - normalizedParameters, - queryParameterFromSchema(name, propertySchema, required.has(name)), - ) - } - } - - operation.parameters = normalizedParameters - delete operation.requestBody -} - const normalizeResponses = (operation: SwaggerOperation) => { const responses = operation.responses ??= {} @@ -710,8 +606,6 @@ const normalizeOperations = (document: SwaggerDocument, surface: string) => { const swaggerOperation = operation as SwaggerOperation swaggerOperation.operationId = operationId(method, routePath) - if (method === 'get') - normalizeGetBodyParameters(swaggerOperation, schemas) normalizeResponses(swaggerOperation) const hasPossiblyInaccurateTypes = hasPossiblyInaccurateGeneratedContractTypes(swaggerOperation, schemas, { method, From e5d5931fec82ce9422638dd9d6a3b62733097dfc Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Fri, 12 Jun 2026 17:48:56 +0800 Subject: [PATCH 051/122] fix: align toast stack with Base UI (#37382) --- .../src/toast/__tests__/index.spec.tsx | 22 +++++++++ packages/dify-ui/src/toast/index.stories.tsx | 45 ++++++++++++++++++- packages/dify-ui/src/toast/index.tsx | 4 +- 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/packages/dify-ui/src/toast/__tests__/index.spec.tsx b/packages/dify-ui/src/toast/__tests__/index.spec.tsx index 0b06c6e1be8..7e9227e362e 100644 --- a/packages/dify-ui/src/toast/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/toast/__tests__/index.spec.tsx @@ -69,6 +69,28 @@ describe('@langgenius/dify-ui/toast', () => { dispatchToastMouseOut(viewport) }) + it('should clamp varying-height toasts to the frontmost stack height when collapsed', async () => { + const screen = await render() + + toast.info('Long background toast', { + description: 'This longer toast intentionally spans multiple lines so it would overflow the collapsed stack without matching the frontmost toast height.', + }) + toast.success('Short front toast', { + description: 'Short message.', + }) + + await expect.element(screen.getByText('Short front toast')).toBeInTheDocument() + await expect.element(screen.getByText('Long background toast')).toBeInTheDocument() + await expect.element(screen.getByRole('region', { name: 'Notifications' })).toHaveAttribute('aria-live', 'polite') + await expect.element(screen.getByRole('dialog', { name: 'Short front toast' })).toBeInTheDocument() + await expect.element(screen.getByRole('dialog', { name: 'Long background toast' })).toBeInTheDocument() + + const longToastContent = screen.getByText('Long background toast').element().closest('[class*="transition-opacity"]') + expect(longToastContent).toHaveAttribute('data-behind') + expect(longToastContent).toHaveClass('h-full') + expect(longToastContent?.parentElement).toHaveClass('h-full') + }) + it('should render a neutral toast when called directly', async () => { const screen = await render() diff --git a/packages/dify-ui/src/toast/index.stories.tsx b/packages/dify-ui/src/toast/index.stories.tsx index 772d0c456ce..b0ead00c9fd 100644 --- a/packages/dify-ui/src/toast/index.stories.tsx +++ b/packages/dify-ui/src/toast/index.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react-vite' import type { ReactNode } from 'react' +import { useRef } from 'react' import { toast, ToastHost } from '.' const buttonClassName = 'rounded-lg border border-divider-subtle bg-components-button-secondary-bg px-3 py-2 text-sm text-text-secondary shadow-xs transition-colors hover:bg-state-base-hover' @@ -117,6 +118,15 @@ const StackExamples = () => { }) } + const createVaryingHeightStack = () => { + toast.info('Long background toast', { + description: 'This longer toast intentionally spans multiple lines so the collapsed stack can be checked against the shorter frontmost toast height without panel overflow.', + }) + toast.success('Short front toast', { + description: 'Short message.', + }) + } + return ( { + ) } @@ -192,11 +205,14 @@ const PromiseExamples = () => { const ActionExamples = () => { const createActionToast = () => { - toast.warning('Project archived', { + let archivedToastId = '' + archivedToastId = toast.warning('Project archived', { description: 'You can restore it from workspace settings for the next 30 days.', + timeout: 10000, actionProps: { children: 'Undo', onClick: () => { + toast.dismiss(archivedToastId) toast.success('Project restored', { description: 'The workspace is active again.', }) @@ -233,6 +249,32 @@ const ActionExamples = () => { ) } +const DeduplicateExamples = () => { + const saveCountRef = useRef(0) + + const saveDraft = () => { + saveCountRef.current += 1 + toast.success('Draft saved', { + id: 'draft-save-status', + description: saveCountRef.current === 1 + ? 'Click again while this toast is visible to update the same mounted toast.' + : `Same toast updated ${saveCountRef.current} times.`, + }) + } + + return ( + + + + ) +} + const UpdateExamples = () => { const createUpdatableToast = () => { const toastId = toast.info('Import started', { @@ -292,6 +334,7 @@ const ToastDocsDemo = () => { +
diff --git a/packages/dify-ui/src/toast/index.tsx b/packages/dify-ui/src/toast/index.tsx index 269e18fa658..cab9841a327 100644 --- a/packages/dify-ui/src/toast/index.tsx +++ b/packages/dify-ui/src/toast/index.tsx @@ -166,12 +166,12 @@ function ToastCard({ 'after:pointer-events-auto after:absolute after:top-full after:left-0 after:h-[calc(var(--toast-gap)+1px)] after:w-full after:content-[\'\']', )} > -
+