mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-07-07 04:58:03 +02:00
Add files via upload
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 <code>{{previous.matched}} == \"true\"</code> 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"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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),由出边决定分支:第一条线为「是」,第二条为「否」;也可在连线上写 <code>{{previous.matched}} == \"true\"</code>。",
|
||||
"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}} 需要输出变量名"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+212
-16
@@ -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 = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>';
|
||||
|
||||
@@ -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 `
|
||||
<div class="form-group">
|
||||
<label for="workflow-join-strategy">${esc(_t('workflows.config.joinStrategy') || '汇聚策略')}</label>
|
||||
<select id="workflow-join-strategy" onchange="updateWorkflowTypedConfig()">
|
||||
${JOIN_STRATEGIES.map(strategy => `<option value="${strategy}" ${strategy === selected ? 'selected' : ''}>${strategy}</option>`).join('')}
|
||||
</select>
|
||||
<p class="workflow-config-hint">${esc(_t('workflows.config.joinStrategyHint') || '多个上游进入同一节点时如何生成 previous。')}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function conditionExpressionGuideHtml() {
|
||||
const examples = [
|
||||
'{{previous.output}} != ""',
|
||||
'{{outputs.risk_score}} >= 8',
|
||||
'{{previous.output}} contains "success"',
|
||||
'{{previous.output}} matches "^ok"',
|
||||
'jsonpath({{previous.output}}, "$.status") == "ok"',
|
||||
'jq({{outputs.scan}}, ".severity") == "high"'
|
||||
];
|
||||
return `
|
||||
<div class="workflow-config-hint workflow-condition-guide">
|
||||
<div><strong>${esc(_t('workflows.config.conditionGuideTitle'))}</strong></div>
|
||||
<div>${esc(_t('workflows.config.conditionGuideVars'))}</div>
|
||||
<div>${esc(_t('workflows.config.conditionGuideOps'))}</div>
|
||||
<div>${esc(_t('workflows.config.conditionGuideJson'))}</div>
|
||||
<div class="workflow-example-chips">
|
||||
${examples.map(expr => `<button type="button" class="btn-secondary btn-small" onclick="useWorkflowConditionExample(this.dataset.expression)" data-expression="${esc(expr)}">${esc(expr)}</button>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderTypedConfig(ele) {
|
||||
const wrap = document.getElementById('workflow-typed-config');
|
||||
if (!wrap || !ele) return;
|
||||
@@ -572,7 +608,17 @@
|
||||
: _t('workflows.config.edgeConditionHintExample');
|
||||
wrap.innerHTML = `
|
||||
${typedField('workflow-edge-condition', _t('workflows.config.edgeCondition'), cfg.condition || '', edgeHint)}
|
||||
${sourceType === 'condition' ? '<p class="workflow-config-hint">' + esc(_t('workflows.config.edgeBranchHint')) + '</p>' : ''}
|
||||
${sourceType === 'condition' ? `
|
||||
<div class="form-group">
|
||||
<label for="workflow-edge-branch">${esc(_t('workflows.config.edgeBranch') || '条件分支')}</label>
|
||||
<select id="workflow-edge-branch" onchange="updateWorkflowTypedConfig()">
|
||||
<option value="">${esc(_t('workflows.config.selectBranch') || '请选择')}</option>
|
||||
<option value="true" ${cfg.branch === 'true' ? 'selected' : ''}>true / 是</option>
|
||||
<option value="false" ${cfg.branch === 'false' ? 'selected' : ''}>false / 否</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="workflow-config-hint">${esc(_t('workflows.config.edgeBranchHint'))}</p>
|
||||
` : ''}
|
||||
`;
|
||||
return;
|
||||
}
|
||||
@@ -583,6 +629,7 @@
|
||||
break;
|
||||
case 'tool':
|
||||
wrap.innerHTML = `
|
||||
${joinStrategyHtml(cfg)}
|
||||
<div class="form-group">
|
||||
<label for="workflow-tool-name">${esc(_t('workflows.config.mcpTool'))}</label>
|
||||
<select id="workflow-tool-name" onchange="updateWorkflowTypedConfig()">
|
||||
@@ -601,6 +648,7 @@
|
||||
break;
|
||||
case 'agent':
|
||||
wrap.innerHTML = `
|
||||
${joinStrategyHtml(cfg)}
|
||||
<div class="form-group">
|
||||
<label for="workflow-agent-mode">${esc(_t('workflows.config.agentMode'))}</label>
|
||||
<select id="workflow-agent-mode" onchange="updateWorkflowTypedConfig()">
|
||||
@@ -614,12 +662,15 @@
|
||||
break;
|
||||
case 'condition':
|
||||
wrap.innerHTML = `
|
||||
${joinStrategyHtml(cfg)}
|
||||
${typedField('workflow-condition-expression', _t('workflows.config.conditionExpression'), cfg.expression, '{{previous.output}} != ""')}
|
||||
<p class="workflow-config-hint">${_t('workflows.config.conditionHint')}</p>
|
||||
${conditionExpressionGuideHtml()}
|
||||
`;
|
||||
break;
|
||||
case 'hitl':
|
||||
wrap.innerHTML = `
|
||||
${joinStrategyHtml(cfg)}
|
||||
${typedTextarea('workflow-hitl-prompt', _t('workflows.config.hitlPrompt'), cfg.prompt, _t('workflows.config.hitlPromptPlaceholder'))}
|
||||
${bindingFieldHtml('workflow-hitl-prompt-binding', 'workflows.config.promptBinding', bindingFromConfig(cfg, 'prompt_binding', 'previous', 'output'), 'workflows.config.promptBindingHint')}
|
||||
<p class="workflow-config-hint">${_t('workflows.config.hitlInteractiveHint')}</p>
|
||||
@@ -634,13 +685,14 @@
|
||||
break;
|
||||
case 'output':
|
||||
wrap.innerHTML = `
|
||||
${joinStrategyHtml(cfg)}
|
||||
${typedField('workflow-output-key', _t('workflows.config.outputKey'), cfg.output_key, 'result')}
|
||||
${bindingFieldHtml('workflow-output-source', 'workflows.config.sourceBinding', bindingFromConfig(cfg, 'source_binding', 'previous', 'output'), 'workflows.config.sourceBindingHint')}
|
||||
${typedField('workflow-output-static', _t('workflows.config.staticValue'), cfg.static_value || '', _t('workflows.config.optional'))}
|
||||
`;
|
||||
break;
|
||||
case 'end':
|
||||
wrap.innerHTML = bindingFieldHtml('workflow-end-result', 'workflows.config.resultBinding', bindingFromConfig(cfg, 'result_binding', 'outputs', 'result'), 'workflows.config.resultBindingHint');
|
||||
wrap.innerHTML = joinStrategyHtml(cfg) + bindingFieldHtml('workflow-end-result', 'workflows.config.resultBinding', bindingFromConfig(cfg, 'result_binding', 'outputs', 'result'), 'workflows.config.resultBindingHint');
|
||||
break;
|
||||
default:
|
||||
wrap.innerHTML = '';
|
||||
@@ -677,9 +729,13 @@
|
||||
function readTypedConfig(ele) {
|
||||
if (!ele) return {};
|
||||
if (!ele.isNode()) {
|
||||
return { condition: (document.getElementById('workflow-edge-condition') || {}).value || '' };
|
||||
const cfg = { condition: (document.getElementById('workflow-edge-condition') || {}).value || '' };
|
||||
const branchEl = document.getElementById('workflow-edge-branch');
|
||||
if (branchEl) cfg.branch = branchEl.value || '';
|
||||
return cfg;
|
||||
}
|
||||
const type = ele.data('type') || 'tool';
|
||||
const join_strategy = (document.getElementById('workflow-join-strategy') || {}).value || 'all_merge';
|
||||
switch (type) {
|
||||
case 'start':
|
||||
return { input_keys: (document.getElementById('workflow-start-input-keys') || {}).value || '' };
|
||||
@@ -687,31 +743,35 @@
|
||||
return {
|
||||
tool_name: (document.getElementById('workflow-tool-name') || {}).value || '',
|
||||
arguments: (document.getElementById('workflow-tool-arguments') || {}).value || '{}',
|
||||
timeout_seconds: (document.getElementById('workflow-tool-timeout') || {}).value || ''
|
||||
timeout_seconds: (document.getElementById('workflow-tool-timeout') || {}).value || '',
|
||||
join_strategy
|
||||
};
|
||||
case 'agent':
|
||||
return {
|
||||
agent_mode: (document.getElementById('workflow-agent-mode') || {}).value || 'eino_single',
|
||||
input_binding: readBinding('workflow-agent-input'),
|
||||
instruction: (document.getElementById('workflow-agent-instruction') || {}).value || '',
|
||||
output_key: (document.getElementById('workflow-agent-output-key') || {}).value || 'agent_result'
|
||||
output_key: (document.getElementById('workflow-agent-output-key') || {}).value || 'agent_result',
|
||||
join_strategy
|
||||
};
|
||||
case 'condition':
|
||||
return { expression: (document.getElementById('workflow-condition-expression') || {}).value || '' };
|
||||
return { expression: (document.getElementById('workflow-condition-expression') || {}).value || '', join_strategy };
|
||||
case 'hitl':
|
||||
return {
|
||||
prompt: (document.getElementById('workflow-hitl-prompt') || {}).value || '',
|
||||
prompt_binding: readBinding('workflow-hitl-prompt-binding'),
|
||||
reviewer: (document.getElementById('workflow-hitl-reviewer') || {}).value || 'human'
|
||||
reviewer: (document.getElementById('workflow-hitl-reviewer') || {}).value || 'human',
|
||||
join_strategy
|
||||
};
|
||||
case 'output':
|
||||
return {
|
||||
output_key: (document.getElementById('workflow-output-key') || {}).value || 'result',
|
||||
source_binding: readBinding('workflow-output-source'),
|
||||
static_value: (document.getElementById('workflow-output-static') || {}).value || ''
|
||||
static_value: (document.getElementById('workflow-output-static') || {}).value || '',
|
||||
join_strategy
|
||||
};
|
||||
case 'end':
|
||||
return { result_binding: readBinding('workflow-end-result') };
|
||||
return { result_binding: readBinding('workflow-end-result'), join_strategy };
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
@@ -937,6 +997,7 @@
|
||||
const ids = new Set(nodes.map(node => node.id));
|
||||
const starts = nodes.filter(node => node.type === 'start');
|
||||
const outputs = nodes.filter(node => node.type === 'output');
|
||||
const terminals = nodes.filter(node => node.type === 'output' || node.type === 'end');
|
||||
if (!starts.length) errors.push(_t('workflows.validation.needStart'));
|
||||
if (!outputs.length) errors.push(_t('workflows.validation.needOutput'));
|
||||
edges.forEach(edge => {
|
||||
@@ -950,6 +1011,15 @@
|
||||
outputs.forEach(node => {
|
||||
if (edges.some(edge => edge.source === node.id)) errors.push(_t('workflows.validation.outputOutgoing', { label: node.label || node.id }));
|
||||
});
|
||||
nodes.filter(node => node.type === 'end').forEach(node => {
|
||||
if (edges.some(edge => edge.source === node.id)) errors.push(_t('workflows.validation.outputOutgoing', { label: node.label || node.id }));
|
||||
});
|
||||
nodes.filter(node => node.type !== 'start').forEach(node => {
|
||||
if (!edges.some(edge => edge.target === node.id)) errors.push(_t('workflows.validation.nodeNeedsIncoming', { label: node.label || node.id }));
|
||||
});
|
||||
nodes.filter(node => node.type !== 'output' && node.type !== 'end').forEach(node => {
|
||||
if (!edges.some(edge => edge.source === node.id)) errors.push(_t('workflows.validation.nodeNeedsOutgoing', { label: node.label || node.id }));
|
||||
});
|
||||
nodes.filter(node => node.type === 'tool').forEach(node => {
|
||||
if (!String((node.config || {}).tool_name || '').trim()) {
|
||||
errors.push(_t('workflows.validation.toolNeedsMcp', { label: node.label || node.id }));
|
||||
@@ -965,13 +1035,86 @@
|
||||
} else if (outEdges.length > 2) {
|
||||
errors.push(_t('workflows.validation.conditionTooManyEdges', { label: node.label || node.id }));
|
||||
}
|
||||
const branches = outEdges.map(edge => String(((edge.config || {}).branch || edge.label || '')).trim().toLowerCase());
|
||||
if (branches.some(branch => !['true', 'false', '是', '否', 'yes', 'no', 'y', 'n'].includes(branch))) {
|
||||
errors.push(_t('workflows.validation.conditionBranchLabel', { label: node.label || node.id }));
|
||||
}
|
||||
if (new Set(branches).size !== branches.length) {
|
||||
errors.push(_t('workflows.validation.conditionBranchDuplicate', { label: node.label || node.id }));
|
||||
}
|
||||
});
|
||||
nodes.filter(node => node.type === 'output').forEach(node => {
|
||||
if (!String((node.config || {}).output_key || '').trim()) {
|
||||
errors.push(_t('workflows.validation.outputNeedsKey', { label: node.label || node.id }));
|
||||
}
|
||||
});
|
||||
return errors;
|
||||
if (terminals.length) {
|
||||
const outgoing = new Map();
|
||||
edges.forEach(edge => {
|
||||
if (!outgoing.has(edge.source)) outgoing.set(edge.source, []);
|
||||
outgoing.get(edge.source).push(edge.target);
|
||||
});
|
||||
const reached = new Set();
|
||||
const queue = starts.map(node => node.id);
|
||||
while (queue.length) {
|
||||
const id = queue.shift();
|
||||
if (reached.has(id)) continue;
|
||||
reached.add(id);
|
||||
(outgoing.get(id) || []).forEach(next => queue.push(next));
|
||||
}
|
||||
nodes.forEach(node => {
|
||||
if (!reached.has(node.id)) errors.push(_t('workflows.validation.nodeUnreachable', { label: node.label || node.id }));
|
||||
});
|
||||
const visiting = new Set();
|
||||
const visited = new Set();
|
||||
function visit(id) {
|
||||
if (visiting.has(id)) return true;
|
||||
if (visited.has(id)) return false;
|
||||
visiting.add(id);
|
||||
for (const next of (outgoing.get(id) || [])) {
|
||||
if (visit(next)) return true;
|
||||
}
|
||||
visiting.delete(id);
|
||||
visited.add(id);
|
||||
return false;
|
||||
}
|
||||
nodes.forEach(node => {
|
||||
if (visit(node.id)) errors.push(_t('workflows.validation.graphCycle', { label: node.label || node.id }));
|
||||
});
|
||||
}
|
||||
return Array.from(new Set(errors));
|
||||
}
|
||||
|
||||
async function validateWorkflowGraphOnServer(graph) {
|
||||
const response = await apiFetch('/api/workflows/validate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ graph })
|
||||
});
|
||||
if (!response.ok) {
|
||||
const err = await response.json().catch(() => ({}));
|
||||
throw new Error(err.error || _t('workflows.validation.serverFailed'));
|
||||
}
|
||||
}
|
||||
|
||||
function renderWorkflowDryRunTrace(result) {
|
||||
const panel = document.getElementById('workflow-dry-run-panel');
|
||||
const output = document.getElementById('workflow-dry-run-output');
|
||||
if (!panel || !output) return;
|
||||
const trace = (result && result.trace) || [];
|
||||
panel.hidden = false;
|
||||
if (!trace.length) {
|
||||
output.textContent = _t('workflows.dryRunNoTrace') || 'No trace';
|
||||
return;
|
||||
}
|
||||
output.innerHTML = trace.map((item, index) => {
|
||||
const status = item.status || '';
|
||||
const label = item.label || item.nodeId || ('#' + (index + 1));
|
||||
return `<div class="workflow-dry-run-step">
|
||||
<strong>${index + 1}. ${esc(label)}</strong>
|
||||
<span>${esc(item.type || '')} · ${esc(status)}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
window.saveWorkflowDraft = async function () {
|
||||
@@ -990,6 +1133,12 @@
|
||||
showNotification(errors.slice(0, 4).join(';'), 'error');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await validateWorkflowGraphOnServer(graph);
|
||||
} catch (error) {
|
||||
showNotification(error.message || _t('workflows.validation.serverFailed'), 'error');
|
||||
return;
|
||||
}
|
||||
const method = currentWorkflowId ? 'PUT' : 'POST';
|
||||
const url = currentWorkflowId ? `/api/workflows/${encodeURIComponent(currentWorkflowId)}` : '/api/workflows';
|
||||
const response = await apiFetch(url, {
|
||||
@@ -1019,6 +1168,45 @@
|
||||
}
|
||||
};
|
||||
|
||||
window.dryRunWorkflowDraft = async function () {
|
||||
initCy();
|
||||
const graph = elementsToGraph();
|
||||
const errors = validateWorkflowGraph(graph);
|
||||
if (errors.length) {
|
||||
showNotification(errors.slice(0, 4).join(';'), 'error');
|
||||
return;
|
||||
}
|
||||
const message = window.prompt(_t('workflows.dryRunPrompt') || 'Input message for dry-run', 'ping');
|
||||
if (message === null) return;
|
||||
try {
|
||||
const response = await apiFetch('/api/workflows/dry-run', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ graph, inputs: { message } })
|
||||
});
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || _t('workflows.dryRunFailed'));
|
||||
}
|
||||
const result = data.result || {};
|
||||
const trace = result.trace || [];
|
||||
console.groupCollapsed('[Workflow dry-run]');
|
||||
console.table(trace.map(item => ({
|
||||
nodeId: item.nodeId,
|
||||
label: item.label,
|
||||
type: item.type,
|
||||
status: item.status
|
||||
})));
|
||||
console.log(result);
|
||||
console.groupEnd();
|
||||
renderWorkflowDryRunTrace(result);
|
||||
const summary = trace.slice(0, 8).map(item => `${item.label || item.nodeId}: ${item.status}`).join('\n');
|
||||
window.alert((_t('workflows.dryRunDone') || 'Dry-run completed') + '\n\n' + summary);
|
||||
} catch (error) {
|
||||
showNotification(error.message || _t('workflows.dryRunFailed'), 'error');
|
||||
}
|
||||
};
|
||||
|
||||
window.deleteCurrentWorkflow = async function () {
|
||||
const meta = readWorkflowMetaFromForm();
|
||||
const id = currentWorkflowId || meta.id;
|
||||
@@ -1140,6 +1328,14 @@
|
||||
mergeVisibleConfig();
|
||||
};
|
||||
|
||||
window.useWorkflowConditionExample = function (expr) {
|
||||
const input = document.getElementById('workflow-condition-expression');
|
||||
if (!input) return;
|
||||
input.value = expr || '';
|
||||
updateWorkflowTypedConfig();
|
||||
input.focus();
|
||||
};
|
||||
|
||||
window.removeWorkflowCustomField = function (index) {
|
||||
if (!selectedElement) return;
|
||||
const entries = Object.entries(stripTypedConfig(selectedElement));
|
||||
|
||||
@@ -2593,6 +2593,7 @@
|
||||
<button class="btn-secondary btn-small" type="button" onclick="toggleWorkflowConnectMode()" id="workflow-connect-btn" data-i18n="workflows.connect">连线</button>
|
||||
<button class="btn-secondary btn-small" type="button" onclick="deleteWorkflowSelection()" data-i18n="workflows.deleteSelected">删除选中</button>
|
||||
<button class="btn-secondary btn-small" type="button" onclick="layoutWorkflowGraph()" data-i18n="workflows.autoLayout">自动布局</button>
|
||||
<button class="btn-secondary btn-small" type="button" onclick="dryRunWorkflowDraft()" data-i18n="workflows.dryRun">试运行</button>
|
||||
<button class="btn-secondary btn-small" onclick="deleteCurrentWorkflow()" data-i18n="common.delete">删除</button>
|
||||
<button class="btn-primary btn-small" onclick="saveWorkflowDraft()" data-i18n="common.save">保存</button>
|
||||
</div>
|
||||
@@ -2632,6 +2633,12 @@
|
||||
</div>
|
||||
<div id="workflow-custom-fields" class="workflow-custom-fields"></div>
|
||||
</div>
|
||||
<div id="workflow-dry-run-panel" class="workflow-property-form" hidden>
|
||||
<div class="workflow-custom-fields-head">
|
||||
<span data-i18n="workflows.dryRunTrace">试运行轨迹</span>
|
||||
</div>
|
||||
<div id="workflow-dry-run-output" class="workflow-property-empty workflow-property-empty--compact"></div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3300,8 +3307,66 @@
|
||||
<p class="settings-description" data-i18n="settings.robots.description">配置企业微信、钉钉、飞书等机器人,在手机端直接与 CyberStrikeAI 对话,无需在服务器上打开网页。</p>
|
||||
</div>
|
||||
|
||||
<div class="robot-manager">
|
||||
<div class="robot-manager-toolbar">
|
||||
<div>
|
||||
<h4 data-i18n="settings.robots.managerTitle">机器人管理</h4>
|
||||
<p data-i18n="settings.robots.managerDesc">先选择机器人类型,再进入对应配置;已配置的平台会显示连接状态。</p>
|
||||
</div>
|
||||
<button type="button" class="btn-primary" onclick="openRobotCreateModal()" data-i18n="settings.robots.newBot">新建机器人</button>
|
||||
</div>
|
||||
|
||||
<div class="robot-card-grid">
|
||||
<button type="button" class="robot-card" data-robot-card="wechat" onclick="openRobotEditor('wechat')">
|
||||
<span class="robot-card-main">
|
||||
<span class="robot-card-title" data-i18n="settings.robots.wechat.title">微信 / iLink</span>
|
||||
<span class="robot-card-desc" data-i18n="settings.robots.wechat.subtitle">扫码绑定个人微信,在手机端直接与 CyberStrikeAI 对话</span>
|
||||
</span>
|
||||
<span class="robot-card-meta">
|
||||
<span id="robot-card-wechat-status" class="robot-status-pill robot-status-pill--idle" data-i18n="settings.robots.statusNotConfigured">未配置</span>
|
||||
<span class="robot-card-action" data-i18n="settings.robots.configure">配置</span>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" class="robot-card" data-robot-card="wecom" onclick="openRobotEditor('wecom')">
|
||||
<span class="robot-card-main">
|
||||
<span class="robot-card-title" data-i18n="settings.robots.wecom.title">企业微信</span>
|
||||
<span class="robot-card-desc" data-i18n="settings.robots.wecom.subtitle">HTTP 回调模式,适合企业内部应用机器人</span>
|
||||
</span>
|
||||
<span class="robot-card-meta">
|
||||
<span id="robot-card-wecom-status" class="robot-status-pill robot-status-pill--idle" data-i18n="settings.robots.statusNotConfigured">未配置</span>
|
||||
<span class="robot-card-action" data-i18n="settings.robots.configure">配置</span>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" class="robot-card" data-robot-card="dingtalk" onclick="openRobotEditor('dingtalk')">
|
||||
<span class="robot-card-main">
|
||||
<span class="robot-card-title" data-i18n="settings.robots.dingtalk.title">钉钉</span>
|
||||
<span class="robot-card-desc" data-i18n="settings.robots.dingtalk.subtitle">企业内部应用机器人,使用 Stream 长连接</span>
|
||||
</span>
|
||||
<span class="robot-card-meta">
|
||||
<span id="robot-card-dingtalk-status" class="robot-status-pill robot-status-pill--idle" data-i18n="settings.robots.statusNotConfigured">未配置</span>
|
||||
<span class="robot-card-action" data-i18n="settings.robots.configure">配置</span>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" class="robot-card" data-robot-card="lark" onclick="openRobotEditor('lark')">
|
||||
<span class="robot-card-main">
|
||||
<span class="robot-card-title" data-i18n="settings.robots.lark.title">飞书 (Lark)</span>
|
||||
<span class="robot-card-desc" data-i18n="settings.robots.lark.subtitle">飞书企业应用机器人,支持单聊与群聊 @</span>
|
||||
</span>
|
||||
<span class="robot-card-meta">
|
||||
<span id="robot-card-lark-status" class="robot-status-pill robot-status-pill--idle" data-i18n="settings.robots.statusNotConfigured">未配置</span>
|
||||
<span class="robot-card-action" data-i18n="settings.robots.configure">配置</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="robot-editor-empty" class="robot-editor-empty">
|
||||
<h4 data-i18n="settings.robots.emptyTitle">选择一个机器人开始配置</h4>
|
||||
<p data-i18n="settings.robots.emptyDesc">从上方列表点击已有平台,或点击“新建机器人”按类型创建。</p>
|
||||
</div>
|
||||
|
||||
<!-- 微信 / iLink -->
|
||||
<div class="settings-subsection robot-wechat-card" id="robot-wechat-subsection">
|
||||
<div class="settings-subsection robot-editor-panel robot-wechat-card" id="robot-wechat-subsection" data-robot-editor="wechat" hidden>
|
||||
<div class="robot-wechat-header">
|
||||
<div class="robot-wechat-header-text">
|
||||
<h4 data-i18n="settings.robots.wechat.title">微信 / iLink</h4>
|
||||
@@ -3375,7 +3440,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 企业微信 -->
|
||||
<div class="settings-subsection">
|
||||
<div class="settings-subsection robot-editor-panel" data-robot-editor="wecom" hidden>
|
||||
<h4 data-i18n="settings.robots.wecom.title">企业微信</h4>
|
||||
<div class="settings-form">
|
||||
<div class="form-group">
|
||||
@@ -3409,7 +3474,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 钉钉 -->
|
||||
<div class="settings-subsection">
|
||||
<div class="settings-subsection robot-editor-panel" data-robot-editor="dingtalk" hidden>
|
||||
<h4 data-i18n="settings.robots.dingtalk.title">钉钉</h4>
|
||||
<div class="settings-form">
|
||||
<div class="form-group">
|
||||
@@ -3432,7 +3497,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 飞书 -->
|
||||
<div class="settings-subsection">
|
||||
<div class="settings-subsection robot-editor-panel" data-robot-editor="lark" hidden>
|
||||
<h4 data-i18n="settings.robots.lark.title">飞书 (Lark)</h4>
|
||||
<div class="settings-form">
|
||||
<div class="form-group">
|
||||
@@ -3457,6 +3522,36 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="robot-create-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="robot-create-title">
|
||||
<div class="modal-content robot-create-modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 id="robot-create-title" data-i18n="settings.robots.createTitle">新建机器人</h2>
|
||||
<button type="button" class="modal-close" onclick="closeRobotCreateModal()" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="settings-description" data-i18n="settings.robots.createDesc">选择机器人类型后进入专属配置。当前版本每个平台支持一个内置机器人配置。</p>
|
||||
<div class="robot-type-grid">
|
||||
<button type="button" class="robot-type-option" onclick="selectRobotType('wechat')">
|
||||
<span data-i18n="settings.robots.wechat.title">微信 / iLink</span>
|
||||
<small data-i18n="settings.robots.wechat.subtitle">扫码绑定个人微信</small>
|
||||
</button>
|
||||
<button type="button" class="robot-type-option" onclick="selectRobotType('wecom')">
|
||||
<span data-i18n="settings.robots.wecom.title">企业微信</span>
|
||||
<small data-i18n="settings.robots.wecom.subtitle">HTTP 回调模式</small>
|
||||
</button>
|
||||
<button type="button" class="robot-type-option" onclick="selectRobotType('dingtalk')">
|
||||
<span data-i18n="settings.robots.dingtalk.title">钉钉</span>
|
||||
<small data-i18n="settings.robots.dingtalk.subtitle">Stream 长连接</small>
|
||||
</button>
|
||||
<button type="button" class="robot-type-option" onclick="selectRobotType('lark')">
|
||||
<span data-i18n="settings.robots.lark.title">飞书 (Lark)</span>
|
||||
<small data-i18n="settings.robots.lark.subtitle">企业应用机器人</small>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-subsection">
|
||||
<h4 data-i18n="settingsRobotsExtra.botCommandsTitle">机器人命令说明</h4>
|
||||
<p class="settings-description" data-i18n="settingsRobotsExtra.botCommandsDesc">在对话中可发送以下命令(支持中英文):</p>
|
||||
|
||||
Reference in New Issue
Block a user