From 72c27273e40d8ddaf932d549221bd58aa44eb1e2 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Wed, 7 Jan 2026 17:34:45 +0800 Subject: [PATCH 1/7] fix(web): ai prompt editor update --- web/src/i18n/en.ts | 4 ++ web/src/i18n/zh.ts | 11 +++++ web/src/views/ApplicationConfig/Agent.tsx | 30 +++++--------- .../components/AiPromptModal.tsx | 12 +++++- .../components/Editor/index.tsx | 40 ++++++++++++++++++- .../Editor/plugin/InitialValuePlugin.tsx | 28 ++++++++++--- 6 files changed, 98 insertions(+), 27 deletions(-) diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 630c6c7e..6e84e720 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -1144,6 +1144,8 @@ export const en = { merge: 'Complete Aggregation', vote: 'Key Information Extraction', priority: 'Structured Integration', + addTool: 'Add Tool', + tool: 'Tool', }, userMemory: { userMemory: 'User Memory', @@ -1201,6 +1203,7 @@ export const en = { IMPLICIT_MEMORY: 'Implicit Memory', EMOTIONAL_MEMORY: 'Emotional Memory', EPISODIC_MEMORY: 'Episodic Memory', + FORGETTING_MANAGEMENT: 'Forgetting Management', endUserProfile: 'Core Profile', editEndUserProfile: 'Edit', @@ -1833,6 +1836,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re status_code: 'Status Code', max_attempts: 'Max Retry Attempts', retry_interval: 'Retry Interval', + errorBranch: 'Error Branch', }, 'jinja-render': { template: 'Code', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index b50ed1d8..48bddb00 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -633,6 +633,8 @@ export const zh = { merge: '完整汇总', vote: '关键信息提取', priority: '结构化整合', + addTool: '添加工具', + tool: '工具', }, // 角色管理相关翻译 role: { @@ -1280,6 +1282,7 @@ export const zh = { IMPLICIT_MEMORY: '隐性记忆', EMOTIONAL_MEMORY: '情绪记忆', EPISODIC_MEMORY: '情景记忆', + FORGETTING_MANAGEMENT: '遗忘', endUserProfile: '核心档案', editEndUserProfile: '编辑', @@ -1933,6 +1936,7 @@ export const zh = { status_code: '状态码', max_attempts: '最大重试次数', retry_interval: '重试间隔', + errorBranch: '异常分支', }, 'jinja-render': { template: '代码', @@ -2246,5 +2250,12 @@ export const zh = { orderPayInfo: '支付信息', create_time: '创建时间', }, + forgetDetail: { + title: '遗忘管理系统帮助AI智能管理记忆生命周期,通过自动识别低价值记忆、设置遗忘策略和执行定期清理,优化记忆库存储空间,提升检索效率。', + overviewTitle: '核心指标概览', + totalMemory: '记忆总量', + MemoryHealth: '记忆健康度', + riskOfForgetting: '遗忘风险', + } }, } \ No newline at end of file diff --git a/web/src/views/ApplicationConfig/Agent.tsx b/web/src/views/ApplicationConfig/Agent.tsx index f3e327ec..ce51c622 100644 --- a/web/src/views/ApplicationConfig/Agent.tsx +++ b/web/src/views/ApplicationConfig/Agent.tsx @@ -19,7 +19,6 @@ import type { MemoryConfig, AiPromptModalRef, Source, - ToolModalRef, ToolOption } from './types' import type { Model } from '@/views/ModelManagement/types' @@ -33,7 +32,6 @@ import { memoryConfigListUrl } from '@/api/memory' import CustomSelect from '@/components/CustomSelect' import aiPrompt from '@/assets/images/application/aiPrompt.png' import AiPromptModal from './components/AiPromptModal' -import ToolModal from './components/ToolModal' import ToolList from './components/ToolList' const DescWrapper: FC<{desc: string, className?: string}> = ({desc, className}) => { @@ -115,6 +113,7 @@ const Agent = forwardRef((_props, ref) => { const [variableList, setVariableList] = useState([]) const [isSave, setIsSave] = useState(false) const initialized = useRef(false) + const [toolList, setToolList] = useState([]) // 初始化完成标记 useEffect(() => { @@ -143,6 +142,11 @@ const Agent = forwardRef((_props, ref) => { if (isSave) return setIsSave(true) }, [values]) + useEffect(() => { + if (!initialized.current) return + if (isSave) return + setIsSave(true) + }, [toolList]) useEffect(() => { getModels() @@ -294,7 +298,11 @@ const Agent = forwardRef((_props, ref) => { ...(item.config || {}) })) } as KnowledgeConfig : null, - tools: toolList + tools: toolList.map(vo => ({ + tool_id: vo.tool_id, + operation: vo.operation, + enabled: vo.enabled + })) } console.log('params', rest, params) @@ -347,18 +355,6 @@ const Agent = forwardRef((_props, ref) => { form.setFieldValue('system_prompt', value) } - const toolModalRef = useRef(null) - const [toolList, setToolList] = useState([]) - const handleAddTool = () => { - toolModalRef.current?.handleOpen() - } - const updateTools = (tool: ToolOption) => { - const tools = [...toolList, tool] - setToolList(tools) - form.setFieldValue('tools', tools) - } - - console.log('toolList', toolList) return ( <> {loading && } @@ -469,10 +465,6 @@ const Agent = forwardRef((_props, ref) => { defaultModel={defaultModel} refresh={updatePrompt} /> - ); }); diff --git a/web/src/views/ApplicationConfig/components/AiPromptModal.tsx b/web/src/views/ApplicationConfig/components/AiPromptModal.tsx index f52c0675..b910e1b0 100644 --- a/web/src/views/ApplicationConfig/components/AiPromptModal.tsx +++ b/web/src/views/ApplicationConfig/components/AiPromptModal.tsx @@ -90,11 +90,19 @@ const AiPromptModal = forwardRef(({ switch (item.event) { case 'start': currentPromptValueRef.current = '' + if (editorRef.current?.clear) { + editorRef.current.clear(); + } break; case 'message': if (content) { currentPromptValueRef.current += content; - form.setFieldsValue({ current_prompt: currentPromptValueRef.current }) + if (editorRef.current?.appendText) { + editorRef.current.appendText(content); + editorRef.current.scrollToBottom(); + } else { + form.setFieldsValue({ current_prompt: currentPromptValueRef.current }) + } } if (desc) { setChatList(prev => { @@ -107,6 +115,8 @@ const AiPromptModal = forwardRef(({ break; case 'end': setLoading(false) + // 流结束时同步表单值 + form.setFieldsValue({ current_prompt: currentPromptValueRef.current }) break } }) diff --git a/web/src/views/ApplicationConfig/components/Editor/index.tsx b/web/src/views/ApplicationConfig/components/Editor/index.tsx index d381e003..0c5e2a86 100644 --- a/web/src/views/ApplicationConfig/components/Editor/index.tsx +++ b/web/src/views/ApplicationConfig/components/Editor/index.tsx @@ -4,7 +4,7 @@ import { LexicalComposer } from '@lexical/react/LexicalComposer'; import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; import { ContentEditable } from '@lexical/react/LexicalContentEditable'; import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; -import { $getSelection } from 'lexical'; +import { $getSelection, $getRoot, $createParagraphNode, $createTextNode, $isParagraphNode, $isTextNode } from 'lexical'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import InitialValuePlugin from './plugin/InitialValuePlugin' import LineBreakPlugin from './plugin/LineBreakPlugin'; @@ -12,6 +12,9 @@ import InsertTextPlugin from './plugin/InsertTextPlugin'; export interface EditorRef { insertText: (text: string) => void; + appendText: (text: string) => void; + clear: () => void; + scrollToBottom: () => void; } interface LexicalEditorProps { @@ -46,6 +49,41 @@ const EditorContent = forwardRef(({ selection.insertText(text); } }); + }, + appendText: (text: string) => { + editor.update(() => { + const root = $getRoot(); + const lastChild = root.getLastChild(); + if (lastChild && $isParagraphNode(lastChild)) { + const lastTextNode = lastChild.getLastChild(); + if (lastTextNode && $isTextNode(lastTextNode)) { + const currentText = lastTextNode.getTextContent(); + lastTextNode.setTextContent(currentText + text); + } else { + const textNode = $createTextNode(text); + lastChild.append(textNode); + } + } else { + const paragraph = $createParagraphNode(); + const textNode = $createTextNode(text); + paragraph.append(textNode); + root.append(paragraph); + } + }); + }, + clear: () => { + editor.update(() => { + const root = $getRoot(); + root.clear(); + const paragraph = $createParagraphNode(); + root.append(paragraph); + }); + }, + scrollToBottom: () => { + const editorElement = editor.getRootElement(); + if (editorElement) { + editorElement.scrollTop = editorElement.scrollHeight; + } } }), [editor]); diff --git a/web/src/views/ApplicationConfig/components/Editor/plugin/InitialValuePlugin.tsx b/web/src/views/ApplicationConfig/components/Editor/plugin/InitialValuePlugin.tsx index b1054055..da373023 100644 --- a/web/src/views/ApplicationConfig/components/Editor/plugin/InitialValuePlugin.tsx +++ b/web/src/views/ApplicationConfig/components/Editor/plugin/InitialValuePlugin.tsx @@ -1,21 +1,37 @@ -import { type FC, useEffect } from 'react'; +import { type FC, useEffect, useRef } from 'react'; import { $getRoot, $createParagraphNode, $createTextNode } from 'lexical'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; // 设置初始值的插件 const InitialValuePlugin: FC<{ value?: string }> = ({ value }) => { const [editor] = useLexicalComposerContext(); + const lastValueRef = useRef(undefined); useEffect(() => { - if (value) { + // 只有当value真正发生变化时才更新 + if (lastValueRef.current !== value) { editor.update(() => { const root = $getRoot(); + const currentText = root.getTextContent(); + + // 如果当前内容和新值相同,则不更新 + if (currentText === (value || '')) { + return; + } + root.clear(); - const paragraph = $createParagraphNode(); - const textNode = $createTextNode(value); - paragraph.append(textNode); - root.append(paragraph); + if (value) { + const paragraph = $createParagraphNode(); + const textNode = $createTextNode(value); + paragraph.append(textNode); + root.append(paragraph); + } else { + // 当value为undefined或空时,创建一个空段落 + const paragraph = $createParagraphNode(); + root.append(paragraph); + } }); + lastValueRef.current = value; } }, [editor, value]); From 030a141c64af35317088365f735c9960eca86457 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Wed, 7 Jan 2026 17:35:23 +0800 Subject: [PATCH 2/7] fix(web): workflow bugfix --- .../Workflow/components/Editor/index.tsx | 65 +++- .../Editor/plugin/AutocompletePlugin.tsx | 29 +- .../Editor/plugin/InitialValuePlugin.tsx | 40 +- .../Editor/plugin/JsonHighlightPlugin.tsx | 109 ++++++ .../HttpRequest/AuthConfigModal.tsx | 16 +- .../Properties/HttpRequest/EditableTable.tsx | 228 ++++++------ .../Properties/HttpRequest/index.tsx | 76 ++-- .../Properties/MappingList/index.tsx | 4 +- .../components/Properties/MessageEditor.tsx | 37 +- .../components/Properties/VariableSelect.tsx | 14 +- .../Workflow/components/Properties/index.tsx | 343 +++++++++++++++++- web/src/views/Workflow/constant.ts | 11 +- .../views/Workflow/hooks/useWorkflowGraph.ts | 46 +++ web/src/views/Workflow/types.ts | 1 + 14 files changed, 833 insertions(+), 186 deletions(-) create mode 100644 web/src/views/Workflow/components/Editor/plugin/JsonHighlightPlugin.tsx diff --git a/web/src/views/Workflow/components/Editor/index.tsx b/web/src/views/Workflow/components/Editor/index.tsx index 2d12f3f0..c487f2f4 100644 --- a/web/src/views/Workflow/components/Editor/index.tsx +++ b/web/src/views/Workflow/components/Editor/index.tsx @@ -1,4 +1,4 @@ -import { type FC, useState } from 'react'; +import { type FC, useState, useEffect } from 'react'; import { LexicalComposer } from '@lexical/react/LexicalComposer'; import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; import { ContentEditable } from '@lexical/react/LexicalContentEditable'; @@ -23,6 +23,7 @@ interface LexicalEditorProps { options: Suggestion[]; variant?: 'outlined' | 'borderless'; height?: number; + enableJinja2?: boolean; } const theme = { @@ -33,6 +34,15 @@ const theme = { }, }; +const jinja2Theme = { + ...theme, + code: 'jinja2-expression', + text: { + ...theme.text, + code: 'jinja2-inline', + }, +}; + const Editor: FC =({ placeholder = "请输入内容...", value = "", @@ -40,19 +50,62 @@ const Editor: FC =({ options, variant = 'borderless', height = 60, + enableJinja2 = false, }) => { + const [_count, setCount] = useState(0); + + useEffect(() => { + if (enableJinja2) { + const styleId = 'jinja2-styles'; + let existingStyle = document.getElementById(styleId); + + if (!existingStyle) { + const style = document.createElement('style'); + style.id = styleId; + style.textContent = ` + .jinja2-expression { + background-color: #f6f8fa !important; + border: 1px solid #d1d9e0 !important; + border-radius: 3px !important; + padding: 2px 4px !important; + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important; + font-size: 13px !important; + color: #0969da !important; + } + .jinja2-inline { + background-color: #f6f8fa !important; + padding: 1px 3px !important; + border-radius: 2px !important; + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important; + font-size: 13px !important; + color: #0969da !important; + } + .editor-paragraph { + margin: 0; + } + .editor-paragraph:has-text('{') .editor-text, + .editor-paragraph:has-text('[') .editor-text { + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important; + } + `; + document.head.appendChild(style); + } + } + }, [enableJinja2]); const initialConfig = { namespace: 'AutocompleteEditor', - theme, - nodes: [ + theme: enableJinja2 ? jinja2Theme : theme, + nodes: enableJinja2 ? [ + // 当启用jinja2时,不使用VariableNode,使用普通文本 + ] : [ // HeadingNode, // QuoteNode, // ListItemNode, // ListNode, // LinkNode, // CodeNode, - VariableNode + VariableNode, ], onError: (error: Error) => { console.error(error); @@ -96,9 +149,9 @@ const Editor: FC =({ /> - + { setCount(count) }} onChange={onChange} /> - + ); diff --git a/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx index 5c5d3956..34ef3196 100644 --- a/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx @@ -17,7 +17,7 @@ export interface Suggestion { disabled?: boolean; // 标记是否禁用 } -const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => { +const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }> = ({ options, enableJinja2 = false }) => { const [editor] = useLexicalComposerContext(); const [showSuggestions, setShowSuggestions] = useState(false); const [selectedIndex, setSelectedIndex] = useState(0); @@ -82,7 +82,32 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => { }, [editor]); const insertMention = (suggestion: Suggestion) => { - editor.dispatchCommand(INSERT_VARIABLE_COMMAND, { data: suggestion }); + if (enableJinja2) { + // 在jinja2模式下,插入{{variable}}格式的文本 + editor.update(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + const anchorNode = selection.anchor.getNode(); + const anchorOffset = selection.anchor.offset; + const nodeText = anchorNode.getTextContent(); + + // 移除触发字符'/' + const textBefore = nodeText.substring(0, anchorOffset - 1); + const textAfter = nodeText.substring(anchorOffset); + const newText = textBefore + `{{${suggestion.value}}}` + textAfter; + + anchorNode.setTextContent(newText); + + // 设置光标位置到插入文本之后 + const newOffset = textBefore.length + `{{${suggestion.value}}}`.length; + selection.anchor.offset = newOffset; + selection.focus.offset = newOffset; + } + }); + } else { + // 普通模式下使用VariableNode + editor.dispatchCommand(INSERT_VARIABLE_COMMAND, { data: suggestion }); + } setShowSuggestions(false); }; diff --git a/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx index 93197150..33e31199 100644 --- a/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx @@ -8,14 +8,31 @@ import { type Suggestion } from '../plugin/AutocompletePlugin' interface InitialValuePluginProps { value: string; options?: Suggestion[]; + enableJinja2?: boolean; } -const InitialValuePlugin: React.FC = ({ value, options = [] }) => { +const InitialValuePlugin: React.FC = ({ value, options = [], enableJinja2 = false }) => { const [editor] = useLexicalComposerContext(); - const initializedRef = useRef(false); + const prevValueRef = useRef(''); + const isUserInputRef = useRef(false); useEffect(() => { - if (!initializedRef.current && value) { + // 监听编辑器变化,标记是否为用户输入 + const removeListener = editor.registerUpdateListener(({ editorState }) => { + editorState.read(() => { + const root = $getRoot(); + const textContent = root.getTextContent(); + if (textContent !== prevValueRef.current) { + isUserInputRef.current = true; + } + }); + }); + + return removeListener; + }, [editor]); + + useEffect(() => { + if (value !== prevValueRef.current && !isUserInputRef.current) { editor.update(() => { const root = $getRoot(); root.clear(); @@ -28,7 +45,11 @@ const InitialValuePlugin: React.FC = ({ value, options const contextMatch = part.match(/^\{\{context\}\}$/); const conversationMatch = part.match(/^\{\{conv\.([^}]+)\}\}$/); - // 匹配{{context}}格式 + if (enableJinja2) { + paragraph.append($createTextNode(part)); + return; + } + if (contextMatch) { const contextSuggestion = options.find(s => s.isContext && s.label === 'context'); if (contextSuggestion) { @@ -39,7 +60,6 @@ const InitialValuePlugin: React.FC = ({ value, options return } - // 匹配{{conv.xx}}格式 if (conversationMatch) { const [_, variableName] = conversationMatch; const conversationSuggestion = options.find(s => @@ -53,7 +73,6 @@ const InitialValuePlugin: React.FC = ({ value, options return } - // 匹配普通变量{{nodeId.label}}格式 if (match) { const [_, nodeId, label] = match; @@ -75,11 +94,12 @@ const InitialValuePlugin: React.FC = ({ value, options }); root.append(paragraph); - }); - - initializedRef.current = true; + }, { discrete: true }); } - }, [options]); + + prevValueRef.current = value; + isUserInputRef.current = false; + }, [value, options, editor, enableJinja2]); return null; }; diff --git a/web/src/views/Workflow/components/Editor/plugin/JsonHighlightPlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/JsonHighlightPlugin.tsx new file mode 100644 index 00000000..93180f79 --- /dev/null +++ b/web/src/views/Workflow/components/Editor/plugin/JsonHighlightPlugin.tsx @@ -0,0 +1,109 @@ +import { useEffect } from 'react'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { $getRoot, $getSelection, $isRangeSelection, TextNode, $createTextNode } from 'lexical'; + +const JsonHighlightPlugin = () => { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + return editor.registerNodeTransform(TextNode, (textNode: TextNode) => { + const text = textNode.getTextContent(); + + // Check if text contains JSON-like patterns + if (containsJsonPatterns(text)) { + const parent = textNode.getParent(); + if (!parent) return; + + // Split text into tokens and create new nodes with appropriate classes + const tokens = tokenizeJson(text); + const newNodes = tokens.map(token => { + const newNode = $createTextNode(token.text); + + // Set format based on token type + switch (token.type) { + case 'string': + newNode.setFormat('code'); + newNode.setStyle('color: #032f62'); + break; + case 'number': + newNode.setFormat('code'); + newNode.setStyle('color: #005cc5'); + break; + case 'boolean': + newNode.setFormat('code'); + newNode.setStyle('color: #d73a49'); + break; + case 'null': + newNode.setFormat('code'); + newNode.setStyle('color: #6f42c1'); + break; + case 'key': + newNode.setFormat('code'); + newNode.setStyle('color: #22863a; font-weight: bold'); + break; + case 'punctuation': + newNode.setFormat('code'); + newNode.setStyle('color: #24292e'); + break; + } + + return newNode; + }); + + // Replace the original text node with the new highlighted nodes + if (newNodes.length > 1) { + textNode.replace(newNodes[0]); + for (let i = 1; i < newNodes.length; i++) { + newNodes[i - 1].insertAfter(newNodes[i]); + } + } + } + }); + }, [editor]); + + return null; +}; + +function containsJsonPatterns(text: string): boolean { + // Check for JSON-like patterns + return /[{}\[\]:,]/.test(text) || + /"[^"]*"/.test(text) || + /\b\d+(\.\d+)?\b/.test(text) || + /\b(true|false|null)\b/.test(text); +} + +function tokenizeJson(text: string): Array<{text: string, type: string}> { + const tokens: Array<{text: string, type: string}> = []; + const regex = /("[^"]*")|([{}\[\]:,])|(\b\d+(?:\.\d+)?\b)|(\b(?:true|false|null)\b)|(\s+)|([^\s{}\[\]:,"]+)/g; + + let match; + while ((match = regex.exec(text)) !== null) { + const [fullMatch, string, punctuation, number, boolean, whitespace, other] = match; + + if (string) { + // Check if it's a key (followed by colon) + const afterMatch = text.slice(match.index + fullMatch.length).trim(); + if (afterMatch.startsWith(':')) { + tokens.push({ text: fullMatch, type: 'key' }); + } else { + tokens.push({ text: fullMatch, type: 'string' }); + } + } else if (punctuation) { + tokens.push({ text: fullMatch, type: 'punctuation' }); + } else if (number) { + tokens.push({ text: fullMatch, type: 'number' }); + } else if (boolean) { + if (fullMatch === 'null') { + tokens.push({ text: fullMatch, type: 'null' }); + } else { + tokens.push({ text: fullMatch, type: 'boolean' }); + } + } else if (whitespace || other) { + tokens.push({ text: fullMatch, type: 'text' }); + } + } + + return tokens; +} + +export default JsonHighlightPlugin; \ No newline at end of file diff --git a/web/src/views/Workflow/components/Properties/HttpRequest/AuthConfigModal.tsx b/web/src/views/Workflow/components/Properties/HttpRequest/AuthConfigModal.tsx index 85df4d87..71cacbab 100644 --- a/web/src/views/Workflow/components/Properties/HttpRequest/AuthConfigModal.tsx +++ b/web/src/views/Workflow/components/Properties/HttpRequest/AuthConfigModal.tsx @@ -28,12 +28,18 @@ const AuthConfigModal = forwardRef(({ const handleOpen = (data?: HttpRequestConfigForm['auth']) => { if (data) { - form.setFieldsValue({ + const initialValues = { auth: !data.auth_type || data.auth_type === 'none' ? 'none' : 'api_key', auth_type: !data.auth_type || data.auth_type === 'none' ? undefined : data.auth_type, header: data.header, api_key: data.api_key - }) + } + form.setFieldValue('auth', initialValues.auth) + if (initialValues.auth !== 'none') { + setTimeout(() => { + form.setFieldsValue(initialValues) + }, 1) + } } setVisible(true); }; @@ -91,6 +97,9 @@ const AuthConfigModal = forwardRef(({ { - name?: string | string[]; - inputType?: 'select' | 'variableSelect'; - options?: { value: string, label: string }[] | Suggestion[]; -} - -const EditableCell: React.FC> = ({ - name, - inputType, - options, - children, - ...restProps -}) => { - const { t } = useTranslation(); - - if (!inputType) return {children}; - - return ( - - - {inputType === 'select' ? ( - { + form.setFieldValue([...Array.isArray(parentName) ? parentName : [parentName], index, 'value'], undefined); + }} + /> + + )} + + ) + }] : []), + { + title: t('workflow.config.value'), + dataIndex: 'value', + width: baseWidth, + render: (_: any, __: TableRow, index: number) => ( + { + const prevType = prevValues?.[Array.isArray(parentName) ? parentName.join('.') : parentName]?.[index]?.type; + const currentType = currentValues?.[Array.isArray(parentName) ? parentName.join('.') : parentName]?.[index]?.type; + return prevType !== currentType; + }} + noStyle + > + {(form) => { + const currentType = form.getFieldValue([...Array.isArray(parentName) ? parentName : [parentName], index, 'type']); + const filteredOptions = currentType === 'file' + ? options.filter(option => option.dataType === 'file') + : options; + + return ( + + + + ); + }} + + ) + }, { title: '', dataIndex: 'actions', width: '10%', render: (_: any, __: TableRow, index: number) => ( - - ); + }; return (
- {title && ( -
-
{title}
- -
- )} - - - - components={{ body: { cell: EditableCell } }} - bordered - dataSource={values} - columns={columns} - pagination={false} - size="small" - locale={{ emptyText: }} - scroll={{ x: 'max-content' }} - /> - - - {!title && } + + {(fields, { add, remove }) => { + const AddButton = ({ block = false }: { block?: boolean }) => ( + + ); + + return ( + <> + {title && ( +
+
{title}
+ +
+ )} + + + bordered + dataSource={fields.map((field) => ({ + key: String(field.key), + name: undefined, + value: undefined, + type: undefined + }))} + columns={getColumns(remove)} + pagination={false} + size="small" + locale={{ emptyText: }} + scroll={{ x: 'max-content' }} + /> + + {!title && } + + ); + }} +
); }; diff --git a/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx b/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx index 23d21ea4..4e704ca8 100644 --- a/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx +++ b/web/src/views/Workflow/components/Properties/HttpRequest/index.tsx @@ -9,8 +9,10 @@ import VariableSelect from "../VariableSelect"; import MessageEditor from '../MessageEditor' import EditableTable from './EditableTable' -const HttpRequest: FC<{ options: Suggestion[]; }> = ({ +const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: any; }> = ({ options, + selectedNode, + graphRef }) => { const { t } = useTranslation() const form = Form.useFormInstance(); @@ -22,18 +24,45 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({ } const handleRefresh = (auth: HttpRequestConfigForm['auth']) => { console.log('handleRefresh', auth) - form.setFieldsValue({ auth: {...auth} }) + form.setFieldsValue({ auth }) } - const handleChangeBodyContentType = (contentType: string) => { - const currentValues = form.getFieldsValue() + const handleChangeBodyContentType = () => { + form.setFieldValue(['body', 'data'], undefined) + } + + const handleChangeErrorHandleMethod = (method: string) => { form.setFieldsValue({ - body: { - ...currentValues?.body, - content_type: contentType, - data: undefined + error_handle: { + method, + body: undefined, + status_code: undefined, + headers: undefined } }) + + // 更新节点连接桩 + console.log('handleChangeErrorHandleMethod', selectedNode, graphRef?.current) + if (selectedNode && graphRef?.current) { + const existingPorts = selectedNode.getPorts(); + const errorPort = existingPorts.find((port: any) => port.id === 'ERROR'); + + if (method === 'branch' && !errorPort) { + // 添加异常节点连接桩 + selectedNode.addPort({ + id: 'ERROR', + group: 'right', + attrs: { text: { text: t('workflow.config.http-request.errorBranch'), fontSize: 12, fill: '#5B6167' }} + }); + } else if (method !== 'branch' && errorPort) { + // 移除异常节点连接桩和相关连线 + const edges = graphRef.current.getEdges().filter((edge: any) => + edge.getSourceCellId() === selectedNode.id && edge.getSourcePortId() === 'ERROR' + ); + edges.forEach((edge: any) => graphRef.current.removeCell(edge)); + selectedNode.removePort('ERROR'); + } + } } console.log('HttpRequest', values) @@ -73,6 +102,7 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({ parentName="headers" title="HEADERS" options={options} + filterBooleanType={true} /> @@ -81,6 +111,7 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({ parentName="params" title="PARAMS" options={options} + filterBooleanType={true} /> @@ -104,6 +135,7 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({ = ({ } {values?.body?.content_type === 'json' && = ({ {values?.body?.content_type === 'raw' && = ({ } @@ -185,7 +223,7 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({ {t('workflow.config.http-request.retry_interval')} (ms)} > @@ -196,6 +234,7 @@ const HttpRequest: FC<{ options: Suggestion[]; }> = ({ status_code number} > { - if (!value) return Promise.resolve(); - try { - JSON.parse(value); - return Promise.resolve(); - } catch { - return Promise.reject(new Error('Please enter valid JSON format')); - } - } - } - ]} + label={<>headers object} > diff --git a/web/src/views/Workflow/components/Properties/MappingList/index.tsx b/web/src/views/Workflow/components/Properties/MappingList/index.tsx index b8f7caf0..f35a422e 100644 --- a/web/src/views/Workflow/components/Properties/MappingList/index.tsx +++ b/web/src/views/Workflow/components/Properties/MappingList/index.tsx @@ -17,14 +17,14 @@ const MappingList: React.FC = ({ name, options }) => { {(fields, { add, remove }) => ( <> {fields.map(({ key, name, ...restField }) => ( - + - + diff --git a/web/src/views/Workflow/components/Properties/MessageEditor.tsx b/web/src/views/Workflow/components/Properties/MessageEditor.tsx index cd9dd17c..e5c72523 100644 --- a/web/src/views/Workflow/components/Properties/MessageEditor.tsx +++ b/web/src/views/Workflow/components/Properties/MessageEditor.tsx @@ -5,14 +5,15 @@ import { MinusCircleOutlined } from '@ant-design/icons'; import Editor from '../Editor' import type { Suggestion } from '../Editor/plugin/AutocompletePlugin' -interface TextareaProps { +interface MessageEditor { options: Suggestion[]; title?: string isArray?: boolean; - parentName?: string; + parentName?: string | string[]; label?: string; placeholder?: string; value?: string; + enableJinja2?: boolean; onChange?: (value?: string) => void; } const roleOptions = [ @@ -20,12 +21,13 @@ const roleOptions = [ { label: 'USER', value: 'USER' }, { label: 'ASSISTANT', value: 'ASSISTANT' }, ] -const MessageEditor: FC = ({ +const MessageEditor: FC = ({ title, isArray = true, parentName = 'messages', placeholder, options, + enableJinja2 = false, }) => { const { t } = useTranslation() const form = Form.useFormInstance(); @@ -33,10 +35,17 @@ const MessageEditor: FC = ({ // 检查是否已经使用了context变量,将已使用的context设置为disabled const processedOptions = useMemo(() => { - if (!isArray || !values?.[parentName]) return options; + if (!isArray) return options; + + // 获取表单中对应字段的值 + const fieldValue = Array.isArray(parentName) + ? parentName.reduce((obj, key) => obj?.[key], values) + : values?.[parentName]; + + if (!fieldValue) return options; // 获取所有消息内容 - const allContents = values[parentName] + const allContents = fieldValue .map((msg: any) => msg?.content || '') .join(' '); @@ -50,7 +59,11 @@ const MessageEditor: FC = ({ }, [options, values, parentName, isArray]); const handleAdd = (add: FormListOperation['add']) => { - const list = values?.[parentName] || []; + const fieldValue = Array.isArray(parentName) + ? parentName.reduce((obj, key) => obj?.[key], values) + : values?.[parentName]; + + const list = fieldValue || []; const lastRole = list.length > 0 ? list[list.length - 1]?.role : 'ASSISTANT'; add({ @@ -61,14 +74,14 @@ const MessageEditor: FC = ({ if (!isArray) { return ( - + {title ?? t('workflow.answerDesc')} - + ); @@ -79,7 +92,11 @@ const MessageEditor: FC = ({ {(fields, { add, remove }) => ( {fields.map(({ key, name, ...restField }) => { - const currentRole = (values?.[parentName]?.[name]?.role || 'USER').toUpperCase(); + const fieldValue = Array.isArray(parentName) + ? parentName.reduce((obj, key) => obj?.[key], values) + : values?.[parentName]; + + const currentRole = (fieldValue?.[name]?.role || 'USER').toUpperCase(); return ( @@ -105,7 +122,7 @@ const MessageEditor: FC = ({ )} - + ); diff --git a/web/src/views/Workflow/components/Properties/VariableSelect.tsx b/web/src/views/Workflow/components/Properties/VariableSelect.tsx index 7b4a7dfb..60eb2d0d 100644 --- a/web/src/views/Workflow/components/Properties/VariableSelect.tsx +++ b/web/src/views/Workflow/components/Properties/VariableSelect.tsx @@ -9,6 +9,7 @@ interface VariableSelectProps extends SelectProps { value?: string; onChange?: (value: string) => void; allowClear?: boolean; + filterBooleanType?: boolean; } const VariableSelect: FC = ({ @@ -18,6 +19,7 @@ const VariableSelect: FC = ({ allowClear = true, onChange, size, + filterBooleanType = false, ...resetPorps }) => { @@ -26,7 +28,7 @@ const VariableSelect: FC = ({ } const labelRender: LabelRender = (props) => { const { value } = props - const filterOption = options.find(vo => `{{${vo.value}}}` === value) + const filterOption = filteredOptions.find(vo => `{{${vo.value}}}` === value) if (filterOption) { return ( @@ -54,7 +56,11 @@ const VariableSelect: FC = ({ } return null } - const groupedSuggestions = options.reduce((groups: Record, suggestion) => { + const filteredOptions = filterBooleanType + ? options.filter(option => option.dataType !== 'boolean') + : options; + + const groupedSuggestions = filteredOptions.reduce((groups: Record, suggestion) => { const { nodeData } = suggestion const nodeId = nodeData.id as string; if (!groups[nodeId]) { @@ -64,12 +70,10 @@ const VariableSelect: FC = ({ return groups; }, {}); - const groupedOptions = Object.entries(groupedSuggestions).map(([nodeId, suggestions]) => ({ + const groupedOptions = Object.entries(groupedSuggestions).map(([_nodeId, suggestions]) => ({ label: suggestions[0].nodeData.name, options: suggestions.map(s => ({ label: s.label, value: `{{${s.value}}}` })) })); - - console.log('groupedOptions', groupedOptions) return ( ({ ...vo, label: t(vo.label) })) : config.options} + options={config.needTranslation ? (config.options || []).map(vo => ({ ...vo, label: t(vo.label) })) : config.options} placeholder={t('common.pleaseSelect')} /> : config.type === 'inputNumber' @@ -698,9 +990,10 @@ const Properties: FC = ({ ? { + const baseVariableList = getFilteredVariableList(selectedNode?.data?.type); // Apply filtering if specified in config if (config.filterNodeTypes || config.filterVariableNames) { - return variableList.filter(variable => { + return baseVariableList.filter(variable => { const nodeTypeMatch = !config.filterNodeTypes || (Array.isArray(config.filterNodeTypes) && config.filterNodeTypes.includes(variable.nodeData?.type)); const variableNameMatch = !config.filterVariableNames || @@ -721,22 +1014,38 @@ const Properties: FC = ({ return nodeData?.cycle === selectedNode.id; }); - return variableList.filter(variable => + return baseVariableList.filter(variable => childNodes.some(node => node.id === variable.nodeData?.id) ); } - return variableList; + return baseVariableList; })() } /> : config.type === 'switch' - ? { form.setFieldValue('group_variables', []) } : undefined} /> + ? { form.setFieldValue('group_variables', []) } : undefined} /> : config.type === 'categoryList' ? : config.type === 'conditionList' ? { + // For loop nodes, add cycle_vars to condition options + if (selectedNode?.data?.type === 'loop') { + const cycleVars = values?.cycle_vars || []; + const cycleVarSuggestions: Suggestion[] = cycleVars.map((cycleVar: any) => ({ + key: `${selectedNode.id}_cycle_${cycleVar.name}`, + label: cycleVar.name, + type: 'variable', + dataType: cycleVar.type || 'String', + value: `${selectedNode.getData().id}.${cycleVar.name}`, + nodeData: selectedNode.getData(), + })); + return [...getFilteredVariableList(selectedNode?.data?.type), ...cycleVarSuggestions]; + } + return getFilteredVariableList(selectedNode?.data?.type); + })() + } selectedNode={selectedNode} graphRef={graphRef} addBtnText={t('workflow.config.addCase')} diff --git a/web/src/views/Workflow/constant.ts b/web/src/views/Workflow/constant.ts index 98f6d865..692339da 100644 --- a/web/src/views/Workflow/constant.ts +++ b/web/src/views/Workflow/constant.ts @@ -300,6 +300,7 @@ export const nodeLibrary: NodeLibrary[] = [ config: { cycle_vars: { type: 'cycleVarsList', + defaultValue: [] }, condition: { type: 'conditionList', @@ -395,12 +396,14 @@ export const nodeLibrary: NodeLibrary[] = [ }, retry: { type: 'switch', - defaultValue: false + defaultValue: { + enable: false + } }, error_handle: { type: 'define', defaultValue: { - method: 'default' + method: 'none' } } } @@ -420,11 +423,13 @@ export const nodeLibrary: NodeLibrary[] = [ config: { mapping: { type: 'mappingList', - defaultValue: [] + defaultValue: [{name: 'arg1'}] }, template: { type: 'messageEditor', isArray: false, + enableJinja2: true, + defaultValue: "{{arg1}}" }, } } diff --git a/web/src/views/Workflow/hooks/useWorkflowGraph.ts b/web/src/views/Workflow/hooks/useWorkflowGraph.ts index 04e6a63a..1f3c5846 100644 --- a/web/src/views/Workflow/hooks/useWorkflowGraph.ts +++ b/web/src/views/Workflow/hooks/useWorkflowGraph.ts @@ -193,6 +193,27 @@ export const useWorkflowGraph = ({ nodeConfig.height = newHeight; } + // 如果是http-request节点,检查error_handle.method配置 + if (type === 'http-request' && (config as any).error_handle?.method === 'branch') { + const portAttrs = { + circle: { + r: 4, magnet: true, stroke: '#155EEF', strokeWidth: 2, fill: '#155EEF', position: { top: 22 } + }, + }; + + nodeConfig.ports = { + groups: { + right: { position: 'right', attrs: portAttrs }, + left: { position: 'left', attrs: portAttrs }, + }, + items: [ + { group: 'left' }, + { group: 'right', id: 'right' }, + { group: 'right', id: 'ERROR', attrs: { text: { text: t('workflow.config.http-request.errorBranch'), fontSize: 12, fill: '#5B6167' }}} + ] + }; + } + return nodeConfig }) @@ -284,6 +305,14 @@ export const useWorkflowGraph = ({ } } + // 如果是http-request节点且有label,根据label匹配对应的端口 + if (sourceCell.getData()?.type === 'http-request' && label) { + const matchingPort = sourcePorts.find((port: any) => port.id === label); + if (matchingPort) { + sourcePort = label; + } + } + const edgeConfig = { source: { cell: sourceCell.id, @@ -954,6 +983,23 @@ export const useWorkflowGraph = ({ }; } + // 如果是http-request节点的右侧端口连线,添加label + if (sourceCell?.getData()?.type === 'http-request') { + if (sourcePortId === 'ERROR') { + return { + source: sourceCell.getData().id, + target: targetCell?.getData().id, + label: 'ERROR', + }; + } else { + return { + source: sourceCell.getData().id, + target: targetCell?.getData().id, + label: 'SUCCESS', + }; + } + } + return { source: sourceCell?.getData().id, target: targetCell?.getData().id, diff --git a/web/src/views/Workflow/types.ts b/web/src/views/Workflow/types.ts index c3e883b1..b5d72b4a 100644 --- a/web/src/views/Workflow/types.ts +++ b/web/src/views/Workflow/types.ts @@ -26,6 +26,7 @@ export interface NodeConfig { group_variables?: Array<{ key: string, value: string[] }> cycle?: string; + cycle_vars?: Array<{ name: string; type: string; value: string; input_type: string; }> [key: string]: unknown; } From 75d512123464142527cfa9efb58220190698c780 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Wed, 7 Jan 2026 17:50:07 +0800 Subject: [PATCH 3/7] feat(web): multi_agent type app support collaboration type --- web/src/i18n/en.ts | 8 ++++---- web/src/i18n/zh.ts | 8 ++++---- web/src/views/ApplicationConfig/Cluster.tsx | 15 +++++++-------- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 6e84e720..a01feb34 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -1131,10 +1131,10 @@ export const en = { promptEmpty: 'Describe your use case on the left, and the orchestration preview will be displayed here.', master: 'Supervisor Mode', - master_agent: 'Supervisor Mode', - master_agentDesc: 'Unified scheduling and management by the main Agent, with sub-Agents executing tasks assigned by the supervisor, suitable for scenarios requiring centralized control.', - handoffs: 'Collaboration Mode', - handoffsDesc: 'Multiple Agents collaborate equally, autonomously coordinating according to task requirements, suitable for complex scenarios requiring flexible interaction.', + supervisor: 'Supervisor Mode', + supervisorDesc: 'Unified scheduling and management by the main Agent, with sub-Agents executing tasks assigned by the supervisor, suitable for scenarios requiring centralized control.', + collaboration: 'Collaboration Mode', + collaborationDesc: 'Multiple Agents collaborate equally, autonomously coordinating according to task requirements, suitable for complex scenarios requiring flexible interaction.', masterConfig: 'Supervisor Configuration', orchestrationMode: 'Task Assignment Strategy', conditional: 'Intelligent Assignment', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 48bddb00..8da2e36c 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -620,10 +620,10 @@ export const zh = { promptEmpty: '在左侧描述您的用例,编排预览将在此处显示。', master: '主管模式', - master_agent: '主管模式', - master_agentDesc: '由主 Agent 统一调度和管理,子 Agent 按照主管分配的任务执行,适合需要集中控制的场景。', - handoffs: '协作模式', - handoffsDesc: '多个 Agent 平等协作,根据任务需求自主协调配合,适合需要灵活互动的复杂场景。', + supervisor: '主管模式', + supervisorDesc: '由主 Agent 统一调度和管理,子 Agent 按照主管分配的任务执行,适合需要集中控制的场景。', + collaboration: '协作模式', + collaborationDesc: '多个 Agent 平等协作,根据任务需求自主协调配合,适合需要灵活互动的复杂场景。', masterConfig: '主管配置', orchestrationMode: '任务分配策略', conditional: '智能分配', diff --git a/web/src/views/ApplicationConfig/Cluster.tsx b/web/src/views/ApplicationConfig/Cluster.tsx index ec38c96a..ce639d62 100644 --- a/web/src/views/ApplicationConfig/Cluster.tsx +++ b/web/src/views/ApplicationConfig/Cluster.tsx @@ -42,7 +42,7 @@ const Cluster = forwardRef((_props, ref) => { const handleSave = (flag = true) => { if (!data) return Promise.resolve() - if (!values.default_model_config_id) { + if (!values.default_model_config_id && values.orchestration_mode === 'supervisor') { message.warning(t('common.selectPlaceholder', { title: t('application.model') })) return Promise.resolve() } @@ -140,15 +140,14 @@ const Cluster = forwardRef((_props, ref) => { ({ + options={['supervisor', 'collaboration'].map((type) => ({ value: type, label: t(`application.${type}`), labelDesc: t(`application.${type}Desc`), - disabled: type === 'handoffs' }))} allowClear={false} /> @@ -192,7 +191,7 @@ const Cluster = forwardRef((_props, ref) => { ))} - + {values?.orchestration_mode !== 'collaboration' && ((_props, ref) => {