diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 3d24c0d0..d813f40f 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -1496,6 +1496,32 @@ export const en = { resetFeaturesTip: 'Please reconfigure the [Conversation Features - File Upload] settings', logTitle: 'Description', range: 'Range', + body: 'BODY Parameter Example', + bodyRequestExample: `{ + "message": "user message content", + // string, required, the conversation content entered by the user; + + "conversation_id": "conversation_id", + // string, optional, session ID; for multi-turn conversations, pass the conversation_id from the previous response; omit on first request; + + "user_id": "user_id", + // string, optional, end-user identifier to distinguish memory and sessions across users; recommended to pass your business system user ID; + + "variables": {}, + // object, optional (requires application configuration to take effect); + + "stream": false, + // boolean, optional, whether to stream the response; defaults to false; when true, returns an SSE event stream; + + "thinking": false, + // boolean, optional, whether to enable deep thinking; defaults to false (requires application configuration when true); + + "files": [], + // array, optional, list of multimodal attachments (requires application configuration to take effect); + + "version":"app_release_id" + // string, optional, application version ID; specify a historical release version ID, or omit to use the currently active version; +}`, }, userMemory: { userMemory: 'User Memory', @@ -2239,6 +2265,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re addvariable: 'Chat Variables', addChatVariable: 'Add Chat Variable', editChatVariable: 'Edit Chat Variable', + invalidJSON: 'Invalid JSON format', config: { llm: { @@ -2341,6 +2368,11 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re "eq": 'Is', "ne": 'Is Not', }, + file: { + "empty": 'Not Exist', + "not_empty": 'Exists', + eq: 'All Are' + }, else_desc: 'Used to define the logic that should be executed when the if condition is not met.', unset: 'Condition Not Set', set: 'Set', @@ -2519,6 +2551,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re 'document-extractor.file_selector': 'File variable', 'list-operator.input_list': 'Input list', }, + checkListHasErrors: 'Please resolve all issues in the checklist before publishing', }, emotionEngine: { emotionEngineConfig: 'Emotion Engine Configuration', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index f5e3653a..fc846dcd 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -831,6 +831,32 @@ export const zh = { resetFeaturesTip: '请重新配置【对话功能-文件上传】功能', logTitle: '描述', range: '范围', + body: 'BODY 参数示例', + bodyRequestExample: `{ + "message": "用户消息内容", + // string,必填,用户输入的对话内容; + + "conversation_id": "conversation_id", + // string,可选,会话ID,多轮对话时传上一次返回的conversation_id,首次不传; + + "user_id": "user_id", + // string,可选,终端用户标识,用于区分不同用户的记忆和会话,建议传业务系统的用户ID; + + "variables": {}, + // object,可选(需要应用配置才支持生效); + + "stream": false, + // boolean,可选,是否流式返回;默认 false,true时返回SSE事件流; + + "thinking": false, + // boolean,可选,是否启用深度思考;默认 false(true时需要应用配置才支持生效); + + "files": [], + // array,可选,多模态附件列表(需要应用配置才支持生效); + + "version":"app_release_id" + //string,可选,应用版本ID;指定历史发布版本ID,不传则使用当前生效版本; +}`, }, table: { totalRecords: '共 {{total}} 条记录' @@ -2200,6 +2226,7 @@ export const zh = { addvariable: '会话变量', addChatVariable: '添加会话变量', editChatVariable: '编辑会话变量', + invalidJSON: 'JSON 格式不正确', config: { llm: { @@ -2302,6 +2329,11 @@ export const zh = { "eq": '是', "ne": '不是', }, + file: { + "empty": '不存在', + "not_empty": '存在', + eq: '全都是' + }, else_desc: '用于定义当 if 条件不满足时应执行的逻辑。', unset: '条件未设置', set: '已设置', @@ -2483,6 +2515,7 @@ export const zh = { 'document-extractor.file_selector': '文件变量', 'list-operator.input_list': '输入变量', }, + checkListHasErrors: '发布前确认检查清单中所有问题均已解决', }, emotionEngine: { emotionEngineConfig: '情感引擎配置', diff --git a/web/src/store/workflow.ts b/web/src/store/workflow.ts new file mode 100644 index 00000000..0999d35a --- /dev/null +++ b/web/src/store/workflow.ts @@ -0,0 +1,21 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-04-10 18:11:19 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-04-10 18:11:19 + */ +import { create } from 'zustand' +import type { NodeCheckResult } from '@/views/Workflow/components/CheckList' + +interface WorkflowState { + checkResults: Record + setCheckResults: (appId: string, results: NodeCheckResult[]) => void + getCheckResults: (appId: string) => NodeCheckResult[] +} + +export const useWorkflowStore = create((set, get) => ({ + checkResults: {}, + setCheckResults: (appId, results) => + set(state => ({ checkResults: { ...state.checkResults, [appId]: results } })), + getCheckResults: (appId) => get().checkResults[appId] ?? [], +})) diff --git a/web/src/styles/index.css b/web/src/styles/index.css index 7c8abb4a..66051085 100644 --- a/web/src/styles/index.css +++ b/web/src/styles/index.css @@ -420,4 +420,7 @@ body { .ant-picker-outlined:focus, .ant-picker-outlined:focus-within { box-shadow: none; +} +.ͼ1.cm-focused { + outline: none; } \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/Api.tsx b/web/src/views/ApplicationConfig/Api.tsx index 14e478dd..4fa19c3e 100644 --- a/web/src/views/ApplicationConfig/Api.tsx +++ b/web/src/views/ApplicationConfig/Api.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:29:29 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-26 15:31:36 + * @Last Modified time: 2026-04-10 18:09:56 */ import { type FC, useState, useRef, useEffect } from 'react'; import clsx from 'clsx'; @@ -18,6 +18,7 @@ import ApiKeyConfigModal from './components/ApiKeyConfigModal'; import { getApiKeyList, getApiKeyStats, deleteApiKey } from '@/api/apiKey'; import { maskApiKeys } from '@/utils/apiKeyReplacer' import RbCard from '@/components/RbCard/Card'; +import CodeMirrorEditor from '@/components/CodeMirrorEditor' /** * API configuration page component @@ -155,6 +156,21 @@ const Api: FC<{ application: Application | null }> = ({ application }) => { {t('common.copy')} + +
+ {t('application.body')} +
+ + + + + + ( diff --git a/web/src/views/ApplicationConfig/ReleasePage.tsx b/web/src/views/ApplicationConfig/ReleasePage.tsx index 3e516c88..ba573795 100644 --- a/web/src/views/ApplicationConfig/ReleasePage.tsx +++ b/web/src/views/ApplicationConfig/ReleasePage.tsx @@ -2,12 +2,13 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:29:41 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-26 15:24:41 + * @Last Modified time: 2026-04-10 17:02:07 */ import { type FC, useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import clsx from 'clsx'; import { Space, Input, Form, App, Flex } from 'antd'; +import copy from 'copy-to-clipboard'; import Tag, { type TagProps } from './components/Tag' import RbCard from '@/components/RbCard/Card' @@ -17,6 +18,7 @@ import ReleaseShareModal from './components/ReleaseShareModal' import AppSharingModal from './components/AppSharingModal' import type { Release, ReleaseModalRef, ReleaseShareModalRef, AppSharingModalRef } from './types' import type { Application } from '@/views/ApplicationManagement/types' +import { useWorkflowStore } from '@/store/workflow' import Empty from '@/components/Empty' import { formatDateTime } from '@/utils/format'; import Markdown from '@/components/Markdown' @@ -40,6 +42,7 @@ const heightClass = 'rb:max-h-[calc(100vh-140px)]' const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refresh}) => { const { t } = useTranslation(); const { message } = App.useApp() + const { getCheckResults } = useWorkflowStore() const releaseModalRef = useRef(null) const releaseShareModalRef = useRef(null) const appSharingModalRef = useRef(null) @@ -75,6 +78,10 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres if (!selectedVersion) return appExport(data.id, data.name, { release_id: selectedVersion.id}) } + const handleCopy = (id: string) => { + copy(id) + message.success(t('common.copySuccess')) + } return (
@@ -102,7 +109,7 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres } } className={clsx("rb:hover:shadow-[0px_2px_8px_0px_rgba(0,0,0,0.2)]! rb:cursor-pointer rb:bg-white", { - 'rb:border-[#171719]!': version.id === selectedVersion.id, + 'rb:border! rb:border-[#171719]!': version.id === selectedVersion.id, 'rb:border-[#DFE4ED] ': version.id !== selectedVersion.id })} headerType="borderless" @@ -140,13 +147,30 @@ const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refres releaseShareModalRef.current?.handleOpen()}>{t('application.share')} {data?.type !== 'multi_agent' && appSharingModalRef.current?.handleOpen()}>{t('application.sharing')}} } - releaseModalRef.current?.handleOpen()}>{t('application.release')} + { + if (data?.type === 'workflow') { + const errors = getCheckResults(data.id) + if (errors.length) { + message.error(t('workflow.checkListHasErrors')) + return + } + } + releaseModalRef.current?.handleOpen() + }}>{t('application.release')} {selectedVersion && {t('application.VersionInformation')} + + (ID: {selectedVersion.id} +
handleCopy(selectedVersion.id)} + >
+ ) +
+
} headerType="borderless" >
diff --git a/web/src/views/ApplicationConfig/components/ConfigHeader.tsx b/web/src/views/ApplicationConfig/components/ConfigHeader.tsx index b2e1e36b..d38a657a 100644 --- a/web/src/views/ApplicationConfig/components/ConfigHeader.tsx +++ b/web/src/views/ApplicationConfig/components/ConfigHeader.tsx @@ -207,7 +207,7 @@ const ConfigHeader: FC = ({ } extra={application?.type === 'workflow' && source !== 'sharing' && activeTab === 'arrangement' ? - +
{ const { t } = useTranslation() const { id } = useParams() - const [total, setTotal] = useState(0) return ( { headerClassName="rb:min-h-[54px]! rb:pt-0! rb:mb-0!" bodyClassName="rb:p-4! rb:pt-0! rb:pb-1! rb:h-[calc(100%-54px)]!" className="rb:h-full!" - extra={
{t('userMemory.totalRagMemory')}: {total}
} > url={getRagContentUrl} query={{ end_user_id: id }} column={1} gutter={0} - onTotalChange={setTotal} renderItem={(item, index) => (
{index !== 0 && } diff --git a/web/src/views/Workflow/components/AddChatVariable/ChatVariableModal.tsx b/web/src/views/Workflow/components/AddChatVariable/ChatVariableModal.tsx index ff773a9e..5666f3ab 100644 --- a/web/src/views/Workflow/components/AddChatVariable/ChatVariableModal.tsx +++ b/web/src/views/Workflow/components/AddChatVariable/ChatVariableModal.tsx @@ -5,7 +5,7 @@ * @Last Modified time: 2026-04-08 11:05:34 */ import { forwardRef, useImperativeHandle, useState, useRef, useMemo } from 'react'; -import { Form, Input, Select, InputNumber, Button, Row, Col, Flex, Spin } from 'antd'; +import { Form, Input, Select, InputNumber, Button, Row, Col, Flex } from 'antd'; import { PlusOutlined } from '@ant-design/icons'; import { useTranslation } from 'react-i18next'; @@ -124,7 +124,7 @@ const ChatVariableModal = forwardRef ) : ( - + { + if (!value) return Promise.resolve(); + try { JSON.parse(value); return Promise.resolve(); } + catch { return Promise.reject(t('workflow.invalidJSON')); } + } + } : {} + ]} + > {type === 'number' ? : type === 'boolean' diff --git a/web/src/views/Workflow/components/CheckList/index.tsx b/web/src/views/Workflow/components/CheckList/index.tsx index fe627e03..636ae9a9 100644 --- a/web/src/views/Workflow/components/CheckList/index.tsx +++ b/web/src/views/Workflow/components/CheckList/index.tsx @@ -1,4 +1,4 @@ -import { type FC, useState, useCallback, useEffect, useRef } from 'react' +import { useState, useCallback, useEffect, useRef, type FC } from 'react' import { Popover, Flex } from 'antd' import { WarningFilled } from '@ant-design/icons' import { useTranslation } from 'react-i18next' @@ -8,17 +8,19 @@ import type { WorkflowRef } from '@/views/ApplicationConfig/types' import { nodeLibrary } from '../../constant' import { getToolMethods } from '@/api/tools' import RbDrawer from '@/components/RbDrawer' +import { useWorkflowStore } from '@/store/workflow' interface CheckListProps { workflowRef: React.RefObject + appId: string } -interface CheckError { +export interface CheckError { key: string message: string } -interface NodeCheckResult { +export interface NodeCheckResult { id: string name: string type: string @@ -112,10 +114,67 @@ function validateNode(type: string, config: Record): CheckError[] { return errors } -const CheckList: FC = ({ workflowRef }) => { +export async function runCheckOnGraph( + graph: import('@antv/x6').Graph, + t: (key: string) => string +): Promise { + const nodes = graph.getNodes() + const edges = graph.getEdges() + const targetIds = new Set() + const childTargetIds = new Set() + edges.forEach(e => { + targetIds.add(e.getTargetCellId()) + const srcData = graph.getCellById(e.getSourceCellId())?.getData() + const tgtData = graph.getCellById(e.getTargetCellId())?.getData() + if (srcData?.cycle && tgtData?.cycle && srcData.cycle === tgtData.cycle) { + childTargetIds.add(e.getTargetCellId()) + } + }) + + const checked: NodeCheckResult[] = [] + for (const node of nodes) { + const data = node.getData() + if (!data || ['add-node', 'notes', 'cycle-start', 'break'].includes(data.type)) continue + + const errors: CheckError[] = [] + const isChildNode = !!data.cycle + const hasIncoming = isChildNode ? childTargetIds.has(node.id) : !['start', 'cycle-start'].includes(data.type) ? targetIds.has(node.id) : true + if (!hasIncoming) errors.push({ key: 'notConnected', message: t('workflow.notConnected') }) + + const configErrors = validateNode(data.type, data.config ?? {}) + configErrors.forEach(e => { + errors.push({ key: e.key, message: `${t(`workflow.checkListErrors.${e.key}`)} ${t('workflow.cannotBeEmpty')}`.trim() }) + }) + + if (data.type === 'tool') { + const toolId = data.config?.tool_id?.defaultValue ?? data.config?.tool_id + const toolParameters = data.config?.tool_parameters?.defaultValue ?? data.config?.tool_parameters ?? {} + if (toolId) { + try { + const methods = await getToolMethods(toolId) as Array<{ name: string; parameters: Array<{ name: string; required: boolean }> }> + const operation = toolParameters?.operation + const method = operation ? methods.find(m => m.name === operation) : methods[0] + if (method) { + method.parameters + .filter(p => p.required && (toolParameters[p.name] === undefined || toolParameters[p.name] === null || toolParameters[p.name] === '')) + .forEach(p => errors.push({ key: 'tool.tool_parameters', message: `${p.name} ${t('workflow.cannotBeEmpty')}` })) + } + } catch { /* ignore */ } + } + } + + if (errors.length) { + checked.push({ id: node.id, name: data.name || t(`workflow.${data.type}`), type: data.type, icon: nodeIconMap[data.type] ?? '', errors }) + } + } + return checked +} + +const CheckList: FC = ({ workflowRef, appId }) => { const { t } = useTranslation() const [open, setOpen] = useState(false) - const [results, setResults] = useState([]) + const { setCheckResults, getCheckResults } = useWorkflowStore() + const results = getCheckResults(appId) const timerRef = useRef>() const runCheck = useCallback(async () => { @@ -195,7 +254,7 @@ const CheckList: FC = ({ workflowRef }) => { const scheduleCheck = useCallback(() => { clearTimeout(timerRef.current) timerRef.current = setTimeout(async () => { - setResults(await runCheck()) + setCheckResults(appId, await runCheck()) }, 500) }, [runCheck]) @@ -211,7 +270,7 @@ const CheckList: FC = ({ workflowRef }) => { } }, [workflowRef.current?.graphRef?.current]) - const handleOpen = () => { +const handleOpen = () => { setOpen(true) } diff --git a/web/src/views/Workflow/components/PortClickHandler.tsx b/web/src/views/Workflow/components/PortClickHandler.tsx index b556ffab..cb3e16c4 100644 --- a/web/src/views/Workflow/components/PortClickHandler.tsx +++ b/web/src/views/Workflow/components/PortClickHandler.tsx @@ -328,7 +328,7 @@ const PortClickHandler: React.FC = ({ graph }) => { }; const content = ( - + {nodeLibrary.map((category) => { const sourceNodeData = sourceNode?.getData(); const isChildOfLoop = sourceNodeData?.cycle && graph?.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle && n.getData()?.type === 'loop'); diff --git a/web/src/views/Workflow/components/Properties/CaseList/index.tsx b/web/src/views/Workflow/components/Properties/CaseList/index.tsx index e1583ca0..40353f64 100644 --- a/web/src/views/Workflow/components/Properties/CaseList/index.tsx +++ b/web/src/views/Workflow/components/Properties/CaseList/index.tsx @@ -4,7 +4,7 @@ * @Last Modified by: ZhaoYing * @Last Modified time: 2026-03-25 15:23:45 */ -import { type FC } from 'react' +import { useMemo, type FC } from 'react' import clsx from 'clsx' import { useTranslation } from 'react-i18next'; import { Form, Button, Select, Space, Divider, InputNumber, type SelectProps, Flex, Row, Col } from 'antd' @@ -15,7 +15,7 @@ import Editor from '../../Editor' import { edgeAttrs, nodeWidth } from '../../../constant' import RbButton from '@/components/RbButton'; import RadioGroupBtn from '../RadioGroupBtn' -import { calcConditionNodeTotalHeight, getConditionNodeCasePortY } from '../../../utils' +import { calcConditionNodeTotalHeight, getConditionNodeCasePortY } from '../../../utils'; interface CaseListProps { value?: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; input_type?: string; }[] }>; @@ -49,6 +49,34 @@ const operatorsObj: { [key: string]: SelectProps['options'] } = { boolean: [ { value: 'eq', label: 'workflow.config.if-else.boolean.eq' }, { value: 'ne', label: 'workflow.config.if-else.boolean.ne' }, + ], + object: [ + { value: 'eq', label: 'workflow.config.if-else.boolean.eq' }, + { value: 'ne', label: 'workflow.config.if-else.boolean.ne' }, + { value: 'empty', label: 'workflow.config.if-else.empty' }, + { value: 'not_empty', label: 'workflow.config.if-else.not_empty' }, + ], + file: [ + { value: 'empty', label: 'workflow.config.if-else.file.empty' }, + { value: 'not_empty', label: 'workflow.config.if-else.file.not_empty' }, + ], + // TODO:包含、不包含、全都是 + 'array[file]': [ + { value: 'empty', label: 'workflow.config.if-else.empty' }, + { value: 'not_empty', label: 'workflow.config.if-else.not_empty' }, + // { value: 'eq', label: 'workflow.config.if-else.eq' }, + // { value: 'contains', label: 'workflow.config.if-else.contains' }, + // { value: 'not_contains', label: 'workflow.config.if-else.not_contains' }, + ], + 'array': [ + { value: 'contains', label: 'workflow.config.if-else.contains' }, + { value: 'not_contains', label: 'workflow.config.if-else.not_contains' }, + { value: 'empty', label: 'workflow.config.if-else.empty' }, + { value: 'not_empty', label: 'workflow.config.if-else.not_empty' }, + ], + 'array[object]': [ + { value: 'empty', label: 'workflow.config.if-else.empty' }, + { value: 'not_empty', label: 'workflow.config.if-else.not_empty' }, ] } @@ -247,6 +275,22 @@ const CaseList: FC = ({ form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'right'], undefined); }; + const filterNumberOptions = useMemo(() => { + const filterList: Suggestion[] = [] + options.forEach(vo => { + if (vo.children && vo.children?.length > 0) { + filterList.push({ + ...vo, + children: vo.children.filter(child => child.dataType === 'number') + }) + } else if (vo.dataType === 'number') { + filterList.push(vo) + } + }) + + return filterList + }, [options]) + return ( <> @@ -284,11 +328,15 @@ const CaseList: FC = ({ const currentCase = cases[caseIndex] || {}; const currentExpression = currentCase.expressions?.[conditionIndex] || {}; const currentOperator = currentExpression.operator; - const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty'; const leftFieldValue = currentExpression.left; const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue); const leftFieldType = leftFieldOption?.dataType; - const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || []; + const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty' || leftFieldType === 'file' || leftFieldType === 'array[object]' || leftFieldType === 'array[file]'; + const operatorList = leftFieldType && operatorsObj[leftFieldType] + ? operatorsObj[leftFieldType] + : leftFieldType && leftFieldType?.includes('array') + ? operatorsObj.array + : operatorsObj.default; const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined; return ( @@ -312,7 +360,7 @@ const CaseList: FC = ({ = ({ {inputType === 'variable' ? vo.dataType === 'number')} + options={filterNumberOptions} allowClear={false} variant="borderless" size="small" /> : form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'right'], value)} - /> + placeholder={t('common.pleaseEnter')} + variant="borderless" + className="rb:w-full!" + onChange={(value) => form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'right'], value)} + /> } : ( - {leftFieldType === 'boolean' - ? + {['boolean', 'array[boolean]'].includes(leftFieldType as string) + ? : } diff --git a/web/src/views/Workflow/components/Properties/ConditionList/index.tsx b/web/src/views/Workflow/components/Properties/ConditionList/index.tsx index cad32e37..ddf92971 100644 --- a/web/src/views/Workflow/components/Properties/ConditionList/index.tsx +++ b/web/src/views/Workflow/components/Properties/ConditionList/index.tsx @@ -1,4 +1,4 @@ -import { type FC } from 'react' +import { type FC, useMemo } from 'react' import clsx from 'clsx' import { useTranslation } from 'react-i18next'; import { Form, Button, Select, InputNumber, Input, Divider, type SelectProps, Flex, Space, Row, Col } from 'antd' @@ -47,6 +47,18 @@ const operatorsObj: { [key: string]: SelectProps['options'] } = { { value: 'ne', label: 'workflow.config.if-else.boolean.ne' }, { value: 'empty', label: 'workflow.config.if-else.empty' }, { value: 'not_empty', label: 'workflow.config.if-else.not_empty' }, + ], + // 为空、不为空 + object: [ + { value: 'empty', label: 'workflow.config.if-else.empty' }, + { value: 'not_empty', label: 'workflow.config.if-else.not_empty' }, + ], + // 包含、不包含、为空、不为空 + 'array': [ + { value: 'contains', label: 'workflow.config.if-else.contains' }, + { value: 'not_contains', label: 'workflow.config.if-else.not_contains' }, + { value: 'empty', label: 'workflow.config.if-else.empty' }, + { value: 'not_empty', label: 'workflow.config.if-else.not_empty' }, ] } @@ -81,6 +93,23 @@ const ConditionList: FC = ({ const currentValue = form.getFieldValue([parentName, 'logical_operator']); form.setFieldValue([parentName, 'logical_operator'], currentValue === 'and' ? 'or' : 'and'); }; + + const getNumVariable = useMemo(() => { + const filterList: Suggestion[] = [] + options.forEach(variable => { + if (variable.dataType === 'number') { + filterList.push(variable) + } else if (variable.dataType === 'file') { + filterList.push({ + ...variable, + disabled: true, + children: variable.children?.filter(child => child.dataType === 'number') + }) + } + }) + + return filterList + }, [options]) return ( <> @@ -125,11 +154,19 @@ const ConditionList: FC = ({ const expressions = form.getFieldValue([parentName, 'expressions']) || []; const currentExpression = expressions[index] || {}; const currentOperator = currentExpression.operator; - const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty'; const leftFieldValue = currentExpression.left; const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue); const leftFieldType = leftFieldOption?.dataType; - const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || []; + const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty' || ['array[object]', 'object'].includes(leftFieldType as string); + const operatorList = leftFieldType && ['array[object]', 'object'].includes(leftFieldType) + ? operatorsObj.object + : leftFieldType && ['array[boolean]', 'boolean'].includes(leftFieldType) + ? operatorsObj.boolean + : leftFieldType && operatorsObj[leftFieldType] + ? operatorsObj[leftFieldType] + : leftFieldType?.includes('array') + ? operatorsObj.array + : operatorsObj.default const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined; return ( = ({ - vo.value.includes('sys.') || + !['file', 'array[file]'].includes(vo.dataType) && + (vo.value.includes('sys.') || vo.value.includes('conv.') || vo.nodeData.type === 'loop' || - (vo.nodeData.cycle && vo.nodeData.cycle === selectedNode?.id) + (vo.nodeData.cycle && vo.nodeData.cycle === selectedNode?.id)) )} size="small" allowClear={false} @@ -163,7 +201,7 @@ const ConditionList: FC = ({ } diff --git a/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx b/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx index 09106a77..5d1138f0 100644 --- a/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx +++ b/web/src/views/Workflow/components/Properties/CycleVarsList/index.tsx @@ -6,6 +6,7 @@ import VariableSelect from '../VariableSelect' import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin' import RadioGroupBtn from '../RadioGroupBtn' import { getChildNodeVariables } from '../hooks/useVariableList' +import CodeMirrorEditor from '@/components/CodeMirrorEditor'; interface CycleVar { name: string; @@ -28,11 +29,17 @@ const types = [ 'string', 'number', 'boolean', + 'object', 'array[string]', 'array[number]', 'array[boolean]', 'array[object]' ] +const object_placeholder = `# example +# { +# "name": "redbear", +# "age": 2 +# }` const CycleVarsList: FC = ({ value = [], @@ -144,6 +151,13 @@ const CycleVarsList: FC = ({ { value: true, label: 'True' }, { value: false, label: 'False' }]} /> + : currentType === 'object' + ? : ( = ({ ...(typeOptions.length > 0 && { type: typeOptions[0].value }) }); - // Filter options based on boolean type if needed - const booleanFilterOptions = useMemo(() => { - return filterBooleanType - ? options.filter(option => option.dataType !== 'boolean') - : options - }, [options, filterBooleanType]) + const namefilterOptions = useMemo(() => { + const filterList: Suggestion[] = []; + options.forEach(vo => { + if (vo.dataType === 'file') { + filterList.push({ + ...vo, + disabled: true, + children: vo.children?.filter(child => child.dataType !== 'boolean') + }) + } else if (vo.dataType !== 'array[file]') { + filterList.push(vo) + } + }) + + return filterList + }, [options]) + const valueFilterOptions = (type?: string) => { + let filterOptions: Suggestion[] = [] + options.forEach(vo => { + if (type === 'file' && vo.dataType === 'file') { + filterOptions.push({ + ...vo, + children: [] + }) + } else if (type === 'file' && vo.dataType === 'array[file]') { + filterOptions.push(vo) + } else if (vo.dataType === 'file') { + filterOptions.push({ + ...vo, + disabled: true + }) + } else if (vo.dataType !== 'array[file]') { + filterOptions.push(vo) + } + }) + + return filterOptions + } const getColumns = (remove: (index: number) => void): TableProps['columns'] => { const hasType = typeOptions.length > 0; @@ -57,7 +89,7 @@ const EditableTable: FC = ({ render: (_: any, __: TableRow, index: number) => ( !option.dataType.includes('file'))} + options={namefilterOptions} type="input" className={contentClassName} size={size} @@ -105,9 +137,7 @@ const EditableTable: FC = ({ > {(form) => { const currentType = form.getFieldValue([...Array.isArray(parentName) ? parentName : [parentName], index, 'type']); - const filteredOptions = currentType === 'file' - ? booleanFilterOptions.filter(option => option.dataType.includes('file')) - : booleanFilterOptions.filter(option => !option.dataType.includes('file')); + const filteredOptions = valueFilterOptions(currentType) return ( diff --git a/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx b/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx index a02549bd..b032016b 100644 --- a/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx +++ b/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx @@ -4,7 +4,7 @@ * @Last Modified by: ZhaoYing * @Last Modified time: 2026-04-02 17:17:06 */ -import { type FC, useRef, useState } from "react"; +import { type FC, useMemo, useRef, useState } from "react"; import { useTranslation } from 'react-i18next' import { Form, Row, Col, Select, Button, Divider, InputNumber, Switch, Input, Flex, Radio } from 'antd' import { CaretDownOutlined, CaretRightOutlined, SettingOutlined } from '@ant-design/icons'; @@ -84,6 +84,64 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an setCollapsed((prev: boolean) => !prev) } + const filterVariables = useMemo(() => { + const filterList: Suggestion[] = [] + options.forEach(variable => { + if (['number', 'string'].includes(variable.dataType)) { + filterList.push(variable) + } else if (variable.dataType === 'file') { + filterList.push({ + ...variable, + disabled: true, + children: variable.children?.filter(child => ['number', 'string'].includes(child.dataType)) + }) + } + }) + + return filterList + }, [options]) + const filterVariablesWithFile = useMemo(() => { + const filterList: Suggestion[] = [] + options.forEach(variable => { + if (['number', 'string', 'file', 'array[file]'].includes(variable.dataType)) { + filterList.push(variable) + } + }) + + return filterList + }, [options]) + const jsonRawFilterVariables = useMemo(() => { + const filterList: Suggestion[] = [] + options.forEach(variable => { + if (['number', 'string', 'array[string]', 'array[number]'].includes(variable.dataType)) { + filterList.push(variable) + } else if (variable.dataType === 'file') { + filterList.push({ + ...variable, + disabled: true, + children: variable.children?.filter(child => ['number', 'string', 'file', 'array[string]', 'array[number]'].includes(child.dataType)) + }) + } + }) + + return filterList + }, [options]) + const fileFilterVariables = useMemo(() => { + const filterList: Suggestion[] = [] + options.forEach(variable => { + if (['array[file]'].includes(variable.dataType)) { + filterList.push(variable) + } else if (variable.dataType === 'file') { + filterList.push({ + ...variable, + children: [] + }) + } + }) + + return filterList + }, [options]) + return ( <> @@ -117,7 +175,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an vo.dataType === 'string' || vo.dataType === 'number')} + options={filterVariables} variant="outlined" type="input" size="small" @@ -134,7 +192,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an size="small" parentName="headers" title="HEADERS" - options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')} + options={filterVariables} /> @@ -143,7 +201,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an size="small" parentName="params" title="PARAMS" - options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')} + options={filterVariables} /> @@ -167,7 +225,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an vo.dataType === 'string' || vo.dataType === 'number' || vo.dataType.includes('file'))} + options={filterVariablesWithFile} typeOptions={[ { label: 'text', value: 'text' }, { label: 'file', value: 'file' } @@ -180,7 +238,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an vo.dataType === 'string' || vo.dataType === 'number')} + options={filterVariablesWithFile} filterBooleanType={true} /> @@ -190,7 +248,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an vo.dataType === 'string' || vo.dataType === 'number')} + options={jsonRawFilterVariables} isArray={false} title="JSON" titleVariant="borderless" @@ -204,7 +262,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an vo.dataType === 'string' || vo.dataType === 'number')} + options={jsonRawFilterVariables} isArray={false} title="RAW TEXT" titleVariant="borderless" @@ -220,7 +278,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an vo.dataType.includes('file'))} + options={fileFilterVariables} type="input" size="small" height={28} diff --git a/web/src/views/Workflow/components/Properties/ToolConfig/index.tsx b/web/src/views/Workflow/components/Properties/ToolConfig/index.tsx index ba1e9a5f..ce30ee8f 100644 --- a/web/src/views/Workflow/components/Properties/ToolConfig/index.tsx +++ b/web/src/views/Workflow/components/Properties/ToolConfig/index.tsx @@ -163,25 +163,45 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({ form.setFieldsValue(inititalValue) } - const getNumberOptions = useMemo(() => { - const list: Suggestion[] = [] + // string -> string + // integer -> number + // number -> number + // boolean -> boolean【只能选true/false】 + // array -> array[file]/array[object]/array[string]/array[number]/array[boolean] + // object -> object/file + const getFilterOptions = (type: string) => { + const filterList: Suggestion[] = []; options.forEach(vo => { - if (vo.children && vo?.children?.length > 0) { - const filterChild = vo.children.filter(child => child.dataType === 'number') + if (vo.children && vo.children?.length > 0) { + const childOptions = vo.children?.filter(child => child.dataType === type || (type === 'integer' && child.dataType === 'number')) - if (filterChild.length > 0) { - list.push({ ...vo, disabled: vo.dataType !== 'number', children: filterChild }) - } else if (vo.dataType === 'number') { - list.push({ ...vo, children: [] }) + if (vo.dataType === type + || (type === 'integer' && vo.dataType === 'number') + || (type === 'array' && vo.dataType.includes(type)) + || (type === 'object' && vo.dataType === 'object') + ) { + filterList.push({ + ...vo, + children: childOptions + }) + } else if (childOptions.length > 0) { + filterList.push({ + ...vo, + disabled: true, + children: childOptions + }) } - } else if (vo.dataType === 'number') { - list.push({ ...vo }) + } else if (vo.dataType === type + || (type === 'integer' && vo.dataType === 'number') + || (type === 'array' && vo.dataType.includes(type)) + || (type === 'object' && vo.dataType === 'object')) { + filterList.push(vo) } }) - console.log('options', options, list) - return list - }, [options]) + + return filterList + } return ( <> @@ -205,7 +225,7 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({ - {parameter.name} + {parameter.name} ({parameter.type})
@@ -220,21 +240,12 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({ ?