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 = '
暂无图编排流程
'; + list.innerHTML = '
' + esc(_t('workflows.emptyList')) + '
'; return; } list.innerHTML = workflows.map(wf => ` `).join(''); } @@ -347,7 +368,7 @@ if (!selectedElement) { empty.hidden = false; form.hidden = true; - if (title) title.textContent = '属性'; + if (title) title.textContent = _t('workflows.properties'); if (deleteBtn) deleteBtn.hidden = true; return; } @@ -355,10 +376,10 @@ selectedElement.select(); empty.hidden = true; form.hidden = false; - if (title) title.textContent = selectedElement.isNode() ? '节点属性' : '连线属性'; + if (title) title.textContent = selectedElement.isNode() ? _t('workflows.nodeProperties') : _t('workflows.edgeProperties'); if (deleteBtn) { deleteBtn.hidden = false; - deleteBtn.textContent = selectedElement.isNode() ? '删除节点' : '删除连线'; + deleteBtn.textContent = selectedElement.isNode() ? _t('workflows.deleteNode') : _t('workflows.deleteEdge'); } const typeWrap = document.getElementById('workflow-prop-type-wrap'); const label = document.getElementById('workflow-prop-label'); @@ -410,30 +431,30 @@ if (!ele.isNode()) { const sourceType = ele.source().data('type') || ''; const edgeHint = sourceType === 'condition' - ? '{{previous.matched}} == "true"(是)或 == "false"(否)' - : '例如: {{previous.output}} == "ok"'; + ? _t('workflows.config.edgeConditionHintCondition') + : _t('workflows.config.edgeConditionHintExample'); wrap.innerHTML = ` - ${typedField('workflow-edge-condition', '连线条件', cfg.condition || '', edgeHint)} - ${sourceType === 'condition' ? '

从条件节点连出的第一条线默认为「是」分支,第二条为「否」分支;也可在此自定义条件。

' : ''} + ${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 = `
- +
- ${typedTextarea('workflow-tool-arguments', '参数模板', cfg.arguments, '{"target":"{{inputs.target}}"}')} - ${typedField('workflow-tool-timeout', '超时秒数', cfg.timeout_seconds, '可选')} + ${typedTextarea('workflow-tool-arguments', _t('workflows.config.argumentsTemplate'), cfg.arguments, '{"target":"{{inputs.target}}"}')} + ${typedField('workflow-tool-timeout', _t('workflows.config.timeoutSeconds'), cfg.timeout_seconds, _t('workflows.config.optional'))} `; if (!workflowToolsLoaded) { loadWorkflowTools().then(() => { @@ -444,27 +465,27 @@ case 'agent': wrap.innerHTML = `
- +
- ${typedField('workflow-agent-input-source', '输入来源', cfg.input_source, '{{previous.output}}')} - ${typedTextarea('workflow-agent-instruction', '节点指令', cfg.instruction, '描述该节点要完成的任务')} - ${typedField('workflow-agent-output-key', '输出变量名', cfg.output_key, 'agent_result')} + ${typedField('workflow-agent-input-source', _t('workflows.config.inputSource'), cfg.input_source, '{{previous.output}}')} + ${typedTextarea('workflow-agent-instruction', _t('workflows.config.nodeInstruction'), cfg.instruction, _t('workflows.config.instructionPlaceholder'))} + ${typedField('workflow-agent-output-key', _t('workflows.config.outputKey'), cfg.output_key, 'agent_result')} `; break; case 'condition': wrap.innerHTML = ` - ${typedField('workflow-condition-expression', '条件表达式', cfg.expression, '{{previous.output}} != ""')} -

节点会计算 matched(true/false),由出边决定分支:第一条线为「是」,第二条为「否」;也可在连线上写 {{previous.matched}} == "true"

+ ${typedField('workflow-condition-expression', _t('workflows.config.conditionExpression'), cfg.expression, '{{previous.output}} != ""')} +

${_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'))}
- + - - - + + + +
- - - - - + + + + +
-
从左侧拖拽节点到画布,或点击节点按钮快速添加
+
从左侧拖拽节点到画布,或点击节点按钮快速添加