diff --git a/web/static/css/style.css b/web/static/css/style.css
index 4637a817..655cc904 100644
--- a/web/static/css/style.css
+++ b/web/static/css/style.css
@@ -5569,6 +5569,227 @@ html[data-theme="dark"] .openapi-doc-btn:hover {
margin-bottom: 16px;
}
+.robot-manager {
+ margin-bottom: 24px;
+}
+
+.robot-manager-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ margin-bottom: 16px;
+}
+
+.robot-manager-toolbar h4 {
+ margin: 0 0 4px;
+ font-size: 1rem;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.robot-manager-toolbar p {
+ margin: 0;
+ font-size: 0.8125rem;
+ color: var(--text-secondary);
+ line-height: 1.45;
+}
+
+.robot-card-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
+ gap: 12px;
+}
+
+.robot-card {
+ display: flex;
+ align-items: stretch;
+ justify-content: space-between;
+ gap: 14px;
+ width: 100%;
+ min-height: 104px;
+ padding: 16px;
+ text-align: left;
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ background: var(--bg-secondary);
+ color: var(--text-primary);
+ cursor: pointer;
+ transition: border-color 0.16s ease, background 0.16s ease, transform 0.16s ease;
+}
+
+.robot-card:hover,
+.robot-card.is-active {
+ border-color: var(--accent-color);
+ background: var(--bg-primary);
+}
+
+.robot-card:hover {
+ transform: translateY(-1px);
+}
+
+.robot-card-main,
+.robot-card-meta {
+ display: flex;
+ flex-direction: column;
+}
+
+.robot-card-main {
+ min-width: 0;
+ gap: 8px;
+}
+
+.robot-card-meta {
+ flex-shrink: 0;
+ align-items: flex-end;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.robot-card-title {
+ font-size: 0.9375rem;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.robot-card-desc {
+ font-size: 0.8125rem;
+ line-height: 1.45;
+ color: var(--text-secondary);
+}
+
+.robot-card-action {
+ font-size: 0.8125rem;
+ color: var(--accent-color);
+ font-weight: 500;
+}
+
+.robot-status-pill {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 58px;
+ padding: 4px 9px;
+ border-radius: 999px;
+ font-size: 0.75rem;
+ font-weight: 600;
+ border: 1px solid var(--border-color);
+ white-space: nowrap;
+}
+
+.robot-status-pill--idle {
+ background: var(--bg-tertiary);
+ color: var(--text-secondary);
+}
+
+.robot-status-pill--ready {
+ color: var(--accent-color);
+ background: rgba(0, 102, 255, 0.1);
+ border-color: rgba(0, 102, 255, 0.25);
+}
+
+.robot-status-pill--enabled {
+ color: var(--success-color);
+ background: rgba(40, 167, 69, 0.12);
+ border-color: rgba(40, 167, 69, 0.35);
+}
+
+.robot-editor-empty {
+ margin: 8px 0 24px;
+ padding: 28px;
+ border: 1px dashed var(--border-color);
+ border-radius: 8px;
+ background: var(--bg-secondary);
+ text-align: center;
+}
+
+.robot-editor-empty h4 {
+ margin: 0 0 8px;
+ font-size: 1rem;
+ color: var(--text-primary);
+}
+
+.robot-editor-empty p {
+ margin: 0;
+ font-size: 0.875rem;
+ color: var(--text-secondary);
+}
+
+.robot-editor-panel {
+ margin-top: 8px;
+ padding: 20px 22px;
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ background: var(--bg-secondary);
+}
+
+.robot-editor-panel[hidden],
+.robot-editor-empty[hidden] {
+ display: none !important;
+}
+
+.robot-create-modal-content {
+ max-width: 680px;
+}
+
+.robot-type-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 12px;
+ margin-top: 16px;
+}
+
+.robot-type-option {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ min-height: 92px;
+ padding: 16px;
+ text-align: left;
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ background: var(--bg-secondary);
+ color: var(--text-primary);
+ cursor: pointer;
+ transition: border-color 0.16s ease, background 0.16s ease, transform 0.16s ease;
+}
+
+.robot-type-option:hover {
+ border-color: var(--accent-color);
+ background: var(--bg-primary);
+ transform: translateY(-1px);
+}
+
+.robot-type-option span {
+ font-size: 0.9375rem;
+ font-weight: 600;
+}
+
+.robot-type-option small {
+ color: var(--text-secondary);
+ line-height: 1.45;
+}
+
+@media (max-width: 768px) {
+ .robot-manager-toolbar {
+ align-items: stretch;
+ flex-direction: column;
+ }
+
+ .robot-manager-toolbar .btn-primary {
+ width: 100%;
+ }
+
+ .robot-card {
+ flex-direction: column;
+ }
+
+ .robot-card-meta {
+ flex-direction: row;
+ align-items: center;
+ }
+}
+
.settings-subsection-header {
margin-top: 24px;
margin-bottom: 16px;
diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json
index d196e988..51b867fd 100644
--- a/web/static/i18n/en-US.json
+++ b/web/static/i18n/en-US.json
@@ -1244,6 +1244,17 @@
"robots": {
"title": "Bot settings",
"description": "Configure WeChat (iLink), WeCom, DingTalk and Lark bots so you can chat with CyberStrikeAI on your phone without opening the web UI.",
+ "managerTitle": "Bot management",
+ "managerDesc": "Choose a bot type first, then configure only the fields that belong to that platform.",
+ "newBot": "New bot",
+ "configure": "Configure",
+ "statusNotConfigured": "Not configured",
+ "statusConfigured": "Configured",
+ "statusEnabled": "Enabled",
+ "emptyTitle": "Select a bot to configure",
+ "emptyDesc": "Pick a platform above, or use New bot to choose by type.",
+ "createTitle": "New bot",
+ "createDesc": "Choose a bot type to open its dedicated setup. This version supports one built-in configuration per platform.",
"wechat": {
"title": "WeChat / iLink",
"subtitle": "Bind personal WeChat via QR code and chat with CyberStrikeAI on your phone",
@@ -1272,6 +1283,7 @@
},
"wecom": {
"title": "WeCom",
+ "subtitle": "HTTP callback mode for enterprise internal app bots",
"enabled": "Enable WeCom bot",
"token": "Token",
"tokenPlaceholder": "Token",
@@ -1286,6 +1298,7 @@
},
"dingtalk": {
"title": "DingTalk",
+ "subtitle": "Enterprise internal app bot using Stream connection",
"enabled": "Enable DingTalk bot",
"clientIdLabel": "Client ID (AppKey)",
"clientIdPlaceholder": "DingTalk App Key",
@@ -1295,6 +1308,7 @@
},
"lark": {
"title": "Lark",
+ "subtitle": "Enterprise app bot for direct chat and group mentions",
"enabled": "Enable Lark bot",
"appIdLabel": "App ID",
"appIdPlaceholder": "Lark/Feishu App ID",
@@ -2952,6 +2966,7 @@
"connecting": "Connecting",
"deleteSelected": "Delete selected",
"autoLayout": "Auto layout",
+ "dryRun": "Dry run",
"canvasEmpty": "Drag nodes from the left onto the canvas, or click node buttons to add quickly",
"properties": "Properties",
"nodeProperties": "Node properties",
@@ -2996,10 +3011,18 @@
"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.",
+ "conditionGuideTitle": "Available syntax",
+ "conditionGuideVars": "Variables: {{previous.output}}, {{previous.matched}}, {{inputs.message}}, {{outputs.name}}, {{nodeId.output}}",
+ "conditionGuideOps": "Operators: ==, !=, >, >=, <, <=, contains, matches, &&, ||",
+ "conditionGuideJson": "Nested fields: jsonpath(value, \"$.status\") or jq(value, \".severity\")",
"edgeCondition": "Edge condition",
+ "edgeBranch": "Condition branch",
+ "selectBranch": "Select",
"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.",
+ "joinStrategy": "Join strategy",
+ "joinStrategyHint": "When multiple upstream nodes enter this node, choose how to build previous: merge all, last by canvas order, first non-empty, or fail fast.",
"hitlPrompt": "Approval prompt",
"hitlPromptPlaceholder": "Approve to continue",
"hitlReviewer": "Reviewer",
@@ -3018,6 +3041,11 @@
"nodeFallback": "Node {{n}}",
"loadFailed": "Failed to load workflows",
"saveFailed": "Failed to save workflow",
+ "dryRunPrompt": "Input message for dry-run",
+ "dryRunFailed": "Dry-run failed",
+ "dryRunDone": "Dry-run completed",
+ "dryRunTrace": "Dry-run trace",
+ "dryRunNoTrace": "No trace",
"deleteFailed": "Failed to delete workflow",
"saved": "Workflow saved",
"deleted": "Workflow deleted",
@@ -3039,6 +3067,13 @@
"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",
+ "conditionBranchLabel": "Condition node {{label}} outgoing edges must be marked Yes/No",
+ "conditionBranchDuplicate": "Condition node {{label}} has duplicate branches",
+ "nodeNeedsIncoming": "Node {{label}} needs an incoming edge",
+ "nodeNeedsOutgoing": "Node {{label}} needs an outgoing edge",
+ "nodeUnreachable": "Node {{label}} cannot be reached from Start",
+ "graphCycle": "Workflow contains a cycle; keep it as a DAG",
+ "serverFailed": "Server graph validation failed",
"outputNeedsKey": "Output node {{label}} requires an output variable name"
}
},
diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json
index c8b097a2..98d1f7bb 100644
--- a/web/static/i18n/zh-CN.json
+++ b/web/static/i18n/zh-CN.json
@@ -1232,6 +1232,17 @@
"robots": {
"title": "机器人设置",
"description": "配置微信、企业微信、钉钉、飞书等机器人,在手机端直接与 CyberStrikeAI 对话,无需在服务器上打开网页。",
+ "managerTitle": "机器人管理",
+ "managerDesc": "先选择机器人类型,再进入对应配置;已配置的平台会显示连接状态。",
+ "newBot": "新建机器人",
+ "configure": "配置",
+ "statusNotConfigured": "未配置",
+ "statusConfigured": "已配置",
+ "statusEnabled": "已启用",
+ "emptyTitle": "选择一个机器人开始配置",
+ "emptyDesc": "从上方列表点击已有平台,或点击“新建机器人”按类型创建。",
+ "createTitle": "新建机器人",
+ "createDesc": "选择机器人类型后进入专属配置。当前版本每个平台支持一个内置机器人配置。",
"wechat": {
"title": "微信 / iLink",
"subtitle": "扫码绑定个人微信,在手机端直接与 CyberStrikeAI 对话",
@@ -1260,6 +1271,7 @@
},
"wecom": {
"title": "企业微信",
+ "subtitle": "HTTP 回调模式,适合企业内部应用机器人",
"enabled": "启用企业微信机器人",
"token": "Token",
"tokenPlaceholder": "Token",
@@ -1274,6 +1286,7 @@
},
"dingtalk": {
"title": "钉钉",
+ "subtitle": "企业内部应用机器人,使用 Stream 长连接",
"enabled": "启用钉钉机器人",
"clientIdLabel": "Client ID (AppKey)",
"clientIdPlaceholder": "钉钉应用 AppKey",
@@ -1283,6 +1296,7 @@
},
"lark": {
"title": "飞书 (Lark)",
+ "subtitle": "飞书企业应用机器人,支持单聊与群聊 @",
"enabled": "启用飞书机器人",
"appIdLabel": "App ID",
"appIdPlaceholder": "飞书应用 App ID",
@@ -2940,6 +2954,7 @@
"connecting": "连线中",
"deleteSelected": "删除选中",
"autoLayout": "自动布局",
+ "dryRun": "试运行",
"canvasEmpty": "从左侧拖拽节点到画布,或点击节点按钮快速添加",
"properties": "属性",
"nodeProperties": "节点属性",
@@ -2984,10 +2999,18 @@
"outputKey": "输出变量名",
"conditionExpression": "条件表达式",
"conditionHint": "节点会计算 matched(true/false),由出边决定分支:第一条线为「是」,第二条为「否」;也可在连线上写 {{previous.matched}} == \"true\"。",
+ "conditionGuideTitle": "可用语法",
+ "conditionGuideVars": "变量:{{previous.output}}、{{previous.matched}}、{{inputs.message}}、{{outputs.变量名}}、{{节点ID.output}}",
+ "conditionGuideOps": "操作符:==、!=、>、>=、<、<=、contains、matches、&&、||",
+ "conditionGuideJson": "嵌套字段:jsonpath(value, \"$.status\") 或 jq(value, \".severity\")",
"edgeCondition": "连线条件",
+ "edgeBranch": "条件分支",
+ "selectBranch": "请选择",
"edgeConditionHintCondition": "{{previous.matched}} == \"true\"(是)或 == \"false\"(否)",
"edgeConditionHintExample": "例如: {{previous.output}} == \"ok\"",
"edgeBranchHint": "从条件节点连出的第一条线默认为「是」分支,第二条为「否」分支;也可在此自定义条件。",
+ "joinStrategy": "汇聚策略",
+ "joinStrategyHint": "当多个上游同时进入该节点时,决定如何生成 previous:合并全部、按画布顺序取最后、取第一个非空或上游失败即中止。",
"hitlPrompt": "审批提示",
"hitlPromptPlaceholder": "请审批是否继续",
"hitlReviewer": "审批方",
@@ -3006,6 +3029,11 @@
"nodeFallback": "节点 {{n}}",
"loadFailed": "加载工作流失败",
"saveFailed": "保存工作流失败",
+ "dryRunPrompt": "输入试运行消息",
+ "dryRunFailed": "试运行失败",
+ "dryRunDone": "试运行完成",
+ "dryRunTrace": "试运行轨迹",
+ "dryRunNoTrace": "暂无轨迹",
"deleteFailed": "删除工作流失败",
"saved": "工作流已保存",
"deleted": "工作流已删除",
@@ -3027,6 +3055,13 @@
"conditionNeedsExpr": "条件节点 {{label}} 需要条件表达式",
"conditionNeedsOutEdge": "条件节点 {{label}} 至少需要一条出边(是/否分支)",
"conditionTooManyEdges": "条件节点 {{label}} 建议最多两条出边(是/否);第三条及以后需配置连线条件",
+ "conditionBranchLabel": "条件节点 {{label}} 的出边必须标记为是/否",
+ "conditionBranchDuplicate": "条件节点 {{label}} 存在重复分支",
+ "nodeNeedsIncoming": "节点 {{label}} 需要入边",
+ "nodeNeedsOutgoing": "节点 {{label}} 需要出边",
+ "nodeUnreachable": "节点 {{label}} 无法从开始节点到达",
+ "graphCycle": "工作流存在环路,请保持 DAG",
+ "serverFailed": "后端图校验失败",
"outputNeedsKey": "输出节点 {{label}} 需要输出变量名"
}
},
diff --git a/web/static/js/settings.js b/web/static/js/settings.js
index 654cbed8..dcd19b09 100644
--- a/web/static/js/settings.js
+++ b/web/static/js/settings.js
@@ -6,6 +6,114 @@ let alwaysVisibleBuiltinToolNames = new Set();
// 全局工具状态映射,用于保存用户在所有页面的修改
// key: 唯一工具标识符(toolKey),value: { enabled: boolean, is_external: boolean, external_mcp: string }
let toolStateMap = new Map();
+let activeRobotEditor = '';
+
+function settingsT(key, fallback) {
+ if (typeof window.t === 'function') {
+ const translated = window.t(key);
+ if (translated && translated !== key) return translated;
+ }
+ return fallback;
+}
+
+function getRobotStatus(type) {
+ const value = (id) => document.getElementById(id)?.value?.trim() || '';
+ const checked = (id) => document.getElementById(id)?.checked === true;
+ let configured = false;
+ let enabled = false;
+
+ if (type === 'wechat') {
+ configured = !!value('robot-wechat-ilink-bot-id');
+ enabled = checked('robot-wechat-enabled');
+ } else if (type === 'wecom') {
+ const agentId = parseInt(value('robot-wecom-agent-id'), 10);
+ configured = !!(value('robot-wecom-token') && value('robot-wecom-corp-id') && value('robot-wecom-secret') && agentId > 0);
+ enabled = checked('robot-wecom-enabled');
+ } else if (type === 'dingtalk') {
+ configured = !!(value('robot-dingtalk-client-id') && value('robot-dingtalk-client-secret'));
+ enabled = checked('robot-dingtalk-enabled');
+ } else if (type === 'lark') {
+ configured = !!(value('robot-lark-app-id') && value('robot-lark-app-secret'));
+ enabled = checked('robot-lark-enabled');
+ }
+
+ if (enabled) {
+ return { state: 'enabled', text: settingsT('settings.robots.statusEnabled', '已启用') };
+ }
+ if (configured) {
+ return { state: 'ready', text: settingsT('settings.robots.statusConfigured', '已配置') };
+ }
+ return { state: 'idle', text: settingsT('settings.robots.statusNotConfigured', '未配置') };
+}
+
+function refreshRobotManager() {
+ ['wechat', 'wecom', 'dingtalk', 'lark'].forEach((type) => {
+ const status = getRobotStatus(type);
+ const pill = document.getElementById(`robot-card-${type}-status`);
+ if (pill) {
+ pill.className = `robot-status-pill robot-status-pill--${status.state}`;
+ pill.textContent = status.text;
+ }
+ const card = document.querySelector(`[data-robot-card="${type}"]`);
+ if (card) {
+ card.classList.toggle('is-active', activeRobotEditor === type);
+ }
+ });
+}
+
+function openRobotEditor(type) {
+ activeRobotEditor = type;
+ const empty = document.getElementById('robot-editor-empty');
+ if (empty) empty.hidden = true;
+ document.querySelectorAll('[data-robot-editor]').forEach((panel) => {
+ panel.hidden = panel.dataset.robotEditor !== type;
+ });
+ refreshRobotManager();
+ const panel = document.querySelector(`[data-robot-editor="${type}"]`);
+ if (panel) {
+ panel.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ }
+}
+
+function openRobotCreateModal() {
+ const modal = document.getElementById('robot-create-modal');
+ if (modal) modal.style.display = 'block';
+}
+
+function closeRobotCreateModal() {
+ const modal = document.getElementById('robot-create-modal');
+ if (modal) modal.style.display = 'none';
+}
+
+function selectRobotType(type) {
+ closeRobotCreateModal();
+ openRobotEditor(type);
+}
+
+function bindRobotManagerEvents() {
+ const robotInputIds = [
+ 'robot-wechat-enabled', 'robot-wechat-ilink-bot-id',
+ 'robot-wecom-enabled', 'robot-wecom-token', 'robot-wecom-corp-id', 'robot-wecom-secret', 'robot-wecom-agent-id',
+ 'robot-dingtalk-enabled', 'robot-dingtalk-client-id', 'robot-dingtalk-client-secret',
+ 'robot-lark-enabled', 'robot-lark-app-id', 'robot-lark-app-secret'
+ ];
+ robotInputIds.forEach((id) => {
+ const el = document.getElementById(id);
+ if (el && !el.dataset.robotManagerBound) {
+ el.addEventListener('input', refreshRobotManager);
+ el.addEventListener('change', refreshRobotManager);
+ el.dataset.robotManagerBound = 'true';
+ }
+ });
+
+ const modal = document.getElementById('robot-create-modal');
+ if (modal && !modal.dataset.robotManagerBound) {
+ modal.addEventListener('click', (event) => {
+ if (event.target === modal) closeRobotCreateModal();
+ });
+ modal.dataset.robotManagerBound = 'true';
+ }
+}
// 生成工具的唯一标识符,用于区分同名但来源不同的工具
function getToolKey(tool) {
@@ -539,6 +647,8 @@ async function loadConfig(loadTools = true) {
if (larkAppSecret) larkAppSecret.value = lark.app_secret || '';
const larkVerify = document.getElementById('robot-lark-verify-token');
if (larkVerify) larkVerify.value = lark.verify_token || '';
+ bindRobotManagerEvents();
+ refreshRobotManager();
// 只有在需要时才加载工具列表(MCP管理页面需要,系统设置页面不需要)
if (loadTools) {
diff --git a/web/static/js/wechat-robot.js b/web/static/js/wechat-robot.js
index d1ee42e1..64376d08 100644
--- a/web/static/js/wechat-robot.js
+++ b/web/static/js/wechat-robot.js
@@ -170,6 +170,9 @@ function showWechatBoundUI(wechat) {
if (btn) {
btn.textContent = wechatT('settings.robots.wechat.rebindButton', '重新绑定');
}
+ if (typeof refreshRobotManager === 'function') {
+ refreshRobotManager();
+ }
}
function escapeHtml(text) {
@@ -342,6 +345,9 @@ async function pollWechatBindStatus() {
bound: true
});
}
+ if (typeof refreshRobotManager === 'function') {
+ refreshRobotManager();
+ }
return;
case 'need_verifycode':
updateWechatSteps('scan');
@@ -405,5 +411,8 @@ function refreshWechatRobotBoundUI(wechat) {
if (btn) {
btn.textContent = wechatT('settings.robots.wechat.bindButton', '生成二维码并绑定');
}
+ if (typeof refreshRobotManager === 'function') {
+ refreshRobotManager();
+ }
}
}
diff --git a/web/static/js/workflows.js b/web/static/js/workflows.js
index 50324aec..87c6e553 100644
--- a/web/static/js/workflows.js
+++ b/web/static/js/workflows.js
@@ -44,6 +44,7 @@
}
const AGENT_MODES = ['eino_single', 'deep', 'plan_execute', 'supervisor'];
+ const JOIN_STRATEGIES = ['all_merge', 'last_by_canvas', 'first_non_empty', 'fail_fast'];
const WORKFLOW_EDIT_ICON = '';
@@ -105,17 +106,17 @@
case 'start':
return { input_keys: 'message, conversationId, projectId' };
case 'tool':
- return { tool_name: '', arguments: '{}', timeout_seconds: '' };
+ return { tool_name: '', arguments: '{}', timeout_seconds: '', join_strategy: 'all_merge' };
case 'agent':
- return { agent_mode: 'eino_single', input_binding: { from: 'previous', field: 'output' }, instruction: '', output_key: 'agent_result' };
+ return { agent_mode: 'eino_single', input_binding: { from: 'previous', field: 'output' }, instruction: '', output_key: 'agent_result', join_strategy: 'all_merge' };
case 'condition':
- return { expression: '{{previous.output}} != ""' };
+ return { expression: '{{previous.output}} != ""', join_strategy: 'all_merge' };
case 'hitl':
- return { prompt: _t('workflows.defaultHitlPrompt'), prompt_binding: { from: 'previous', field: 'output' }, reviewer: 'human' };
+ return { prompt: _t('workflows.defaultHitlPrompt'), prompt_binding: { from: 'previous', field: 'output' }, reviewer: 'human', join_strategy: 'all_merge' };
case 'output':
- return { output_key: 'result', source_binding: { from: 'previous', field: 'output' } };
+ return { output_key: 'result', source_binding: { from: 'previous', field: 'output' }, join_strategy: 'all_merge' };
case 'end':
- return { result_binding: { from: 'outputs', field: 'result' } };
+ return { result_binding: { from: 'outputs', field: 'result' }, join_strategy: 'all_merge' };
default:
return {};
}
@@ -561,6 +562,41 @@
`;
}
+ function joinStrategyHtml(cfg) {
+ const selected = cfg.join_strategy || 'all_merge';
+ return `
+
${esc(_t('workflows.config.joinStrategyHint') || '多个上游进入同一节点时如何生成 previous。')}
+' + esc(_t('workflows.config.edgeBranchHint')) + '
' : ''} + ${sourceType === 'condition' ? ` +${esc(_t('workflows.config.edgeBranchHint'))}
+ ` : ''} `; return; } @@ -583,6 +629,7 @@ break; case 'tool': wrap.innerHTML = ` + ${joinStrategyHtml(cfg)}配置企业微信、钉钉、飞书等机器人,在手机端直接与 CyberStrikeAI 对话,无需在服务器上打开网页。
+从上方列表点击已有平台,或点击“新建机器人”按类型创建。
+在对话中可发送以下命令(支持中英文):