From 58be62fa24301fdc2059bf4d8f7ee789b5995970 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=85=AC=E6=98=8E?=
<83812544+Ed1s0nZ@users.noreply.github.com>
Date: Fri, 3 Jul 2026 17:55:08 +0800
Subject: [PATCH] Add files via upload
---
web/static/i18n/en-US.json | 107 +++++++++++++++++++-
web/static/i18n/zh-CN.json | 107 +++++++++++++++++++-
web/static/js/workflows.js | 200 ++++++++++++++++++++++++-------------
web/templates/index.html | 90 ++++++++---------
4 files changed, 390 insertions(+), 114 deletions(-)
diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json
index 2f1778de..6ffeacc6 100644
--- a/web/static/i18n/en-US.json
+++ b/web/static/i18n/en-US.json
@@ -85,6 +85,7 @@
"agentsManagement": "Agent management",
"roles": "Roles",
"rolesManagement": "Roles Management",
+ "workflows": "Graph Orchestration",
"settings": "System settings",
"hitl": "Human-in-the-loop",
"c2": "C2",
@@ -2915,7 +2916,111 @@
"mcpDisabledBadgeTitle": "Off in MCP Management; check only expresses role linkage—turn on in MCP to run",
"roleFilterOnBanner": "These tools are checked and linked to this role (independent of MCP-wide enable).",
"roleFilterOffBanner": "These tools are unchecked and not linked to this role.",
- "checkboxLinkTitle": "Check to link this tool to this role"
+ "checkboxLinkTitle": "Check to link this tool to this role",
+ "bindWorkflow": "Bind graph workflow",
+ "bindWorkflowHint": "When a workflow is selected, conversations with this role automatically run the bound graph; workflow fields are configured freely in the graph JSON.",
+ "workflowPolicy": "Workflow trigger policy",
+ "workflowPolicyAuto": "Auto trigger",
+ "workflowPolicyOff": "Off",
+ "noWorkflowBind": "No workflow",
+ "workflowDisabledSuffix": " (disabled)"
+ },
+ "workflows": {
+ "title": "Graph Orchestration",
+ "newGraph": "New graph",
+ "processLibrary": "Process library",
+ "nodeLibrary": "Node library",
+ "emptyList": "No graph workflows yet",
+ "statusEnabled": "Enabled",
+ "statusDisabled": "Disabled",
+ "metaId": "ID",
+ "metaName": "Name",
+ "metaDescription": "Description",
+ "metaEnabled": "Enabled",
+ "namePlaceholder": "Basic Web scan",
+ "descriptionPlaceholder": "Optional",
+ "connect": "Connect",
+ "connecting": "Connecting",
+ "deleteSelected": "Delete selected",
+ "autoLayout": "Auto layout",
+ "canvasEmpty": "Drag nodes from the left onto the canvas, or click node buttons to add quickly",
+ "properties": "Properties",
+ "nodeProperties": "Node properties",
+ "edgeProperties": "Edge properties",
+ "deleteNode": "Delete node",
+ "deleteEdge": "Delete edge",
+ "propertyEmpty": "Select a node or edge to edit properties",
+ "propLabel": "Name",
+ "propType": "Type",
+ "customFields": "Custom fields",
+ "addField": "Add field",
+ "noCustomFields": "No custom fields",
+ "nodes": {
+ "start": "Start",
+ "tool": "Tool",
+ "agent": "Agent",
+ "condition": "Condition",
+ "hitl": "Approval",
+ "output": "Output",
+ "end": "End",
+ "default": "Node"
+ },
+ "edges": {
+ "yes": "Yes",
+ "no": "No"
+ },
+ "config": {
+ "inputKeys": "Input variables",
+ "mcpTool": "MCP tool",
+ "selectTool": "Select a tool",
+ "toolDisabled": " (disabled)",
+ "argumentsTemplate": "Arguments template",
+ "timeoutSeconds": "Timeout (seconds)",
+ "optional": "Optional",
+ "agentMode": "Agent mode",
+ "inputSource": "Input source",
+ "nodeInstruction": "Node instruction",
+ "instructionPlaceholder": "Describe what this node should accomplish",
+ "outputKey": "Output variable name",
+ "conditionExpression": "Condition expression",
+ "conditionHint": "The node computes matched (true/false); outgoing edges define branches: first edge is \"Yes\", second is \"No\". You can also write {{previous.matched}} == \"true\" on the edge.",
+ "edgeCondition": "Edge condition",
+ "edgeConditionHintCondition": "{{previous.matched}} == \"true\" (Yes) or == \"false\" (No)",
+ "edgeConditionHintExample": "e.g. {{previous.output}} == \"ok\"",
+ "edgeBranchHint": "The first edge from a condition node defaults to the \"Yes\" branch, the second to \"No\"; you can customize conditions here.",
+ "hitlPrompt": "Approval prompt",
+ "hitlPromptPlaceholder": "Approve to continue",
+ "hitlReviewer": "Reviewer",
+ "outputSource": "Variable source",
+ "endTemplate": "End summary template"
+ },
+ "defaultHitlPrompt": "Please approve whether this step should continue",
+ "nodeFallback": "Node {{n}}",
+ "loadFailed": "Failed to load workflows",
+ "saveFailed": "Failed to save workflow",
+ "deleteFailed": "Failed to delete workflow",
+ "saved": "Workflow saved",
+ "deleted": "Workflow deleted",
+ "idNameRequired": "Workflow ID and name are required",
+ "selectToDelete": "Select a workflow to delete",
+ "confirmDelete": "Delete workflow {{id}}?",
+ "duplicateEdge": "An edge already exists between these two nodes",
+ "connectModeOn": "Connect mode: click source node then target node",
+ "connectModeOff": "Exited connect mode",
+ "validation": {
+ "needStart": "At least one Start node is required",
+ "needOutput": "At least one Output node is required",
+ "edgeSelfLoop": "Edge {{id}} cannot point to itself",
+ "edgeSourceMissing": "Edge {{id}} source node does not exist",
+ "edgeTargetMissing": "Edge {{id}} target node does not exist",
+ "startIncoming": "Start node {{label}} must not have incoming edges",
+ "outputOutgoing": "Output node {{label}} must not have outgoing edges",
+ "toolNeedsMcp": "Tool node {{label}} requires an MCP tool",
+ "conditionNeedsExpr": "Condition node {{label}} requires a condition expression",
+ "conditionNeedsOutEdge": "Condition node {{label}} needs at least one outgoing edge (Yes/No branch)",
+ "conditionTooManyEdges": "Condition node {{label}} should have at most two outgoing edges (Yes/No); configure edge conditions for a third and beyond",
+ "outputNeedsKey": "Output node {{label}} requires an output variable name"
+ }
},
"c2": {
"clipboardCopied": "Copied to clipboard",
diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json
index 7be6e6da..e1c5658a 100644
--- a/web/static/i18n/zh-CN.json
+++ b/web/static/i18n/zh-CN.json
@@ -85,6 +85,7 @@
"agentsManagement": "Agent管理",
"roles": "角色",
"rolesManagement": "角色管理",
+ "workflows": "图编排",
"settings": "系统设置",
"hitl": "人机协同",
"c2": "C2",
@@ -2903,7 +2904,111 @@
"mcpDisabledBadgeTitle": "MCP 管理里该工具为关闭;勾选只表示想关联到本角色,实际调用需先在 MCP 中打开",
"roleFilterOnBanner": "以下为「已勾选、关联到本角色」的工具(与 MCP 管理里全局开/关无关)。",
"roleFilterOffBanner": "以下为「未勾选、未关联到本角色」的工具。",
- "checkboxLinkTitle": "勾选表示本角色关联使用该工具"
+ "checkboxLinkTitle": "勾选表示本角色关联使用该工具",
+ "bindWorkflow": "绑定图编排流程",
+ "bindWorkflowHint": "选中流程后,对话页使用该角色会自动触发绑定图;流程字段由图定义 JSON 自由配置。",
+ "workflowPolicy": "流程触发策略",
+ "workflowPolicyAuto": "自动触发",
+ "workflowPolicyOff": "关闭",
+ "noWorkflowBind": "不绑定流程",
+ "workflowDisabledSuffix": "(已禁用)"
+ },
+ "workflows": {
+ "title": "图编排",
+ "newGraph": "新建图",
+ "processLibrary": "流程库",
+ "nodeLibrary": "节点库",
+ "emptyList": "暂无图编排流程",
+ "statusEnabled": "启用",
+ "statusDisabled": "禁用",
+ "metaId": "ID",
+ "metaName": "名称",
+ "metaDescription": "描述",
+ "metaEnabled": "启用",
+ "namePlaceholder": "基础 Web 扫描",
+ "descriptionPlaceholder": "可选",
+ "connect": "连线",
+ "connecting": "连线中",
+ "deleteSelected": "删除选中",
+ "autoLayout": "自动布局",
+ "canvasEmpty": "从左侧拖拽节点到画布,或点击节点按钮快速添加",
+ "properties": "属性",
+ "nodeProperties": "节点属性",
+ "edgeProperties": "连线属性",
+ "deleteNode": "删除节点",
+ "deleteEdge": "删除连线",
+ "propertyEmpty": "选择一个节点或连线后编辑属性",
+ "propLabel": "名称",
+ "propType": "类型",
+ "customFields": "自定义字段",
+ "addField": "添加字段",
+ "noCustomFields": "暂无自定义字段",
+ "nodes": {
+ "start": "开始",
+ "tool": "工具",
+ "agent": "Agent",
+ "condition": "条件",
+ "hitl": "审批",
+ "output": "输出",
+ "end": "结束",
+ "default": "节点"
+ },
+ "edges": {
+ "yes": "是",
+ "no": "否"
+ },
+ "config": {
+ "inputKeys": "输入变量",
+ "mcpTool": "MCP 工具",
+ "selectTool": "请选择工具",
+ "toolDisabled": "(未启用)",
+ "argumentsTemplate": "参数模板",
+ "timeoutSeconds": "超时秒数",
+ "optional": "可选",
+ "agentMode": "Agent 模式",
+ "inputSource": "输入来源",
+ "nodeInstruction": "节点指令",
+ "instructionPlaceholder": "描述该节点要完成的任务",
+ "outputKey": "输出变量名",
+ "conditionExpression": "条件表达式",
+ "conditionHint": "节点会计算 matched(true/false),由出边决定分支:第一条线为「是」,第二条为「否」;也可在连线上写 {{previous.matched}} == \"true\"。",
+ "edgeCondition": "连线条件",
+ "edgeConditionHintCondition": "{{previous.matched}} == \"true\"(是)或 == \"false\"(否)",
+ "edgeConditionHintExample": "例如: {{previous.output}} == \"ok\"",
+ "edgeBranchHint": "从条件节点连出的第一条线默认为「是」分支,第二条为「否」分支;也可在此自定义条件。",
+ "hitlPrompt": "审批提示",
+ "hitlPromptPlaceholder": "请审批是否继续",
+ "hitlReviewer": "审批方",
+ "outputSource": "变量来源",
+ "endTemplate": "结束摘要模板"
+ },
+ "defaultHitlPrompt": "请审批该步骤是否继续执行",
+ "nodeFallback": "节点 {{n}}",
+ "loadFailed": "加载工作流失败",
+ "saveFailed": "保存工作流失败",
+ "deleteFailed": "删除工作流失败",
+ "saved": "工作流已保存",
+ "deleted": "工作流已删除",
+ "idNameRequired": "工作流 ID 和名称不能为空",
+ "selectToDelete": "请选择要删除的工作流",
+ "confirmDelete": "确定删除工作流 {{id}}?",
+ "duplicateEdge": "这两个节点之间已经有连线",
+ "connectModeOn": "连线模式:依次点击源节点和目标节点",
+ "connectModeOff": "已退出连线模式",
+ "validation": {
+ "needStart": "至少需要一个开始节点",
+ "needOutput": "至少需要一个输出节点",
+ "edgeSelfLoop": "连线 {{id}} 不能指向自身",
+ "edgeSourceMissing": "连线 {{id}} 的源节点不存在",
+ "edgeTargetMissing": "连线 {{id}} 的目标节点不存在",
+ "startIncoming": "开始节点 {{label}} 不应有入边",
+ "outputOutgoing": "输出节点 {{label}} 不应有出边",
+ "toolNeedsMcp": "工具节点 {{label}} 需要选择 MCP 工具",
+ "conditionNeedsExpr": "条件节点 {{label}} 需要条件表达式",
+ "conditionNeedsOutEdge": "条件节点 {{label}} 至少需要一条出边(是/否分支)",
+ "conditionTooManyEdges": "条件节点 {{label}} 建议最多两条出边(是/否);第三条及以后需配置连线条件",
+ "outputNeedsKey": "输出节点 {{label}} 需要输出变量名"
+ }
},
"c2": {
"clipboardCopied": "已复制到剪贴板",
diff --git a/web/static/js/workflows.js b/web/static/js/workflows.js
index 4b5667e3..45e4d3cb 100644
--- a/web/static/js/workflows.js
+++ b/web/static/js/workflows.js
@@ -1,6 +1,18 @@
(function () {
'use strict';
+ function _t(key, opts) {
+ if (typeof window.t === 'function') {
+ try {
+ var translated = window.t(key, opts);
+ if (typeof translated === 'string' && translated && translated !== key) {
+ return translated;
+ }
+ } catch (e) { /* ignore */ }
+ }
+ return key;
+ }
+
let workflows = [];
let currentWorkflowId = '';
let cy = null;
@@ -12,15 +24,24 @@
let workflowToolOptions = [];
let workflowToolsLoaded = false;
- const NODE_LABELS = {
- start: '开始',
- tool: '工具',
- agent: 'Agent',
- condition: '条件',
- hitl: '审批',
- output: '输出',
- end: '结束'
+ const KNOWN_NODE_LABELS = {
+ start: ['开始', 'Start'],
+ tool: ['工具', 'Tool'],
+ agent: ['Agent'],
+ condition: ['条件', 'Condition'],
+ hitl: ['审批', 'Approval'],
+ output: ['输出', 'Output'],
+ end: ['结束', 'End']
};
+ const KNOWN_EDGE_LABELS = {
+ yes: ['是', 'Yes'],
+ no: ['否', 'No']
+ };
+
+ function wfNodeLabel(type) {
+ const key = type && KNOWN_NODE_LABELS[type] ? 'workflows.nodes.' + type : 'workflows.nodes.default';
+ return _t(key);
+ }
const AGENT_MODES = ['eino_single', 'deep', 'plan_execute', 'supervisor'];
@@ -49,7 +70,7 @@
case 'condition':
return { expression: '{{previous.output}} != ""' };
case 'hitl':
- return { prompt: '请审批该步骤是否继续执行', reviewer: 'human' };
+ return { prompt: _t('workflows.defaultHitlPrompt'), reviewer: 'human' };
case 'output':
return { output_key: 'result', source: '{{previous.output}}' };
case 'end':
@@ -85,7 +106,7 @@
group: 'nodes',
data: {
id: node.id || `node-${index + 1}`,
- label: node.label || NODE_LABELS[node.type] || node.id || `节点 ${index + 1}`,
+ label: node.label || wfNodeLabel(node.type) || node.id || _t('workflows.nodeFallback', { n: index + 1 }),
type: node.type || 'tool',
config: configWithDefaults(node.type || 'tool', node.config)
},
@@ -237,7 +258,7 @@
const response = await apiFetch(`/api/workflows?includeDisabled=${includeDisabled ? 'true' : 'false'}`);
if (!response.ok) {
const err = await response.json().catch(() => ({}));
- throw new Error(err.error || '加载工作流失败');
+ throw new Error(err.error || _t('workflows.loadFailed'));
}
const data = await response.json();
workflows = data.workflows || [];
@@ -273,13 +294,13 @@
const list = document.getElementById('workflow-list');
if (!list) return;
if (!workflows.length) {
- list.innerHTML = '
从条件节点连出的第一条线默认为「是」分支,第二条为「否」分支;也可在此自定义条件。
' : ''} + ${typedField('workflow-edge-condition', _t('workflows.config.edgeCondition'), cfg.condition || '', edgeHint)} + ${sourceType === 'condition' ? '' + esc(_t('workflows.config.edgeBranchHint')) + '
' : ''} `; return; } const type = ele.data('type') || 'tool'; switch (type) { case 'start': - wrap.innerHTML = typedField('workflow-start-input-keys', '输入变量', cfg.input_keys, 'message, projectId'); + wrap.innerHTML = typedField('workflow-start-input-keys', _t('workflows.config.inputKeys'), cfg.input_keys, 'message, projectId'); break; case 'tool': wrap.innerHTML = `节点会计算 matched(true/false),由出边决定分支:第一条线为「是」,第二条为「否」;也可在连线上写 {{previous.matched}} == "true"。
${_t('workflows.config.conditionHint')}
`; break; case 'hitl': wrap.innerHTML = ` - ${typedTextarea('workflow-hitl-prompt', '审批提示', cfg.prompt, '请审批是否继续')} + ${typedTextarea('workflow-hitl-prompt', _t('workflows.config.hitlPrompt'), cfg.prompt, _t('workflows.config.hitlPromptPlaceholder'))}