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)