diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 25f7b026..95be5849 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -1589,6 +1589,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re model_id: 'Model', temperature: 'Temperature', max_tokens: 'Max Tokens', + context: 'Context', }, start: { variables: 'Input Fields', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 363c54c9..e00fe134 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -1690,6 +1690,7 @@ export const zh = { model_id: '模型', temperature: '温度', max_tokens: '最大令牌数', + context: '上下文', }, start: { variables: '输入字段', diff --git a/web/src/views/Workflow/components/Editor/nodes/VariableNode.tsx b/web/src/views/Workflow/components/Editor/nodes/VariableNode.tsx index b1f38ada..bf810bec 100644 --- a/web/src/views/Workflow/components/Editor/nodes/VariableNode.tsx +++ b/web/src/views/Workflow/components/Editor/nodes/VariableNode.tsx @@ -42,13 +42,21 @@ const VariableComponent: React.FC<{ nodeKey: NodeKey; data: Suggestion }> = ({ })} contentEditable={false} > - - {data.nodeData?.name} - / + {data.isContext ? ( + 📄 + ) : ( + + )} + {!data.isContext && ( + <> + {data.nodeData?.name} + / + + )} {data.label} ); diff --git a/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx index d3570b0f..a35096a4 100644 --- a/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx @@ -11,7 +11,9 @@ export interface Suggestion { type: string; dataType: string; value: string; - nodeData: NodeProperties + nodeData: NodeProperties; + isContext?: boolean; // 标记是否为context变量 + disabled?: boolean; // 标记是否禁用 } const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => { @@ -131,19 +133,20 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => { key={option.key} style={{ padding: '8px 12px', - cursor: 'pointer', + cursor: option.disabled ? 'not-allowed' : 'pointer', background: selectedIndex === globalIndex ? '#f0f8ff' : 'white', display: 'flex', alignItems: 'center', justifyContent: 'space-between', + opacity: option.disabled ? 0.5 : 1, }} - onClick={() => insertMention(option)} + onClick={() => !option.disabled && insertMention(option)} onMouseEnter={() => setSelectedIndex(globalIndex)} >
= ({ options }) => { textAlign: 'center', }} > - {option.type === 'context' ? '📄' : + {option.isContext ? '📄' : option.type === 'system' ? 'x' : 'x'} {option.label} diff --git a/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx index fb60927f..4059b300 100644 --- a/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/InitialValuePlugin.tsx @@ -25,7 +25,20 @@ const InitialValuePlugin: React.FC = ({ value, options parts.forEach(part => { const match = part.match(/^\{\{([^.]+)\.([^}]+)\}\}$/); + const contextMatch = part.match(/^\{\{context\}\}$/); + // 匹配{{context}}格式 + if (contextMatch) { + const contextSuggestion = options.find(s => s.isContext && s.label === 'context'); + if (contextSuggestion) { + paragraph.append($createVariableNode(contextSuggestion)); + } else { + paragraph.append($createTextNode(part)); + } + return + } + + // 匹配普通变量{{nodeId.label}}格式 if (match) { const [_, nodeId, label] = match; diff --git a/web/src/views/Workflow/components/Properties/MappingList/index.tsx b/web/src/views/Workflow/components/Properties/MappingList/index.tsx index 9c8ed265..b2066681 100644 --- a/web/src/views/Workflow/components/Properties/MappingList/index.tsx +++ b/web/src/views/Workflow/components/Properties/MappingList/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { useTranslation } from 'react-i18next' -import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; +import { MinusCircleOutlined } from '@ant-design/icons'; import { Button, Form, Input, Space } from 'antd'; interface MappingListProps { @@ -33,8 +33,8 @@ const MappingList: React.FC = ({ name }) => { ))} - diff --git a/web/src/views/Workflow/components/Properties/MessageEditor.tsx b/web/src/views/Workflow/components/Properties/MessageEditor.tsx index 57ac251b..9ced9c79 100644 --- a/web/src/views/Workflow/components/Properties/MessageEditor.tsx +++ b/web/src/views/Workflow/components/Properties/MessageEditor.tsx @@ -1,4 +1,4 @@ -import { type FC } from 'react'; +import { type FC, useMemo } from 'react'; import { useTranslation } from 'react-i18next' import { Input, Form, Space, Button, Row, Col, Select, type FormListOperation } from 'antd'; import { MinusCircleOutlined } from '@ant-design/icons'; @@ -31,6 +31,24 @@ const MessageEditor: FC = ({ const form = Form.useFormInstance(); const values = form.getFieldsValue() + // 检查是否已经使用了context变量,将已使用的context设置为disabled + const processedOptions = useMemo(() => { + if (!isArray || !values[parentName]) return options; + + // 获取所有消息内容 + const allContents = values[parentName] + .map((msg: any) => msg.content || '') + .join(' '); + + // 将已使用的context变量标记为disabled + return options.map(opt => { + if (opt.isContext && allContents.includes(opt.value)) { + return { ...opt, disabled: true }; + } + return opt; + }); + }, [options, values, parentName, isArray]); + const handleAdd = (add: FormListOperation['add']) => { const list = values[parentName]; const lastRole = list[list.length - 1].role @@ -80,7 +98,7 @@ const MessageEditor: FC = ({ name={[name, 'content']} noStyle > - + ) @@ -104,7 +122,7 @@ const MessageEditor: FC = ({ name={parentName} noStyle > - + } diff --git a/web/src/views/Workflow/components/Properties/index.tsx b/web/src/views/Workflow/components/Properties/index.tsx index ec2116cb..2b08a04c 100644 --- a/web/src/views/Workflow/components/Properties/index.tsx +++ b/web/src/views/Workflow/components/Properties/index.tsx @@ -330,9 +330,30 @@ const Properties: FC = ({ } if (selectedNode?.data?.type === 'llm' && key === 'messages' && config.type === 'define') { + // 为llm节点且isArray=true时添加context变量支持 + let contextVariableList = [...variableList]; + const isArrayMode = config.isArray !== false; // 默认为true + + if (isArrayMode) { + const contextKey = `${selectedNode.id}_context`; + const hasContextVariable = contextVariableList.some(v => v.key === contextKey); + + if (!hasContextVariable) { + contextVariableList.unshift({ + key: contextKey, + label: 'context', + type: 'variable', + dataType: 'String', + value: `{{context}}`, + nodeData: selectedNode.getData(), + isContext: true, + }); + } + } + return ( - + ) } diff --git a/web/src/views/Workflow/constant.ts b/web/src/views/Workflow/constant.ts index 2aa95d0e..af444d70 100644 --- a/web/src/views/Workflow/constant.ts +++ b/web/src/views/Workflow/constant.ts @@ -117,6 +117,9 @@ export const nodeLibrary: NodeLibrary[] = [ step: 1, defaultValue: 2000 }, + context: { + type: 'variableList', + }, messages: { type: 'define', defaultValue: [ @@ -142,27 +145,27 @@ export const nodeLibrary: NodeLibrary[] = [ } }, // { type: "classification", icon: classificationIcon }, - // { type: "parameter-extractor", icon: parameterExtractionIcon, - // config: { - // model_id: { - // type: 'customSelect', - // url: getModelListUrl, - // params: { type: 'llm,chat' }, // llm/chat - // valueKey: 'id', - // labelKey: 'name', - // }, - // text: { - // type: 'variableList', - // }, - // params: { - // type: 'paramList', - // }, - // prompt: { - // type: 'messageEditor', - // isArray: false, - // }, - // } - // } + { type: "parameter-extractor", icon: parameterExtractionIcon, + config: { + model_id: { + type: 'customSelect', + url: getModelListUrl, + params: { type: 'llm,chat' }, // llm/chat + valueKey: 'id', + labelKey: 'name', + }, + text: { + type: 'variableList', + }, + params: { + type: 'paramList', + }, + prompt: { + type: 'messageEditor', + isArray: false, + }, + } + } ] }, // { @@ -182,115 +185,115 @@ export const nodeLibrary: NodeLibrary[] = [ // { type: "agent_arbitration", icon: agentArbitrationIcon } // ] // }, - // { - // category: "flowControl", - // nodes: [ - // { type: "if-else", icon: conditionIcon, - // config: { - // cases: { - // type: 'caseList', - // defaultValue: [ - // { - // logical_operator: 'and', - // expressions: [] - // } - // ] - // } - // } - // }, - // // { type: "iteration", icon: iterationIcon }, - // { type: "loop", icon: loopIcon }, - // // { type: "parallel", icon: parallelIcon }, - // { type: "var-aggregator", icon: aggregatorIcon, - // config: { - // group: { - // type: 'switch', - // defaultValue: false - // }, - // group_names: { - // type: 'groupVariableList', - // defaultValue: [{ key: 'Group1', value: []}] - // } - // } - // } - // ] - // }, - // { - // category: "externalInteraction", - // nodes: [ - // { type: "http-request", icon: httpRequestIcon, - // config: { - // method: { - // type: 'select', - // options: [ - // { label: 'GET', value: 'GET' }, - // { label: 'POST', value: 'POST' }, - // { label: 'HEAD', value: 'HEAD' }, - // { label: 'PATCH', value: 'PATCH' }, - // { label: 'PUT', value: 'PUT' }, - // { label: 'DELETE', value: 'DELETE' }, - // ], - // defaultValue: 'GET' - // }, - // url: { - // type: 'messageEditor', - // isArray: false, - // }, - // auth: { - // type: 'define', - // defaultValue: { - // auth_type: 'none' - // } - // }, - // headers: { - // type: 'define', - // defaultValue: {} - // }, - // params: { - // type: 'define', - // defaultValue: {} - // }, - // body: { - // type: 'define', - // defaultValue: { - // 'content_type': 'none' - // } - // }, - // verify_ssl: { - // type: 'switch', - // defaultValue: false - // }, - // timeouts: { - // type: 'define', - // defaultValue: {} - // }, - // retry: { - // type: 'define', - // }, - // error_handle: { - // type: 'define', - // defaultValue: { - // method: 'default' - // } - // } - // } - // }, - // // { type: "tools", icon: toolsIcon }, - // // { type: "code_execution", icon: codeExecutionIcon }, - // { type: "jinja-render", icon: templateRenderingIcon, - // config: { - // mapping: { - // type: 'mappingList', - // defaultValue: [] - // }, - // template: { - // type: 'messageEditor', - // isArray: false, - // }, - // } - // } - // ] - // }, + { + category: "flowControl", + nodes: [ + { type: "if-else", icon: conditionIcon, + config: { + cases: { + type: 'caseList', + defaultValue: [ + { + logical_operator: 'and', + expressions: [] + } + ] + } + } + }, + // { type: "iteration", icon: iterationIcon }, + // { type: "loop", icon: loopIcon }, + // { type: "parallel", icon: parallelIcon }, + { type: "var-aggregator", icon: aggregatorIcon, + config: { + group: { + type: 'switch', + defaultValue: false + }, + group_names: { + type: 'groupVariableList', + defaultValue: [{ key: 'Group1', value: []}] + } + } + } + ] + }, + { + category: "externalInteraction", + nodes: [ + { type: "http-request", icon: httpRequestIcon, + config: { + method: { + type: 'select', + options: [ + { label: 'GET', value: 'GET' }, + { label: 'POST', value: 'POST' }, + { label: 'HEAD', value: 'HEAD' }, + { label: 'PATCH', value: 'PATCH' }, + { label: 'PUT', value: 'PUT' }, + { label: 'DELETE', value: 'DELETE' }, + ], + defaultValue: 'GET' + }, + url: { + type: 'messageEditor', + isArray: false, + }, + auth: { + type: 'define', + defaultValue: { + auth_type: 'none' + } + }, + headers: { + type: 'define', + defaultValue: {} + }, + params: { + type: 'define', + defaultValue: {} + }, + body: { + type: 'define', + defaultValue: { + 'content_type': 'none' + } + }, + verify_ssl: { + type: 'switch', + defaultValue: false + }, + timeouts: { + type: 'define', + defaultValue: {} + }, + retry: { + type: 'define', + }, + error_handle: { + type: 'define', + defaultValue: { + method: 'default' + } + } + } + }, + // { type: "tools", icon: toolsIcon }, + // { type: "code_execution", icon: codeExecutionIcon }, + { type: "jinja-render", icon: templateRenderingIcon, + config: { + mapping: { + type: 'mappingList', + defaultValue: [] + }, + template: { + type: 'messageEditor', + isArray: false, + }, + } + } + ] + }, // { // category: "safetyAndCompliance", // nodes: [ diff --git a/web/src/views/Workflow/hooks/useWorkflowGraph.ts b/web/src/views/Workflow/hooks/useWorkflowGraph.ts index c4482497..c7480f51 100644 --- a/web/src/views/Workflow/hooks/useWorkflowGraph.ts +++ b/web/src/views/Workflow/hooks/useWorkflowGraph.ts @@ -160,7 +160,12 @@ export const useWorkflowGraph = ({ graphRef.current?.addNodes(nodeList) } if (edges.length) { - const edgeList = edges.map(edge => { + // 去重处理:相同节点之间的连线仅连一次 + const uniqueEdges = edges.filter((edge, index, arr) => { + return arr.findIndex(e => e.source === edge.source && e.target === edge.target) === index; + }); + + const edgeList = uniqueEdges.map(edge => { const { source, target, label } = edge const sourceCell = graphRef.current?.getCellById(source) const targetCell = graphRef.current?.getCellById(target) @@ -788,6 +793,11 @@ export const useWorkflowGraph = ({ const targetCell = graphRef.current?.getCellById(edge.getTargetCellId()); const sourcePortId = edge.getSourcePortId(); + // 过滤无效连线:源节点或目标节点不存在 + if (!sourceCell?.getData()?.id || !targetCell?.getData()?.id) { + return null; + } + // 如果是if-else节点的右侧端口连线,添加label if (sourceCell?.getData()?.type === 'if-else' && sourcePortId?.startsWith('CASE')) { return { @@ -801,6 +811,11 @@ export const useWorkflowGraph = ({ source: sourceCell?.getData().id, target: targetCell?.getData().id, }; + }) + .filter(edge => edge !== null) + .filter((edge, index, arr) => { + // 去重:相同节点之间的连线仅保留一次 + return arr.findIndex(e => e && e.source === edge?.source && e.target === edge?.target) === index; }), } saveWorkflowConfig(config.app_id, params as WorkflowConfig)