mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-07-07 13:07:57 +02:00
Add files via upload
This commit is contained in:
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user