mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-12 02:37:18 +02:00
Add files via upload
This commit is contained in:
7 files changed
+1680
-13
No files matched your search
@@ -5108,6 +5108,146 @@ html[data-theme="dark"] .openapi-doc-btn:hover {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workflow-agent-io {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-agent-input {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: color-mix(in srgb, var(--bg-tertiary) 70%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-agent-input summary {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto auto minmax(0, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-agent-input summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-agent-input summary::before {
|
||||||
|
content: '▸';
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-agent-input[open] summary::before {
|
||||||
|
content: '▾';
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-agent-io-label {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-agent-input code {
|
||||||
|
color: var(--accent-color);
|
||||||
|
background: color-mix(in srgb, var(--accent-color) 12%, transparent);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--accent-color) 24%, transparent);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-agent-input-summary {
|
||||||
|
color: var(--text-muted);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-agent-input pre {
|
||||||
|
margin: 0 10px 10px;
|
||||||
|
max-height: 180px;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: 10px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-agent-empty {
|
||||||
|
margin: 0 10px 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-agent-output {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-agent-output-body {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-condition-result,
|
||||||
|
.workflow-branch-detail {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-condition-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-condition-result code,
|
||||||
|
.workflow-branch-detail code {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg-secondary, rgba(127, 127, 127, 0.12));
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-condition-branch {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-condition-branch.is-true {
|
||||||
|
color: #86efac;
|
||||||
|
background: rgba(34, 197, 94, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-condition-branch.is-false {
|
||||||
|
color: #fcd34d;
|
||||||
|
background: rgba(245, 158, 11, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item-workflow_branch_taken .timeline-item-title {
|
||||||
|
color: #86efac;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item-workflow_branch_skipped .timeline-item-title {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
/* 整页 HTML(如 HTTP 探测响应)在时间线中仅以转义文本展示 */
|
/* 整页 HTML(如 HTTP 探测响应)在时间线中仅以转义文本展示 */
|
||||||
.timeline-item-content .sanitized-raw-html-fallback {
|
.timeline-item-content .sanitized-raw-html-fallback {
|
||||||
max-height: 320px;
|
max-height: 320px;
|
||||||
@@ -30026,3 +30166,277 @@ html[data-theme="dark"] .form-group select {
|
|||||||
background-repeat: no-repeat !important;
|
background-repeat: no-repeat !important;
|
||||||
background-position: right 12px center !important;
|
background-position: right 12px center !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Workflow drag editor */
|
||||||
|
.workflow-page-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 280px minmax(0, 1fr) 320px;
|
||||||
|
gap: 14px;
|
||||||
|
min-height: calc(100vh - 150px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-sidebar,
|
||||||
|
.workflow-properties,
|
||||||
|
.workflow-main {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-panel,
|
||||||
|
.workflow-properties,
|
||||||
|
.workflow-meta-bar,
|
||||||
|
.workflow-canvas-wrap {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-panel {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-panel-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
max-height: 330px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-list-item {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-list-item:hover,
|
||||||
|
.workflow-list-item.is-active {
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
background: rgba(59, 130, 246, 0.10);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-list-title {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-list-meta {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-node-palette {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-node-palette button {
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px dashed var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-node-palette button:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-main {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto minmax(520px, 1fr);
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-meta-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-meta-fields {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(140px, 220px) minmax(180px, 260px) minmax(200px, 1fr) auto;
|
||||||
|
gap: 10px;
|
||||||
|
flex: 1;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-meta-fields label {
|
||||||
|
display: grid;
|
||||||
|
gap: 5px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-meta-fields input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 7px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-enabled-toggle {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px !important;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-toolbar .active {
|
||||||
|
background: var(--accent-color);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-canvas-wrap {
|
||||||
|
position: relative;
|
||||||
|
min-height: 520px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#workflow-canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 520px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 1px 1px, rgba(148, 163, 184, 0.22) 1px, transparent 0);
|
||||||
|
background-size: 22px 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-canvas-empty {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
pointer-events: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-properties {
|
||||||
|
padding-bottom: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-property-empty {
|
||||||
|
margin: 14px;
|
||||||
|
padding: 18px;
|
||||||
|
border: 1px dashed var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-property-empty--compact {
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-property-form {
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-config-hint {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-config-hint code {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg-secondary, rgba(127, 127, 127, 0.12));
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-custom-fields-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 14px 0 8px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-custom-fields {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-custom-field {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 0.8fr) minmax(0, 1fr) 32px;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-custom-field input {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 8px 9px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 7px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-custom-field button {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 7px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.workflow-page-content {
|
||||||
|
grid-template-columns: 240px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-properties {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-meta-bar,
|
||||||
|
.workflow-meta-fields {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
-1
@@ -2411,6 +2411,15 @@ function processDetailRowFingerprint(d) {
|
|||||||
return et + '\0' + msg + '\0' + dataKey;
|
return et + '\0' + msg + '\0' + dataKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compactWorkflowProcessDetails(details) {
|
||||||
|
if (!Array.isArray(details) || details.length === 0) return details || [];
|
||||||
|
return details.filter((detail) => {
|
||||||
|
const eventType = detail && detail.eventType ? String(detail.eventType) : '';
|
||||||
|
// workflow_node_start 已经表达了节点进入;这些事件只用于实时状态,落到详情里会让 Agent 节点看起来重复启动。
|
||||||
|
return eventType !== 'workflow_agent_start';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 渲染过程详情
|
// 渲染过程详情
|
||||||
// options.append=true 时分页追加;options.markLoaded=false 时保留 lazy 标记(分页加载中)
|
// options.append=true 时分页追加;options.markLoaded=false 时保留 lazy 标记(分页加载中)
|
||||||
function renderProcessDetails(messageId, processDetails, options) {
|
function renderProcessDetails(messageId, processDetails, options) {
|
||||||
@@ -2502,6 +2511,7 @@ function renderProcessDetails(messageId, processDetails, options) {
|
|||||||
if (typeof window.coalesceProcessDetailsToolPairs === 'function') {
|
if (typeof window.coalesceProcessDetailsToolPairs === 'function') {
|
||||||
processDetails = window.coalesceProcessDetailsToolPairs(processDetails);
|
processDetails = window.coalesceProcessDetailsToolPairs(processDetails);
|
||||||
}
|
}
|
||||||
|
processDetails = compactWorkflowProcessDetails(processDetails);
|
||||||
// 如果没有processDetails或为空,显示空状态
|
// 如果没有processDetails或为空,显示空状态
|
||||||
if (!processDetails || processDetails.length === 0) {
|
if (!processDetails || processDetails.length === 0) {
|
||||||
if (!appendMode) {
|
if (!appendMode) {
|
||||||
@@ -2529,7 +2539,40 @@ function renderProcessDetails(messageId, processDetails, options) {
|
|||||||
const agPx = processDetailAgentPrefix(data);
|
const agPx = processDetailAgentPrefix(data);
|
||||||
|
|
||||||
let itemTitle = title;
|
let itemTitle = title;
|
||||||
if (eventType === 'iteration') {
|
if (eventType === 'workflow_start') {
|
||||||
|
const name = data.workflowName || data.workflowId || '';
|
||||||
|
itemTitle = '🧭 工作流开始' + (name ? (' · ' + name) : '');
|
||||||
|
} else if (eventType === 'workflow_done') {
|
||||||
|
const name = data.workflowName || data.workflowId || '';
|
||||||
|
itemTitle = '✅ 工作流完成' + (name ? (' · ' + name) : '');
|
||||||
|
} else if (eventType === 'workflow_node_start') {
|
||||||
|
const label = data.label || title || data.nodeId || '';
|
||||||
|
itemTitle = '▶ 节点开始' + (label ? (' · ' + label) : '');
|
||||||
|
} else if (eventType === 'workflow_node_result') {
|
||||||
|
const label = data.label || data.nodeId || '';
|
||||||
|
const status = data.status || '';
|
||||||
|
const nodeType = data.nodeType != null ? String(data.nodeType).toLowerCase() : '';
|
||||||
|
if (nodeType === 'condition') {
|
||||||
|
const matched = data.matched === true || data.matched === 'true' || (data.output && (data.output.matched === true || data.output.matched === 'true'));
|
||||||
|
itemTitle = (matched ? '✅' : '🔀') + ' 条件判断' + (label ? (' · ' + label) : '') + ' → ' + (matched ? '是' : '否');
|
||||||
|
} else {
|
||||||
|
const icon = status === 'failed' ? '❌' : (status === 'skipped' ? '⏭️' : '✅');
|
||||||
|
itemTitle = icon + ' 节点完成' + (label ? (' · ' + label) : '') + (status ? ('(' + status + ')') : '');
|
||||||
|
}
|
||||||
|
} else if (eventType === 'workflow_branch_taken' || eventType === 'workflow_branch_skipped') {
|
||||||
|
const branch = data.branchLabel || '';
|
||||||
|
const target = data.targetLabel || data.targetId || '';
|
||||||
|
const taken = eventType === 'workflow_branch_taken';
|
||||||
|
itemTitle = (taken ? '➡️' : '⏭️') + (taken ? ' 执行分支' : ' 跳过分支') + (branch ? (' · ' + branch) : '') + (target ? (' → ' + target) : '');
|
||||||
|
} else if (eventType === 'workflow_tool_start') {
|
||||||
|
const tool = data.tool || data.toolName || '';
|
||||||
|
itemTitle = '🔧 工具节点' + (tool ? (' · ' + tool) : '');
|
||||||
|
} else if (eventType === 'workflow_agent_output') {
|
||||||
|
const label = data.label || data.nodeId || '';
|
||||||
|
itemTitle = '🤖 Agent 输出' + (label ? (' · ' + label) : '');
|
||||||
|
} else if (eventType === 'workflow_hitl_checkpoint') {
|
||||||
|
itemTitle = '🧑⚖️ 人工确认检查点';
|
||||||
|
} else if (eventType === 'iteration') {
|
||||||
const n = data.iteration || 1;
|
const n = data.iteration || 1;
|
||||||
if (data.orchestration === 'plan_execute' && data.einoScope === 'main') {
|
if (data.orchestration === 'plan_execute' && data.einoScope === 'main') {
|
||||||
const phase = typeof window.translatePlanExecuteAgentName === 'function'
|
const phase = typeof window.translatePlanExecuteAgentName === 'function'
|
||||||
|
|||||||
+188
-8
@@ -333,6 +333,19 @@ const responseStreamStateByProgressId = new Map();
|
|||||||
// 主通道当前迭代轮次缓存:progressId -> { iteration, orchestration }
|
// 主通道当前迭代轮次缓存:progressId -> { iteration, orchestration }
|
||||||
const mainIterationStateByProgressId = new Map();
|
const mainIterationStateByProgressId = new Map();
|
||||||
|
|
||||||
|
/** 图编排多 Agent 节点切换时清空流式聚合,避免推理/输出条目覆盖上一节点内容 */
|
||||||
|
function clearTimelineStreamStates(progressId) {
|
||||||
|
responseStreamStateByProgressId.delete(progressId);
|
||||||
|
thinkingStreamStateByProgressId.delete(progressId);
|
||||||
|
einoAgentReplyStreamStateByProgressId.delete(progressId);
|
||||||
|
const prefix = String(progressId) + '::';
|
||||||
|
for (const key of Array.from(toolResultStreamStateByKey.keys())) {
|
||||||
|
if (String(key).startsWith(prefix)) {
|
||||||
|
toolResultStreamStateByKey.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 同一段主通道流式输出(Eino 可能重复 response_start) */
|
/** 同一段主通道流式输出(Eino 可能重复 response_start) */
|
||||||
function sameMainResponseStreamMeta(a, b) {
|
function sameMainResponseStreamMeta(a, b) {
|
||||||
if (!a || !b) return false;
|
if (!a || !b) return false;
|
||||||
@@ -341,7 +354,10 @@ function sameMainResponseStreamMeta(a, b) {
|
|||||||
if (!agentA || agentA !== agentB) return false;
|
if (!agentA || agentA !== agentB) return false;
|
||||||
const orchA = String(a.orchestration != null ? a.orchestration : '').trim();
|
const orchA = String(a.orchestration != null ? a.orchestration : '').trim();
|
||||||
const orchB = String(b.orchestration != null ? b.orchestration : '').trim();
|
const orchB = String(b.orchestration != null ? b.orchestration : '').trim();
|
||||||
return orchA === orchB;
|
if (orchA !== orchB) return false;
|
||||||
|
const nodeA = String(a.workflowNodeId != null ? a.workflowNodeId : '').trim();
|
||||||
|
const nodeB = String(b.workflowNodeId != null ? b.workflowNodeId : '').trim();
|
||||||
|
return nodeA === nodeB;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveMainIterationTag(progressId, responseData) {
|
function resolveMainIterationTag(progressId, responseData) {
|
||||||
@@ -366,7 +382,8 @@ function buildMainResponseStreamIdentity(progressId, responseData) {
|
|||||||
const agent = String(d.einoAgent != null ? d.einoAgent : '').trim();
|
const agent = String(d.einoAgent != null ? d.einoAgent : '').trim();
|
||||||
const orch = String(d.orchestration != null ? d.orchestration : '').trim();
|
const orch = String(d.orchestration != null ? d.orchestration : '').trim();
|
||||||
const iterTag = resolveMainIterationTag(progressId, d);
|
const iterTag = resolveMainIterationTag(progressId, d);
|
||||||
return agent + '|' + orch + '|iter=' + iterTag;
|
const nodeId = String(d.workflowNodeId != null ? d.workflowNodeId : '').trim();
|
||||||
|
return agent + '|' + orch + '|iter=' + iterTag + '|wfNode=' + nodeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractIterationTagFromStreamIdentity(identity) {
|
function extractIterationTagFromStreamIdentity(identity) {
|
||||||
@@ -1747,13 +1764,18 @@ function handleStreamEvent(event, progressElement, progressId,
|
|||||||
if (scope !== 'sub') {
|
if (scope !== 'sub') {
|
||||||
const prevMainIter = mainIterationStateByProgressId.get(String(progressId));
|
const prevMainIter = mainIterationStateByProgressId.get(String(progressId));
|
||||||
const prevN = prevMainIter && prevMainIter.iteration != null ? prevMainIter.iteration : null;
|
const prevN = prevMainIter && prevMainIter.iteration != null ? prevMainIter.iteration : null;
|
||||||
|
const prevNode = prevMainIter && prevMainIter.workflowNodeId != null
|
||||||
|
? String(prevMainIter.workflowNodeId).trim()
|
||||||
|
: '';
|
||||||
|
const curNode = d.workflowNodeId != null ? String(d.workflowNodeId).trim() : '';
|
||||||
mainIterationStateByProgressId.set(String(progressId), {
|
mainIterationStateByProgressId.set(String(progressId), {
|
||||||
iteration: n,
|
iteration: n,
|
||||||
orchestration: d.orchestration != null ? d.orchestration : ''
|
orchestration: d.orchestration != null ? d.orchestration : '',
|
||||||
|
workflowNodeId: curNode
|
||||||
});
|
});
|
||||||
// 主通道进入新轮次后不复用上一轮的「执行输出」时间线条目
|
// 主通道进入新轮次或图编排切换到新 Agent 节点后,不复用上一段的流式时间线条目
|
||||||
if (prevN != null && prevN !== n) {
|
if (prevN != null && (n < prevN || prevN !== n || (curNode && prevNode && curNode !== prevNode))) {
|
||||||
responseStreamStateByProgressId.delete(progressId);
|
clearTimelineStreamStates(progressId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let iterTitle;
|
let iterTitle;
|
||||||
@@ -1785,6 +1807,109 @@ function handleStreamEvent(event, progressElement, progressId,
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'workflow_start': {
|
||||||
|
const d = event.data || {};
|
||||||
|
const name = d.workflowName || d.workflowId || '';
|
||||||
|
addTimelineItem(timeline, 'workflow_start', {
|
||||||
|
title: '🧭 工作流开始' + (name ? (' · ' + name) : ''),
|
||||||
|
message: event.message || '',
|
||||||
|
data: d
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'workflow_done': {
|
||||||
|
const d = event.data || {};
|
||||||
|
const name = d.workflowName || d.workflowId || '';
|
||||||
|
addTimelineItem(timeline, 'workflow_done', {
|
||||||
|
title: '✅ 工作流完成' + (name ? (' · ' + name) : ''),
|
||||||
|
message: event.message || '',
|
||||||
|
data: d
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'workflow_node_start': {
|
||||||
|
const d = event.data || {};
|
||||||
|
const label = d.label || d.nodeId || '';
|
||||||
|
const nodeType = d.nodeType != null ? String(d.nodeType).toLowerCase() : '';
|
||||||
|
if (nodeType === 'agent') {
|
||||||
|
clearTimelineStreamStates(progressId);
|
||||||
|
}
|
||||||
|
addTimelineItem(timeline, 'workflow_node_start', {
|
||||||
|
title: '▶ 节点开始' + (label ? (' · ' + label) : ''),
|
||||||
|
message: event.message || '',
|
||||||
|
data: d
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'workflow_node_result': {
|
||||||
|
const d = event.data || {};
|
||||||
|
const label = d.label || d.nodeId || '';
|
||||||
|
const status = d.status || '';
|
||||||
|
const nodeType = d.nodeType != null ? String(d.nodeType).toLowerCase() : '';
|
||||||
|
let title;
|
||||||
|
if (nodeType === 'condition') {
|
||||||
|
const matched = d.matched === true || d.matched === 'true' || (d.output && (d.output.matched === true || d.output.matched === 'true'));
|
||||||
|
title = (matched ? '✅' : '🔀') + ' 条件判断' + (label ? (' · ' + label) : '') + ' → ' + (matched ? '是' : '否');
|
||||||
|
} else {
|
||||||
|
const icon = status === 'failed' ? '❌' : (status === 'skipped' ? '⏭️' : '✅');
|
||||||
|
title = icon + ' 节点完成' + (label ? (' · ' + label) : '') + (status ? ('(' + status + ')') : '');
|
||||||
|
}
|
||||||
|
addTimelineItem(timeline, 'workflow_node_result', {
|
||||||
|
title: title,
|
||||||
|
message: event.message || '',
|
||||||
|
data: d
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'workflow_branch_taken':
|
||||||
|
case 'workflow_branch_skipped': {
|
||||||
|
const d = event.data || {};
|
||||||
|
const branch = d.branchLabel || '';
|
||||||
|
const target = d.targetLabel || d.targetId || '';
|
||||||
|
const taken = event.type === 'workflow_branch_taken';
|
||||||
|
addTimelineItem(timeline, event.type, {
|
||||||
|
title: (taken ? '➡️' : '⏭️') + (taken ? ' 执行分支' : ' 跳过分支') + (branch ? (' · ' + branch) : '') + (target ? (' → ' + target) : ''),
|
||||||
|
message: event.message || '',
|
||||||
|
data: d
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'workflow_tool_start': {
|
||||||
|
const d = event.data || {};
|
||||||
|
const tool = d.tool || d.toolName || '';
|
||||||
|
addTimelineItem(timeline, 'workflow_tool_start', {
|
||||||
|
title: '🔧 工具节点' + (tool ? (' · ' + tool) : ''),
|
||||||
|
message: event.message || '',
|
||||||
|
data: d
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'workflow_agent_output': {
|
||||||
|
const d = event.data || {};
|
||||||
|
const label = d.label || d.nodeId || '';
|
||||||
|
addTimelineItem(timeline, 'workflow_agent_output', {
|
||||||
|
title: '🤖 Agent 输出' + (label ? (' · ' + label) : ''),
|
||||||
|
message: event.message || '',
|
||||||
|
data: d
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'workflow_hitl_checkpoint': {
|
||||||
|
addTimelineItem(timeline, 'workflow_hitl_checkpoint', {
|
||||||
|
title: '🧑⚖️ 人工确认检查点',
|
||||||
|
message: event.message || '',
|
||||||
|
data: event.data || {}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'eino_trace_run':
|
case 'eino_trace_run':
|
||||||
case 'eino_trace_start':
|
case 'eino_trace_start':
|
||||||
case 'eino_trace_end':
|
case 'eino_trace_end':
|
||||||
@@ -3262,6 +3387,34 @@ function updateToolCallStatus(progressId, toolCallId, status) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 添加时间线项目
|
// 添加时间线项目
|
||||||
|
function buildWorkflowConditionResultHtml(data) {
|
||||||
|
const output = (data && data.output) || {};
|
||||||
|
const expr = (data && data.expression) || output.condition || '';
|
||||||
|
const matched = (data && (data.matched === true || data.matched === 'true'))
|
||||||
|
|| output.matched === true || output.matched === 'true';
|
||||||
|
const branchText = matched ? '是(true)' : '否(false)';
|
||||||
|
const branchClass = matched ? 'is-true' : 'is-false';
|
||||||
|
return `<div class="timeline-item-content workflow-condition-result">
|
||||||
|
<div class="workflow-condition-row">
|
||||||
|
<span class="workflow-agent-io-label">表达式</span>
|
||||||
|
<code>${escapeHtml(String(expr || '(空)'))}</code>
|
||||||
|
</div>
|
||||||
|
<div class="workflow-condition-row">
|
||||||
|
<span class="workflow-agent-io-label">结果</span>
|
||||||
|
<span class="workflow-condition-branch ${branchClass}">${escapeHtml(branchText)}</span>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWorkflowBranchDetailHtml(data) {
|
||||||
|
const cond = (data && data.edgeCondition) || '';
|
||||||
|
if (!cond) return '';
|
||||||
|
return `<div class="timeline-item-content workflow-branch-detail">
|
||||||
|
<span class="workflow-agent-io-label">连线条件</span>
|
||||||
|
<code>${escapeHtml(cond)}</code>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
function addTimelineItem(timeline, type, options) {
|
function addTimelineItem(timeline, type, options) {
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
// 生成唯一ID
|
// 生成唯一ID
|
||||||
@@ -3382,8 +3535,35 @@ function addTimelineItem(timeline, type, options) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (type === 'eino_agent_reply' && options.message) {
|
} else if ((type === 'eino_agent_reply' || type === 'workflow_agent_output') && options.message) {
|
||||||
content += `<div class="timeline-item-content">${formatMarkdown(options.message, timelineMarkdownOpts)}</div>`;
|
let prefix = '';
|
||||||
|
if (type === 'workflow_agent_output' && options.data) {
|
||||||
|
const source = options.data.inputSource || '';
|
||||||
|
const preview = options.data.inputPreview || '';
|
||||||
|
if (source || preview) {
|
||||||
|
const previewText = String(preview || '').trim();
|
||||||
|
const summaryPreview = previewText.length > 80 ? (previewText.slice(0, 80) + '...') : previewText;
|
||||||
|
prefix = `<details class="workflow-agent-input">
|
||||||
|
<summary>
|
||||||
|
<span class="workflow-agent-io-label">输入</span>
|
||||||
|
${source ? `<code>${escapeHtml(source)}</code>` : ''}
|
||||||
|
${summaryPreview ? `<span class="workflow-agent-input-summary">${escapeHtml(summaryPreview)}</span>` : ''}
|
||||||
|
</summary>
|
||||||
|
${previewText ? `<pre>${escapeHtml(previewText)}</pre>` : '<div class="workflow-agent-empty">暂无输入预览</div>'}
|
||||||
|
</details>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const body = type === 'workflow_agent_output'
|
||||||
|
? `<div class="workflow-agent-output">
|
||||||
|
<div class="workflow-agent-io-label">输出</div>
|
||||||
|
<div class="workflow-agent-output-body">${formatMarkdown(options.message, timelineMarkdownOpts)}</div>
|
||||||
|
</div>`
|
||||||
|
: formatMarkdown(options.message, timelineMarkdownOpts);
|
||||||
|
content += `<div class="timeline-item-content workflow-agent-io">${prefix}${body}</div>`;
|
||||||
|
} else if (type === 'workflow_node_result' && options.data && String(options.data.nodeType || '').toLowerCase() === 'condition') {
|
||||||
|
content += buildWorkflowConditionResultHtml(options.data);
|
||||||
|
} else if ((type === 'workflow_branch_taken' || type === 'workflow_branch_skipped') && options.data) {
|
||||||
|
content += buildWorkflowBranchDetailHtml(options.data);
|
||||||
} else if (type === 'tool_result' && options.data) {
|
} else if (type === 'tool_result' && options.data) {
|
||||||
const data = options.data;
|
const data = options.data;
|
||||||
const isError = data.isError || !data.success;
|
const isError = data.isError || !data.success;
|
||||||
|
|||||||
+22
-1
@@ -1058,6 +1058,13 @@ async function showAddRoleModal() {
|
|||||||
document.getElementById('role-icon').value = '';
|
document.getElementById('role-icon').value = '';
|
||||||
document.getElementById('role-user-prompt').value = '';
|
document.getElementById('role-user-prompt').value = '';
|
||||||
document.getElementById('role-enabled').checked = true;
|
document.getElementById('role-enabled').checked = true;
|
||||||
|
if (typeof loadWorkflowOptionsForRoleModal === 'function') {
|
||||||
|
await loadWorkflowOptionsForRoleModal('');
|
||||||
|
}
|
||||||
|
const workflowPolicy = document.getElementById('role-workflow-policy');
|
||||||
|
if (workflowPolicy) {
|
||||||
|
workflowPolicy.value = 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
// 添加角色时:显示工具选择界面,隐藏默认角色提示
|
// 添加角色时:显示工具选择界面,隐藏默认角色提示
|
||||||
const toolsSection = document.getElementById('role-tools-section');
|
const toolsSection = document.getElementById('role-tools-section');
|
||||||
@@ -1144,6 +1151,13 @@ async function editRole(roleName) {
|
|||||||
document.getElementById('role-icon').value = iconValue;
|
document.getElementById('role-icon').value = iconValue;
|
||||||
document.getElementById('role-user-prompt').value = role.user_prompt || '';
|
document.getElementById('role-user-prompt').value = role.user_prompt || '';
|
||||||
document.getElementById('role-enabled').checked = role.enabled !== false;
|
document.getElementById('role-enabled').checked = role.enabled !== false;
|
||||||
|
if (typeof loadWorkflowOptionsForRoleModal === 'function') {
|
||||||
|
await loadWorkflowOptionsForRoleModal(role.workflow_id || '');
|
||||||
|
}
|
||||||
|
const workflowPolicy = document.getElementById('role-workflow-policy');
|
||||||
|
if (workflowPolicy) {
|
||||||
|
workflowPolicy.value = role.workflow_policy || 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否为默认角色
|
// 检查是否为默认角色
|
||||||
const isDefaultRole = roleName === '默认';
|
const isDefaultRole = roleName === '默认';
|
||||||
@@ -1398,6 +1412,10 @@ async function saveRole() {
|
|||||||
}
|
}
|
||||||
const userPrompt = document.getElementById('role-user-prompt').value.trim();
|
const userPrompt = document.getElementById('role-user-prompt').value.trim();
|
||||||
const enabled = document.getElementById('role-enabled').checked;
|
const enabled = document.getElementById('role-enabled').checked;
|
||||||
|
const workflowIdEl = document.getElementById('role-workflow-id');
|
||||||
|
const workflowPolicyEl = document.getElementById('role-workflow-policy');
|
||||||
|
const workflowId = workflowIdEl ? workflowIdEl.value.trim() : '';
|
||||||
|
const workflowPolicy = workflowPolicyEl ? workflowPolicyEl.value.trim() : 'auto';
|
||||||
|
|
||||||
const isEdit = document.getElementById('role-name').disabled;
|
const isEdit = document.getElementById('role-name').disabled;
|
||||||
|
|
||||||
@@ -1504,7 +1522,10 @@ async function saveRole() {
|
|||||||
icon: icon || undefined, // 如果为空字符串,则不发送该字段
|
icon: icon || undefined, // 如果为空字符串,则不发送该字段
|
||||||
user_prompt: userPrompt,
|
user_prompt: userPrompt,
|
||||||
tools: tools, // 默认角色为空数组,表示使用所有工具
|
tools: tools, // 默认角色为空数组,表示使用所有工具
|
||||||
enabled: enabled
|
enabled: enabled,
|
||||||
|
workflow_id: workflowId || undefined,
|
||||||
|
workflow_version: workflowId ? 'latest' : undefined,
|
||||||
|
workflow_policy: workflowId ? (workflowPolicy || 'auto') : undefined
|
||||||
};
|
};
|
||||||
const url = isEdit ? `/api/roles/${encodeURIComponent(name)}` : '/api/roles';
|
const url = isEdit ? `/api/roles/${encodeURIComponent(name)}` : '/api/roles';
|
||||||
const method = isEdit ? 'PUT' : 'POST';
|
const method = isEdit ? 'PUT' : 'POST';
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ function initRouter() {
|
|||||||
const hashParts = hash.split('?');
|
const hashParts = hash.split('?');
|
||||||
let pageId = hashParts[0];
|
let pageId = hashParts[0];
|
||||||
if (pageId === 'c2') pageId = 'c2-listeners';
|
if (pageId === 'c2') pageId = 'c2-listeners';
|
||||||
if (pageId && ['dashboard', 'chat', 'hitl', 'info-collect', 'projects', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'tasks', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
|
if (pageId && ['dashboard', 'chat', 'hitl', 'info-collect', 'projects', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'workflows', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'tasks', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
|
||||||
switchPage(pageId);
|
switchPage(pageId);
|
||||||
if (pageId === 'chat') {
|
if (pageId === 'chat') {
|
||||||
scheduleChatConversationFromHash(500);
|
scheduleChatConversationFromHash(500);
|
||||||
@@ -449,6 +449,11 @@ async function initPage(pageId) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'workflows':
|
||||||
|
if (typeof refreshWorkflows === 'function') {
|
||||||
|
refreshWorkflows();
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'skills-monitor':
|
case 'skills-monitor':
|
||||||
// 初始化Skills状态监控页面
|
// 初始化Skills状态监控页面
|
||||||
if (typeof loadSkillsMonitor === 'function') {
|
if (typeof loadSkillsMonitor === 'function') {
|
||||||
@@ -510,7 +515,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
let pageId = hashParts[0];
|
let pageId = hashParts[0];
|
||||||
|
|
||||||
if (pageId === 'c2') pageId = 'c2-listeners';
|
if (pageId === 'c2') pageId = 'c2-listeners';
|
||||||
if (pageId && ['dashboard', 'chat', 'hitl', 'info-collect', 'projects', 'tasks', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
|
if (pageId && ['dashboard', 'chat', 'hitl', 'info-collect', 'projects', 'tasks', 'workflows', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
|
||||||
switchPage(pageId);
|
switchPage(pageId);
|
||||||
if (pageId === 'chat') {
|
if (pageId === 'chat') {
|
||||||
scheduleChatConversationFromHash(200);
|
scheduleChatConversationFromHash(200);
|
||||||
@@ -569,4 +574,3 @@ function initConversationSidebarState() {
|
|||||||
|
|
||||||
// 导出函数供其他脚本使用(与上方尽早绑定保持一致,便于外部脚本探测)
|
// 导出函数供其他脚本使用(与上方尽早绑定保持一致,便于外部脚本探测)
|
||||||
window.currentPage = function() { return currentPage; };
|
window.currentPage = function() { return currentPage; };
|
||||||
|
|
||||||
@@ -0,0 +1,891 @@
|
|||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
let workflows = [];
|
||||||
|
let currentWorkflowId = '';
|
||||||
|
let cy = null;
|
||||||
|
let nodeSeq = 1;
|
||||||
|
let edgeSeq = 1;
|
||||||
|
let connectMode = false;
|
||||||
|
let connectSourceId = '';
|
||||||
|
let selectedElement = null;
|
||||||
|
let workflowToolOptions = [];
|
||||||
|
let workflowToolsLoaded = false;
|
||||||
|
|
||||||
|
const NODE_LABELS = {
|
||||||
|
start: '开始',
|
||||||
|
tool: '工具',
|
||||||
|
agent: 'Agent',
|
||||||
|
condition: '条件',
|
||||||
|
hitl: '审批',
|
||||||
|
output: '输出',
|
||||||
|
end: '结束'
|
||||||
|
};
|
||||||
|
|
||||||
|
const AGENT_MODES = ['eino_single', 'deep', 'plan_execute', 'supervisor'];
|
||||||
|
|
||||||
|
function esc(text) {
|
||||||
|
if (typeof escapeHtml === 'function') return escapeHtml(text == null ? '' : String(text));
|
||||||
|
return String(text == null ? '' : text)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultGraph() {
|
||||||
|
return { nodes: [], edges: [], config: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultConfigForType(type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'start':
|
||||||
|
return { input_keys: 'message, conversationId, projectId' };
|
||||||
|
case 'tool':
|
||||||
|
return { tool_name: '', arguments: '{}', timeout_seconds: '' };
|
||||||
|
case 'agent':
|
||||||
|
return { agent_mode: 'eino_single', input_source: '{{previous.output}}', instruction: '', output_key: 'agent_result' };
|
||||||
|
case 'condition':
|
||||||
|
return { expression: '{{previous.output}} != ""' };
|
||||||
|
case 'hitl':
|
||||||
|
return { prompt: '请审批该步骤是否继续执行', reviewer: 'human' };
|
||||||
|
case 'output':
|
||||||
|
return { output_key: 'result', source: '{{previous.output}}' };
|
||||||
|
case 'end':
|
||||||
|
return { result_template: '{{outputs.result}}' };
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function configWithDefaults(type, config) {
|
||||||
|
return Object.assign(defaultConfigForType(type), config && typeof config === 'object' ? config : {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseGraph(raw) {
|
||||||
|
if (!raw) return defaultGraph();
|
||||||
|
let graph = raw;
|
||||||
|
if (typeof raw === 'string') {
|
||||||
|
try {
|
||||||
|
graph = JSON.parse(raw);
|
||||||
|
} catch (_) {
|
||||||
|
return defaultGraph();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
nodes: Array.isArray(graph.nodes) ? graph.nodes : [],
|
||||||
|
edges: Array.isArray(graph.edges) ? graph.edges : [],
|
||||||
|
config: graph.config && typeof graph.config === 'object' ? graph.config : {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function graphToElements(graph) {
|
||||||
|
const nodes = (graph.nodes || []).map((node, index) => ({
|
||||||
|
group: 'nodes',
|
||||||
|
data: {
|
||||||
|
id: node.id || `node-${index + 1}`,
|
||||||
|
label: node.label || NODE_LABELS[node.type] || node.id || `节点 ${index + 1}`,
|
||||||
|
type: node.type || 'tool',
|
||||||
|
config: configWithDefaults(node.type || 'tool', node.config)
|
||||||
|
},
|
||||||
|
position: node.position || { x: 120 + index * 80, y: 120 + index * 40 }
|
||||||
|
}));
|
||||||
|
const edges = (graph.edges || []).map((edge, index) => ({
|
||||||
|
group: 'edges',
|
||||||
|
data: {
|
||||||
|
id: edge.id || `edge-${index + 1}`,
|
||||||
|
source: edge.source,
|
||||||
|
target: edge.target,
|
||||||
|
label: edge.label || '',
|
||||||
|
config: edge.config && typeof edge.config === 'object' ? edge.config : {}
|
||||||
|
}
|
||||||
|
})).filter(edge => edge.data.source && edge.data.target);
|
||||||
|
return nodes.concat(edges);
|
||||||
|
}
|
||||||
|
|
||||||
|
function elementsToGraph() {
|
||||||
|
if (!cy) return defaultGraph();
|
||||||
|
return {
|
||||||
|
nodes: cy.nodes().map(node => ({
|
||||||
|
id: node.id(),
|
||||||
|
type: node.data('type') || 'tool',
|
||||||
|
label: node.data('label') || '',
|
||||||
|
position: node.position(),
|
||||||
|
config: node.data('config') || {}
|
||||||
|
})),
|
||||||
|
edges: cy.edges().map(edge => ({
|
||||||
|
id: edge.id(),
|
||||||
|
source: edge.source().id(),
|
||||||
|
target: edge.target().id(),
|
||||||
|
label: edge.data('label') || '',
|
||||||
|
config: edge.data('config') || {}
|
||||||
|
})),
|
||||||
|
config: { schema_version: 1 }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateEmptyState() {
|
||||||
|
const empty = document.getElementById('workflow-canvas-empty');
|
||||||
|
if (!empty || !cy) return;
|
||||||
|
empty.style.display = cy.nodes().length ? 'none' : 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function initCy() {
|
||||||
|
const container = document.getElementById('workflow-canvas');
|
||||||
|
if (!container || typeof cytoscape !== 'function') return;
|
||||||
|
if (cy) {
|
||||||
|
cy.resize();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cy = cytoscape({
|
||||||
|
container,
|
||||||
|
elements: [],
|
||||||
|
wheelSensitivity: 0.18,
|
||||||
|
style: [
|
||||||
|
{
|
||||||
|
selector: 'node',
|
||||||
|
style: {
|
||||||
|
'shape': 'round-rectangle',
|
||||||
|
'width': 150,
|
||||||
|
'height': 52,
|
||||||
|
'background-color': '#1d4ed8',
|
||||||
|
'border-width': 1,
|
||||||
|
'border-color': '#60a5fa',
|
||||||
|
'label': 'data(label)',
|
||||||
|
'color': '#e5edff',
|
||||||
|
'font-size': 13,
|
||||||
|
'font-weight': 700,
|
||||||
|
'text-valign': 'center',
|
||||||
|
'text-halign': 'center',
|
||||||
|
'text-wrap': 'wrap',
|
||||||
|
'text-max-width': 132
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ selector: 'node[type="start"]', style: { 'background-color': '#047857', 'border-color': '#34d399' } },
|
||||||
|
{ selector: 'node[type="tool"]', style: { 'background-color': '#1d4ed8', 'border-color': '#60a5fa' } },
|
||||||
|
{ selector: 'node[type="agent"]', style: { 'background-color': '#7c3aed', 'border-color': '#c4b5fd' } },
|
||||||
|
{ selector: 'node[type="condition"]', style: { 'shape': 'diamond', 'background-color': '#b45309', 'border-color': '#fbbf24', 'width': 118, 'height': 86 } },
|
||||||
|
{ selector: 'node[type="hitl"]', style: { 'background-color': '#0f766e', 'border-color': '#5eead4' } },
|
||||||
|
{ selector: 'node[type="output"]', style: { 'background-color': '#4338ca', 'border-color': '#a5b4fc' } },
|
||||||
|
{ selector: 'node[type="end"]', style: { 'background-color': '#be123c', 'border-color': '#fb7185' } },
|
||||||
|
{
|
||||||
|
selector: 'edge',
|
||||||
|
style: {
|
||||||
|
'width': 2,
|
||||||
|
'line-color': '#64748b',
|
||||||
|
'target-arrow-color': '#64748b',
|
||||||
|
'target-arrow-shape': 'triangle',
|
||||||
|
'curve-style': 'bezier',
|
||||||
|
'label': 'data(label)',
|
||||||
|
'font-size': 11,
|
||||||
|
'color': '#cbd5e1',
|
||||||
|
'text-background-color': '#0f172a',
|
||||||
|
'text-background-opacity': 0.8,
|
||||||
|
'text-background-padding': 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: ':selected',
|
||||||
|
style: {
|
||||||
|
'border-width': 3,
|
||||||
|
'border-color': '#93c5fd',
|
||||||
|
'line-color': '#93c5fd',
|
||||||
|
'target-arrow-color': '#93c5fd'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: '.connect-source',
|
||||||
|
style: {
|
||||||
|
'border-width': 4,
|
||||||
|
'border-color': '#fbbf24'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
layout: { name: 'preset' }
|
||||||
|
});
|
||||||
|
cy.on('tap', 'node', event => {
|
||||||
|
if (connectMode) {
|
||||||
|
handleConnectTap(event.target);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectWorkflowElement(event.target);
|
||||||
|
});
|
||||||
|
cy.on('tap', 'edge', event => {
|
||||||
|
selectWorkflowElement(event.target);
|
||||||
|
});
|
||||||
|
cy.on('tap', event => {
|
||||||
|
if (event.target === cy) {
|
||||||
|
if (connectMode) clearConnectSource();
|
||||||
|
selectWorkflowElement(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cy.on('add remove', updateEmptyState);
|
||||||
|
document.addEventListener('keydown', event => {
|
||||||
|
const active = document.activeElement;
|
||||||
|
const editing = active && ['INPUT', 'TEXTAREA', 'SELECT'].includes(active.tagName);
|
||||||
|
if (editing) return;
|
||||||
|
if (typeof currentPage !== 'undefined' && currentPage !== 'workflows') return;
|
||||||
|
if (event.key === 'Delete' || event.key === 'Backspace') {
|
||||||
|
event.preventDefault();
|
||||||
|
deleteWorkflowSelection();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadWorkflows(includeDisabled) {
|
||||||
|
const response = await apiFetch(`/api/workflows?includeDisabled=${includeDisabled ? 'true' : 'false'}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
const err = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(err.error || '加载工作流失败');
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
workflows = data.workflows || [];
|
||||||
|
return workflows;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadWorkflowTools() {
|
||||||
|
if (workflowToolsLoaded) return workflowToolOptions;
|
||||||
|
const collected = [];
|
||||||
|
const seen = new Set();
|
||||||
|
let page = 1;
|
||||||
|
let totalPages = 1;
|
||||||
|
while (page <= totalPages && page <= 20) {
|
||||||
|
const response = await apiFetch(`/api/config/tools?page=${page}&page_size=100`);
|
||||||
|
if (!response.ok) break;
|
||||||
|
const data = await response.json();
|
||||||
|
totalPages = data.total_pages || 1;
|
||||||
|
(data.tools || []).forEach(tool => {
|
||||||
|
if (!tool || !tool.name) return;
|
||||||
|
const key = tool.is_external && tool.external_mcp ? `${tool.external_mcp}::${tool.name}` : tool.name;
|
||||||
|
if (seen.has(key)) return;
|
||||||
|
seen.add(key);
|
||||||
|
collected.push({ key, name: tool.name, enabled: tool.enabled !== false });
|
||||||
|
});
|
||||||
|
page += 1;
|
||||||
|
}
|
||||||
|
workflowToolOptions = collected;
|
||||||
|
workflowToolsLoaded = true;
|
||||||
|
return workflowToolOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWorkflowList() {
|
||||||
|
const list = document.getElementById('workflow-list');
|
||||||
|
if (!list) return;
|
||||||
|
if (!workflows.length) {
|
||||||
|
list.innerHTML = '<div class="empty-state">暂无图编排流程</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.innerHTML = workflows.map(wf => `
|
||||||
|
<button type="button" class="workflow-list-item ${wf.id === currentWorkflowId ? 'is-active' : ''}" onclick="selectWorkflow(decodeURIComponent('${encodeURIComponent(wf.id)}'))">
|
||||||
|
<span class="workflow-list-title">${esc(wf.name || wf.id)}</span>
|
||||||
|
<span class="workflow-list-meta">${esc(wf.id)} · v${wf.version || 1} · ${wf.enabled ? '启用' : '禁用'}</span>
|
||||||
|
</button>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextNodeId(type) {
|
||||||
|
while (cy && cy.getElementById(`node-${nodeSeq}`).length) nodeSeq += 1;
|
||||||
|
const id = `node-${nodeSeq}`;
|
||||||
|
nodeSeq += 1;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextEdgeId() {
|
||||||
|
while (cy && cy.getElementById(`edge-${edgeSeq}`).length) edgeSeq += 1;
|
||||||
|
const id = `edge-${edgeSeq}`;
|
||||||
|
edgeSeq += 1;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSequences(graph) {
|
||||||
|
nodeSeq = 1;
|
||||||
|
edgeSeq = 1;
|
||||||
|
(graph.nodes || []).forEach(node => {
|
||||||
|
const m = String(node.id || '').match(/^node-(\d+)$/);
|
||||||
|
if (m) nodeSeq = Math.max(nodeSeq, Number(m[1]) + 1);
|
||||||
|
});
|
||||||
|
(graph.edges || []).forEach(edge => {
|
||||||
|
const m = String(edge.id || '').match(/^edge-(\d+)$/);
|
||||||
|
if (m) edgeSeq = Math.max(edgeSeq, Number(m[1]) + 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillWorkflowForm(wf) {
|
||||||
|
initCy();
|
||||||
|
const idEl = document.getElementById('workflow-id');
|
||||||
|
const nameEl = document.getElementById('workflow-name');
|
||||||
|
const descEl = document.getElementById('workflow-description');
|
||||||
|
const enabledEl = document.getElementById('workflow-enabled');
|
||||||
|
if (!idEl || !nameEl || !descEl || !enabledEl || !cy) return;
|
||||||
|
idEl.value = wf.id || '';
|
||||||
|
idEl.disabled = !!wf.id;
|
||||||
|
nameEl.value = wf.name || '';
|
||||||
|
descEl.value = wf.description || '';
|
||||||
|
enabledEl.checked = wf.enabled !== false;
|
||||||
|
currentWorkflowId = wf.id || '';
|
||||||
|
const graph = parseGraph(wf.graph_json || wf.graph || defaultGraph());
|
||||||
|
resetSequences(graph);
|
||||||
|
cy.elements().remove();
|
||||||
|
cy.add(graphToElements(graph));
|
||||||
|
if (cy.nodes().length) {
|
||||||
|
layoutWorkflowGraph(false);
|
||||||
|
}
|
||||||
|
selectWorkflowElement(null);
|
||||||
|
updateEmptyState();
|
||||||
|
renderWorkflowList();
|
||||||
|
setTimeout(() => cy && cy.resize(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectWorkflowElement(ele) {
|
||||||
|
selectedElement = ele && ele.length ? ele : null;
|
||||||
|
const empty = document.getElementById('workflow-property-empty');
|
||||||
|
const form = document.getElementById('workflow-property-form');
|
||||||
|
const title = document.getElementById('workflow-property-title');
|
||||||
|
const deleteBtn = document.getElementById('workflow-property-delete-btn');
|
||||||
|
if (!empty || !form) return;
|
||||||
|
if (!selectedElement) {
|
||||||
|
empty.hidden = false;
|
||||||
|
form.hidden = true;
|
||||||
|
if (title) title.textContent = '属性';
|
||||||
|
if (deleteBtn) deleteBtn.hidden = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cy.elements().unselect();
|
||||||
|
selectedElement.select();
|
||||||
|
empty.hidden = true;
|
||||||
|
form.hidden = false;
|
||||||
|
if (title) title.textContent = selectedElement.isNode() ? '节点属性' : '连线属性';
|
||||||
|
if (deleteBtn) {
|
||||||
|
deleteBtn.hidden = false;
|
||||||
|
deleteBtn.textContent = selectedElement.isNode() ? '删除节点' : '删除连线';
|
||||||
|
}
|
||||||
|
const typeWrap = document.getElementById('workflow-prop-type-wrap');
|
||||||
|
const label = document.getElementById('workflow-prop-label');
|
||||||
|
const type = document.getElementById('workflow-prop-type');
|
||||||
|
label.value = selectedElement.data('label') || '';
|
||||||
|
if (selectedElement.isNode()) {
|
||||||
|
typeWrap.style.display = '';
|
||||||
|
type.value = selectedElement.data('type') || 'tool';
|
||||||
|
} else {
|
||||||
|
typeWrap.style.display = 'none';
|
||||||
|
}
|
||||||
|
renderTypedConfig(selectedElement);
|
||||||
|
renderCustomFields(stripTypedConfig(selectedElement));
|
||||||
|
}
|
||||||
|
|
||||||
|
function typedKeysForType(type) {
|
||||||
|
return new Set(Object.keys(defaultConfigForType(type)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripTypedConfig(ele) {
|
||||||
|
const cfg = Object.assign({}, ele.data('config') || {});
|
||||||
|
const typed = ele.isNode() ? typedKeysForType(ele.data('type') || 'tool') : new Set(['condition']);
|
||||||
|
typed.forEach(key => delete cfg[key]);
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
function typedField(id, label, value, placeholder) {
|
||||||
|
return `
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="${id}">${label}</label>
|
||||||
|
<input type="text" id="${id}" class="form-input" value="${esc(value || '')}" placeholder="${esc(placeholder || '')}" oninput="updateWorkflowTypedConfig()">
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function typedTextarea(id, label, value, placeholder) {
|
||||||
|
return `
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="${id}">${label}</label>
|
||||||
|
<textarea id="${id}" class="form-input" rows="4" placeholder="${esc(placeholder || '')}" oninput="updateWorkflowTypedConfig()">${esc(value || '')}</textarea>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTypedConfig(ele) {
|
||||||
|
const wrap = document.getElementById('workflow-typed-config');
|
||||||
|
if (!wrap || !ele) return;
|
||||||
|
const cfg = configWithDefaults(ele.isNode() ? ele.data('type') : 'edge', ele.data('config') || {});
|
||||||
|
if (!ele.isNode()) {
|
||||||
|
const sourceType = ele.source().data('type') || '';
|
||||||
|
const edgeHint = sourceType === 'condition'
|
||||||
|
? '{{previous.matched}} == "true"(是)或 == "false"(否)'
|
||||||
|
: '例如: {{previous.output}} == "ok"';
|
||||||
|
wrap.innerHTML = `
|
||||||
|
${typedField('workflow-edge-condition', '连线条件', cfg.condition || '', edgeHint)}
|
||||||
|
${sourceType === 'condition' ? '<p class="workflow-config-hint">从条件节点连出的第一条线默认为「是」分支,第二条为「否」分支;也可在此自定义条件。</p>' : ''}
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const type = ele.data('type') || 'tool';
|
||||||
|
switch (type) {
|
||||||
|
case 'start':
|
||||||
|
wrap.innerHTML = typedField('workflow-start-input-keys', '输入变量', cfg.input_keys, 'message, projectId');
|
||||||
|
break;
|
||||||
|
case 'tool':
|
||||||
|
wrap.innerHTML = `
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="workflow-tool-name">MCP 工具</label>
|
||||||
|
<select id="workflow-tool-name" onchange="updateWorkflowTypedConfig()">
|
||||||
|
<option value="">请选择工具</option>
|
||||||
|
${workflowToolOptions.map(tool => `<option value="${esc(tool.key)}" ${tool.key === cfg.tool_name ? 'selected' : ''}>${esc(tool.key)}${tool.enabled ? '' : '(未启用)'}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
${typedTextarea('workflow-tool-arguments', '参数模板', cfg.arguments, '{"target":"{{inputs.target}}"}')}
|
||||||
|
${typedField('workflow-tool-timeout', '超时秒数', cfg.timeout_seconds, '可选')}
|
||||||
|
`;
|
||||||
|
if (!workflowToolsLoaded) {
|
||||||
|
loadWorkflowTools().then(() => {
|
||||||
|
if (selectedElement === ele) renderTypedConfig(ele);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'agent':
|
||||||
|
wrap.innerHTML = `
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="workflow-agent-mode">Agent 模式</label>
|
||||||
|
<select id="workflow-agent-mode" onchange="updateWorkflowTypedConfig()">
|
||||||
|
${AGENT_MODES.map(mode => `<option value="${mode}" ${mode === cfg.agent_mode ? 'selected' : ''}>${mode}</option>`).join('')}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
${typedField('workflow-agent-input-source', '输入来源', cfg.input_source, '{{previous.output}}')}
|
||||||
|
${typedTextarea('workflow-agent-instruction', '节点指令', cfg.instruction, '描述该节点要完成的任务')}
|
||||||
|
${typedField('workflow-agent-output-key', '输出变量名', cfg.output_key, 'agent_result')}
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
case 'condition':
|
||||||
|
wrap.innerHTML = `
|
||||||
|
${typedField('workflow-condition-expression', '条件表达式', cfg.expression, '{{previous.output}} != ""')}
|
||||||
|
<p class="workflow-config-hint">节点会计算 matched(true/false),由出边决定分支:第一条线为「是」,第二条为「否」;也可在连线上写 <code>{{previous.matched}} == "true"</code>。</p>
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
case 'hitl':
|
||||||
|
wrap.innerHTML = `
|
||||||
|
${typedTextarea('workflow-hitl-prompt', '审批提示', cfg.prompt, '请审批是否继续')}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="workflow-hitl-reviewer">审批方</label>
|
||||||
|
<select id="workflow-hitl-reviewer" onchange="updateWorkflowTypedConfig()">
|
||||||
|
<option value="human" ${cfg.reviewer === 'human' ? 'selected' : ''}>human</option>
|
||||||
|
<option value="audit_agent" ${cfg.reviewer === 'audit_agent' ? 'selected' : ''}>audit_agent</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
case 'output':
|
||||||
|
wrap.innerHTML = `
|
||||||
|
${typedField('workflow-output-key', '输出变量名', cfg.output_key, 'result')}
|
||||||
|
${typedField('workflow-output-source', '变量来源', cfg.source, '{{previous.output}}')}
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
case 'end':
|
||||||
|
wrap.innerHTML = typedTextarea('workflow-end-template', '结束摘要模板', cfg.result_template, '{{outputs.result}}');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
wrap.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCustomFields(config) {
|
||||||
|
const wrap = document.getElementById('workflow-custom-fields');
|
||||||
|
if (!wrap) return;
|
||||||
|
const entries = Object.entries(config || {});
|
||||||
|
if (!entries.length) {
|
||||||
|
wrap.innerHTML = '<div class="workflow-property-empty workflow-property-empty--compact">暂无自定义字段</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wrap.innerHTML = entries.map(([key, value], index) => `
|
||||||
|
<div class="workflow-custom-field" data-index="${index}">
|
||||||
|
<input type="text" value="${esc(key)}" data-field-key oninput="updateWorkflowCustomFields()">
|
||||||
|
<input type="text" value="${esc(String(value == null ? '' : value))}" data-field-value oninput="updateWorkflowCustomFields()">
|
||||||
|
<button type="button" onclick="removeWorkflowCustomField(${index})">×</button>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCustomFields() {
|
||||||
|
const out = {};
|
||||||
|
document.querySelectorAll('#workflow-custom-fields .workflow-custom-field').forEach(row => {
|
||||||
|
const key = row.querySelector('[data-field-key]').value.trim();
|
||||||
|
const value = row.querySelector('[data-field-value]').value;
|
||||||
|
if (key) out[key] = value;
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readTypedConfig(ele) {
|
||||||
|
if (!ele) return {};
|
||||||
|
if (!ele.isNode()) {
|
||||||
|
return { condition: (document.getElementById('workflow-edge-condition') || {}).value || '' };
|
||||||
|
}
|
||||||
|
const type = ele.data('type') || 'tool';
|
||||||
|
switch (type) {
|
||||||
|
case 'start':
|
||||||
|
return { input_keys: (document.getElementById('workflow-start-input-keys') || {}).value || '' };
|
||||||
|
case 'tool':
|
||||||
|
return {
|
||||||
|
tool_name: (document.getElementById('workflow-tool-name') || {}).value || '',
|
||||||
|
arguments: (document.getElementById('workflow-tool-arguments') || {}).value || '{}',
|
||||||
|
timeout_seconds: (document.getElementById('workflow-tool-timeout') || {}).value || ''
|
||||||
|
};
|
||||||
|
case 'agent':
|
||||||
|
return {
|
||||||
|
agent_mode: (document.getElementById('workflow-agent-mode') || {}).value || 'eino_single',
|
||||||
|
input_source: (document.getElementById('workflow-agent-input-source') || {}).value || '{{previous.output}}',
|
||||||
|
instruction: (document.getElementById('workflow-agent-instruction') || {}).value || '',
|
||||||
|
output_key: (document.getElementById('workflow-agent-output-key') || {}).value || 'agent_result'
|
||||||
|
};
|
||||||
|
case 'condition':
|
||||||
|
return { expression: (document.getElementById('workflow-condition-expression') || {}).value || '' };
|
||||||
|
case 'hitl':
|
||||||
|
return {
|
||||||
|
prompt: (document.getElementById('workflow-hitl-prompt') || {}).value || '',
|
||||||
|
reviewer: (document.getElementById('workflow-hitl-reviewer') || {}).value || 'human'
|
||||||
|
};
|
||||||
|
case 'output':
|
||||||
|
return {
|
||||||
|
output_key: (document.getElementById('workflow-output-key') || {}).value || 'result',
|
||||||
|
source: (document.getElementById('workflow-output-source') || {}).value || ''
|
||||||
|
};
|
||||||
|
case 'end':
|
||||||
|
return { result_template: (document.getElementById('workflow-end-template') || {}).value || '' };
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeVisibleConfig() {
|
||||||
|
if (!selectedElement) return;
|
||||||
|
selectedElement.data('config', Object.assign({}, readCustomFields(), readTypedConfig(selectedElement)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConnectTap(node) {
|
||||||
|
if (!connectSourceId) {
|
||||||
|
connectSourceId = node.id();
|
||||||
|
node.addClass('connect-source');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (connectSourceId === node.id()) {
|
||||||
|
clearConnectSource();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const duplicate = cy.edges().some(edge => edge.source().id() === connectSourceId && edge.target().id() === node.id());
|
||||||
|
if (duplicate) {
|
||||||
|
if (typeof showNotification === 'function') {
|
||||||
|
showNotification('这两个节点之间已经有连线', 'warning');
|
||||||
|
}
|
||||||
|
clearConnectSource();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sourceNode = cy.getElementById(connectSourceId);
|
||||||
|
const sourceType = sourceNode.data('type') || '';
|
||||||
|
let edgeLabel = '';
|
||||||
|
let edgeConfig = {};
|
||||||
|
if (sourceType === 'condition') {
|
||||||
|
const siblingCount = cy.edges().filter(edge => edge.source().id() === connectSourceId).length;
|
||||||
|
if (siblingCount === 0) {
|
||||||
|
edgeLabel = '是';
|
||||||
|
edgeConfig = { condition: '{{previous.matched}} == "true"', branch: 'true' };
|
||||||
|
} else if (siblingCount === 1) {
|
||||||
|
edgeLabel = '否';
|
||||||
|
edgeConfig = { condition: '{{previous.matched}} == "false"', branch: 'false' };
|
||||||
|
} else {
|
||||||
|
edgeConfig = { condition: '' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cy.add({
|
||||||
|
group: 'edges',
|
||||||
|
data: {
|
||||||
|
id: nextEdgeId(),
|
||||||
|
source: connectSourceId,
|
||||||
|
target: node.id(),
|
||||||
|
label: edgeLabel,
|
||||||
|
config: edgeConfig
|
||||||
|
}
|
||||||
|
});
|
||||||
|
clearConnectSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearConnectSource() {
|
||||||
|
if (cy) cy.nodes().removeClass('connect-source');
|
||||||
|
connectSourceId = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNode(type, position) {
|
||||||
|
initCy();
|
||||||
|
if (!cy) return;
|
||||||
|
const node = cy.add({
|
||||||
|
group: 'nodes',
|
||||||
|
data: {
|
||||||
|
id: nextNodeId(type),
|
||||||
|
type,
|
||||||
|
label: NODE_LABELS[type] || '节点',
|
||||||
|
config: defaultConfigForType(type)
|
||||||
|
},
|
||||||
|
position: position || { x: 180 + cy.nodes().length * 28, y: 160 + cy.nodes().length * 28 }
|
||||||
|
});
|
||||||
|
selectWorkflowElement(node);
|
||||||
|
updateEmptyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.refreshWorkflows = async function () {
|
||||||
|
initCy();
|
||||||
|
const list = document.getElementById('workflow-list');
|
||||||
|
if (list) list.innerHTML = '<div class="loading-spinner">加载中...</div>';
|
||||||
|
try {
|
||||||
|
await loadWorkflows(true);
|
||||||
|
renderWorkflowList();
|
||||||
|
if (!currentWorkflowId && workflows.length) {
|
||||||
|
fillWorkflowForm(workflows[0]);
|
||||||
|
} else if (!workflows.length) {
|
||||||
|
newWorkflowDraft();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (list) list.innerHTML = `<div class="empty-state">${esc(error.message)}</div>`;
|
||||||
|
if (typeof showNotification === 'function') showNotification(error.message, 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.newWorkflowDraft = function () {
|
||||||
|
fillWorkflowForm({
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
enabled: true,
|
||||||
|
graph_json: defaultGraph()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.selectWorkflow = function (id) {
|
||||||
|
const wf = workflows.find(item => item.id === id);
|
||||||
|
if (wf) fillWorkflowForm(wf);
|
||||||
|
};
|
||||||
|
|
||||||
|
function validateWorkflowGraph(graph) {
|
||||||
|
const errors = [];
|
||||||
|
const nodes = graph.nodes || [];
|
||||||
|
const edges = graph.edges || [];
|
||||||
|
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');
|
||||||
|
if (!starts.length) errors.push('至少需要一个开始节点');
|
||||||
|
if (!outputs.length) errors.push('至少需要一个输出节点');
|
||||||
|
edges.forEach(edge => {
|
||||||
|
if (edge.source === edge.target) errors.push(`连线 ${edge.id} 不能指向自身`);
|
||||||
|
if (!ids.has(edge.source)) errors.push(`连线 ${edge.id} 的源节点不存在`);
|
||||||
|
if (!ids.has(edge.target)) errors.push(`连线 ${edge.id} 的目标节点不存在`);
|
||||||
|
});
|
||||||
|
starts.forEach(node => {
|
||||||
|
if (edges.some(edge => edge.target === node.id)) errors.push(`开始节点 ${node.label || node.id} 不应有入边`);
|
||||||
|
});
|
||||||
|
outputs.forEach(node => {
|
||||||
|
if (edges.some(edge => edge.source === node.id)) errors.push(`输出节点 ${node.label || node.id} 不应有出边`);
|
||||||
|
});
|
||||||
|
nodes.filter(node => node.type === 'tool').forEach(node => {
|
||||||
|
if (!String((node.config || {}).tool_name || '').trim()) {
|
||||||
|
errors.push(`工具节点 ${node.label || node.id} 需要选择 MCP 工具`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
nodes.filter(node => node.type === 'condition').forEach(node => {
|
||||||
|
if (!String((node.config || {}).expression || '').trim()) {
|
||||||
|
errors.push(`条件节点 ${node.label || node.id} 需要条件表达式`);
|
||||||
|
}
|
||||||
|
const outEdges = edges.filter(edge => edge.source === node.id);
|
||||||
|
if (outEdges.length === 0) {
|
||||||
|
errors.push(`条件节点 ${node.label || node.id} 至少需要一条出边(是/否分支)`);
|
||||||
|
} else if (outEdges.length > 2) {
|
||||||
|
errors.push(`条件节点 ${node.label || node.id} 建议最多两条出边(是/否);第三条及以后需配置连线条件`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
nodes.filter(node => node.type === 'output').forEach(node => {
|
||||||
|
if (!String((node.config || {}).output_key || '').trim()) {
|
||||||
|
errors.push(`输出节点 ${node.label || node.id} 需要输出变量名`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.saveWorkflowDraft = async function () {
|
||||||
|
initCy();
|
||||||
|
const id = document.getElementById('workflow-id').value.trim();
|
||||||
|
const name = document.getElementById('workflow-name').value.trim();
|
||||||
|
const description = document.getElementById('workflow-description').value.trim();
|
||||||
|
const enabled = document.getElementById('workflow-enabled').checked;
|
||||||
|
if (!id || !name) {
|
||||||
|
showNotification('工作流 ID 和名称不能为空', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const graph = elementsToGraph();
|
||||||
|
const errors = validateWorkflowGraph(graph);
|
||||||
|
if (errors.length) {
|
||||||
|
showNotification(errors.slice(0, 4).join(';'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const method = currentWorkflowId ? 'PUT' : 'POST';
|
||||||
|
const url = currentWorkflowId ? `/api/workflows/${encodeURIComponent(currentWorkflowId)}` : '/api/workflows';
|
||||||
|
const response = await apiFetch(url, {
|
||||||
|
method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ id, name, description, enabled, graph })
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const err = await response.json().catch(() => ({}));
|
||||||
|
showNotification(err.error || '保存工作流失败', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
currentWorkflowId = data.workflow && data.workflow.id ? data.workflow.id : id;
|
||||||
|
showNotification('工作流已保存', 'success');
|
||||||
|
await refreshWorkflows();
|
||||||
|
if (typeof loadWorkflowOptionsForRoleModal === 'function') {
|
||||||
|
await loadWorkflowOptionsForRoleModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.deleteCurrentWorkflow = async function () {
|
||||||
|
const id = currentWorkflowId || document.getElementById('workflow-id').value.trim();
|
||||||
|
if (!id) {
|
||||||
|
showNotification('请选择要删除的工作流', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!confirm(`确定删除工作流 ${id}?`)) return;
|
||||||
|
const response = await apiFetch(`/api/workflows/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
||||||
|
if (!response.ok) {
|
||||||
|
const err = await response.json().catch(() => ({}));
|
||||||
|
showNotification(err.error || '删除工作流失败', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentWorkflowId = '';
|
||||||
|
showNotification('工作流已删除', 'success');
|
||||||
|
newWorkflowDraft();
|
||||||
|
await refreshWorkflows();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.workflowPaletteDragStart = function (event) {
|
||||||
|
const type = event.currentTarget.dataset.nodeType || 'tool';
|
||||||
|
event.dataTransfer.setData('application/x-workflow-node', type);
|
||||||
|
event.dataTransfer.setData('text/plain', type);
|
||||||
|
event.dataTransfer.effectAllowed = 'copy';
|
||||||
|
};
|
||||||
|
|
||||||
|
window.workflowCanvasDragOver = function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.dataTransfer.dropEffect = 'copy';
|
||||||
|
};
|
||||||
|
|
||||||
|
window.workflowCanvasDrop = function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const type = event.dataTransfer.getData('application/x-workflow-node') || event.dataTransfer.getData('text/plain') || 'tool';
|
||||||
|
const rect = document.getElementById('workflow-canvas').getBoundingClientRect();
|
||||||
|
const pan = cy.pan();
|
||||||
|
const zoom = cy.zoom();
|
||||||
|
addNode(type, {
|
||||||
|
x: (event.clientX - rect.left - pan.x) / zoom,
|
||||||
|
y: (event.clientY - rect.top - pan.y) / zoom
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addWorkflowNodeFromPalette = function (type) {
|
||||||
|
addNode(type || 'tool');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.toggleWorkflowConnectMode = function () {
|
||||||
|
connectMode = !connectMode;
|
||||||
|
clearConnectSource();
|
||||||
|
const btn = document.getElementById('workflow-connect-btn');
|
||||||
|
if (btn) {
|
||||||
|
btn.classList.toggle('active', connectMode);
|
||||||
|
btn.textContent = connectMode ? '连线中' : '连线';
|
||||||
|
}
|
||||||
|
if (typeof showNotification === 'function') {
|
||||||
|
showNotification(connectMode ? '连线模式:依次点击源节点和目标节点' : '已退出连线模式', 'info');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.deleteWorkflowSelection = function () {
|
||||||
|
if (!cy) return;
|
||||||
|
const selected = selectedElement && selectedElement.length ? selectedElement : cy.$(':selected');
|
||||||
|
if (!selected.length) return;
|
||||||
|
selected.remove();
|
||||||
|
selectWorkflowElement(null);
|
||||||
|
updateEmptyState();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.layoutWorkflowGraph = function (animate) {
|
||||||
|
if (!cy || !cy.nodes().length) return;
|
||||||
|
cy.layout({
|
||||||
|
name: 'breadthfirst',
|
||||||
|
directed: true,
|
||||||
|
padding: 40,
|
||||||
|
spacingFactor: 1.25,
|
||||||
|
animate: animate !== false,
|
||||||
|
animationDuration: 250
|
||||||
|
}).run();
|
||||||
|
cy.fit(undefined, 40);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.updateWorkflowSelectedProperty = function () {
|
||||||
|
if (!selectedElement) return;
|
||||||
|
const label = document.getElementById('workflow-prop-label').value.trim();
|
||||||
|
selectedElement.data('label', label);
|
||||||
|
if (selectedElement.isNode()) {
|
||||||
|
const type = document.getElementById('workflow-prop-type').value || 'tool';
|
||||||
|
const prevType = selectedElement.data('type') || 'tool';
|
||||||
|
selectedElement.data('type', type);
|
||||||
|
if (type !== prevType) {
|
||||||
|
selectedElement.data('config', defaultConfigForType(type));
|
||||||
|
selectedElement.data('label', label || NODE_LABELS[type] || '节点');
|
||||||
|
document.getElementById('workflow-prop-label').value = selectedElement.data('label') || '';
|
||||||
|
renderTypedConfig(selectedElement);
|
||||||
|
renderCustomFields({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addWorkflowCustomField = function () {
|
||||||
|
if (!selectedElement) return;
|
||||||
|
const cfg = Object.assign({}, selectedElement.data('config') || {});
|
||||||
|
let i = 1;
|
||||||
|
while (Object.prototype.hasOwnProperty.call(cfg, `field_${i}`)) i += 1;
|
||||||
|
cfg[`field_${i}`] = '';
|
||||||
|
selectedElement.data('config', cfg);
|
||||||
|
renderCustomFields(cfg);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.updateWorkflowCustomFields = function () {
|
||||||
|
if (!selectedElement) return;
|
||||||
|
mergeVisibleConfig();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.updateWorkflowTypedConfig = function () {
|
||||||
|
if (!selectedElement) return;
|
||||||
|
mergeVisibleConfig();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.removeWorkflowCustomField = function (index) {
|
||||||
|
if (!selectedElement) return;
|
||||||
|
const entries = Object.entries(stripTypedConfig(selectedElement));
|
||||||
|
entries.splice(index, 1);
|
||||||
|
const next = {};
|
||||||
|
entries.forEach(([key, value]) => {
|
||||||
|
if (key) next[key] = value;
|
||||||
|
});
|
||||||
|
selectedElement.data('config', Object.assign({}, next, readTypedConfig(selectedElement)));
|
||||||
|
renderCustomFields(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.loadWorkflowOptionsForRoleModal = async function (selectedId) {
|
||||||
|
try {
|
||||||
|
await loadWorkflows(true);
|
||||||
|
} catch (_) {
|
||||||
|
workflows = [];
|
||||||
|
}
|
||||||
|
const select = document.getElementById('role-workflow-id');
|
||||||
|
if (!select) return;
|
||||||
|
const current = selectedId !== undefined ? selectedId : select.value;
|
||||||
|
select.innerHTML = '<option value="">不绑定流程</option>' + workflows.map(wf => (
|
||||||
|
`<option value="${esc(wf.id)}">${esc(wf.name || wf.id)}${wf.enabled ? '' : '(已禁用)'}</option>`
|
||||||
|
)).join('');
|
||||||
|
select.value = current || '';
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -201,6 +201,16 @@
|
|||||||
<span data-i18n="nav.tasks">任务管理</span>
|
<span data-i18n="nav.tasks">任务管理</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="nav-item" data-page="workflows">
|
||||||
|
<div class="nav-item-content" data-title="图编排" onclick="switchPage('workflows')">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="3" y="3" width="8" height="8" rx="2"/>
|
||||||
|
<path d="M7 11v4a2 2 0 0 0 2 2h4"/>
|
||||||
|
<rect x="13" y="13" width="8" height="8" rx="2"/>
|
||||||
|
</svg>
|
||||||
|
<span>图编排</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="nav-item" data-page="projects">
|
<div class="nav-item" data-page="projects">
|
||||||
<div class="nav-item-content" data-title="项目管理" onclick="switchPage('projects')" data-i18n="nav.projects" data-i18n-attr="data-title" data-i18n-skip-text="true">
|
<div class="nav-item-content" data-title="项目管理" onclick="switchPage('projects')" data-i18n="nav.projects" data-i18n-attr="data-title" data-i18n-skip-text="true">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
@@ -2537,6 +2547,95 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 图编排页面 -->
|
||||||
|
<div id="page-workflows" class="page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>图编排</h2>
|
||||||
|
<div class="page-header-actions">
|
||||||
|
<button class="btn-secondary" onclick="refreshWorkflows()">刷新</button>
|
||||||
|
<button class="btn-primary" onclick="newWorkflowDraft()">新建图</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="page-content workflow-page-content">
|
||||||
|
<aside class="workflow-sidebar">
|
||||||
|
<div class="workflow-panel">
|
||||||
|
<div class="workflow-panel-header">
|
||||||
|
<h3>流程库</h3>
|
||||||
|
</div>
|
||||||
|
<div id="workflow-list" class="workflow-list">
|
||||||
|
<div class="loading-spinner">加载中...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="workflow-panel">
|
||||||
|
<div class="workflow-panel-header">
|
||||||
|
<h3>节点库</h3>
|
||||||
|
</div>
|
||||||
|
<div class="workflow-node-palette">
|
||||||
|
<button type="button" draggable="true" data-node-type="start" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('start')">开始</button>
|
||||||
|
<button type="button" draggable="true" data-node-type="tool" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('tool')">工具</button>
|
||||||
|
<button type="button" draggable="true" data-node-type="agent" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('agent')">Agent</button>
|
||||||
|
<button type="button" draggable="true" data-node-type="condition" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('condition')">条件</button>
|
||||||
|
<button type="button" draggable="true" data-node-type="hitl" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('hitl')">审批</button>
|
||||||
|
<button type="button" draggable="true" data-node-type="output" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('output')">输出</button>
|
||||||
|
<button type="button" draggable="true" data-node-type="end" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('end')">结束</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<main class="workflow-main">
|
||||||
|
<section class="workflow-meta-bar">
|
||||||
|
<div class="workflow-meta-fields">
|
||||||
|
<label>ID <input type="text" id="workflow-id" placeholder="web-scan-basic" autocomplete="off"></label>
|
||||||
|
<label>名称 <input type="text" id="workflow-name" placeholder="基础 Web 扫描" autocomplete="off"></label>
|
||||||
|
<label>描述 <input type="text" id="workflow-description" placeholder="可选" autocomplete="off"></label>
|
||||||
|
<label class="workflow-enabled-toggle"><input type="checkbox" id="workflow-enabled" checked> 启用</label>
|
||||||
|
</div>
|
||||||
|
<div class="workflow-toolbar">
|
||||||
|
<button class="btn-secondary btn-small" type="button" onclick="toggleWorkflowConnectMode()" id="workflow-connect-btn">连线</button>
|
||||||
|
<button class="btn-secondary btn-small" type="button" onclick="deleteWorkflowSelection()">删除选中</button>
|
||||||
|
<button class="btn-secondary btn-small" type="button" onclick="layoutWorkflowGraph()">自动布局</button>
|
||||||
|
<button class="btn-secondary btn-small" onclick="deleteCurrentWorkflow()">删除</button>
|
||||||
|
<button class="btn-primary btn-small" onclick="saveWorkflowDraft()">保存</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="workflow-canvas-wrap" ondragover="workflowCanvasDragOver(event)" ondrop="workflowCanvasDrop(event)">
|
||||||
|
<div id="workflow-canvas"></div>
|
||||||
|
<div id="workflow-canvas-empty" class="workflow-canvas-empty">从左侧拖拽节点到画布,或点击节点按钮快速添加</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<aside class="workflow-properties">
|
||||||
|
<div class="workflow-panel-header">
|
||||||
|
<h3 id="workflow-property-title">属性</h3>
|
||||||
|
<button type="button" id="workflow-property-delete-btn" class="btn-secondary btn-small" onclick="deleteWorkflowSelection()" hidden>删除</button>
|
||||||
|
</div>
|
||||||
|
<div id="workflow-property-empty" class="workflow-property-empty">选择一个节点或连线后编辑属性</div>
|
||||||
|
<div id="workflow-property-form" class="workflow-property-form" hidden>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="workflow-prop-label">名称</label>
|
||||||
|
<input type="text" id="workflow-prop-label" class="form-input" oninput="updateWorkflowSelectedProperty()">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" id="workflow-prop-type-wrap">
|
||||||
|
<label for="workflow-prop-type">类型</label>
|
||||||
|
<select id="workflow-prop-type" onchange="updateWorkflowSelectedProperty()">
|
||||||
|
<option value="start">开始</option>
|
||||||
|
<option value="tool">工具</option>
|
||||||
|
<option value="agent">Agent</option>
|
||||||
|
<option value="condition">条件</option>
|
||||||
|
<option value="hitl">审批</option>
|
||||||
|
<option value="output">输出</option>
|
||||||
|
<option value="end">结束</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="workflow-typed-config" class="workflow-typed-config"></div>
|
||||||
|
<div class="workflow-custom-fields-head">
|
||||||
|
<span>自定义字段</span>
|
||||||
|
<button type="button" class="btn-secondary btn-small" onclick="addWorkflowCustomField()">添加字段</button>
|
||||||
|
</div>
|
||||||
|
<div id="workflow-custom-fields" class="workflow-custom-fields"></div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 角色管理页面 -->
|
<!-- 角色管理页面 -->
|
||||||
<div id="page-roles-management" class="page">
|
<div id="page-roles-management" class="page">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
@@ -4561,6 +4660,20 @@
|
|||||||
<textarea id="role-user-prompt" rows="10" data-i18n="roleModal.userPromptPlaceholder" data-i18n-attr="placeholder" placeholder="输入用户提示词,会在用户消息前追加此提示词..."></textarea>
|
<textarea id="role-user-prompt" rows="10" data-i18n="roleModal.userPromptPlaceholder" data-i18n-attr="placeholder" placeholder="输入用户提示词,会在用户消息前追加此提示词..."></textarea>
|
||||||
<small class="form-hint" data-i18n="roleModal.userPromptHint">此提示词会追加到用户消息前,用于指导AI的行为。注意:这不会修改系统提示词。</small>
|
<small class="form-hint" data-i18n="roleModal.userPromptHint">此提示词会追加到用户消息前,用于指导AI的行为。注意:这不会修改系统提示词。</small>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="role-workflow-id">绑定图编排流程</label>
|
||||||
|
<select id="role-workflow-id">
|
||||||
|
<option value="">不绑定流程</option>
|
||||||
|
</select>
|
||||||
|
<small class="form-hint">选中流程后,对话页使用该角色会自动触发绑定图;流程字段由图定义 JSON 自由配置。</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="role-workflow-policy">流程触发策略</label>
|
||||||
|
<select id="role-workflow-policy">
|
||||||
|
<option value="auto">自动触发</option>
|
||||||
|
<option value="off">关闭</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="form-group" id="role-tools-section">
|
<div class="form-group" id="role-tools-section">
|
||||||
<label data-i18n="roleModal.relatedTools">关联的工具(可选)</label>
|
<label data-i18n="roleModal.relatedTools">关联的工具(可选)</label>
|
||||||
<div id="role-tools-default-hint" class="role-tools-default-hint" style="display: none;">
|
<div id="role-tools-default-hint" class="role-tools-default-hint" style="display: none;">
|
||||||
@@ -4779,6 +4892,7 @@
|
|||||||
<script src="/static/js/webshell.js"></script>
|
<script src="/static/js/webshell.js"></script>
|
||||||
<script src="/static/js/chat-files.js"></script>
|
<script src="/static/js/chat-files.js"></script>
|
||||||
<script src="/static/js/tasks.js"></script>
|
<script src="/static/js/tasks.js"></script>
|
||||||
|
<script src="/static/js/workflows.js"></script>
|
||||||
<script src="/static/js/roles.js"></script>
|
<script src="/static/js/roles.js"></script>
|
||||||
<script src="/static/js/c2.js"></script>
|
<script src="/static/js/c2.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in new issue
Block a user