mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-01 08:37:41 +02:00
Add files via upload
This commit is contained in:
@@ -40124,6 +40124,247 @@ html[data-theme="dark"] .workflow-status-toggle.is-disabled {
|
||||
padding: 12px 18px;
|
||||
}
|
||||
|
||||
.workflow-ai-modal-content {
|
||||
width: min(760px, calc(100vw - 32px));
|
||||
max-height: calc(100vh - 64px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.workflow-ai-modal-header {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.workflow-ai-modal-header h2 {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.workflow-ai-subtitle {
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.workflow-ai-modal-body {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
max-height: min(66vh, 640px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.workflow-ai-prompt-field textarea {
|
||||
resize: vertical;
|
||||
min-height: 116px;
|
||||
}
|
||||
|
||||
.workflow-ai-examples,
|
||||
.workflow-ai-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.workflow-ai-options label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 7px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.workflow-ai-result {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.workflow-ai-result:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.workflow-ai-result-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.workflow-ai-progress {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.workflow-ai-progress[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.workflow-ai-progress span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
padding: 9px 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.workflow-ai-progress b {
|
||||
display: inline-grid;
|
||||
flex: 0 0 22px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.workflow-ai-progress small {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.workflow-ai-progress .is-active {
|
||||
border-color: color-mix(in srgb, var(--accent-color) 45%, var(--border-color));
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.workflow-ai-progress .is-active b {
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.workflow-ai-progress .is-complete b {
|
||||
background: #16a34a;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.workflow-ai-preview {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.workflow-ai-preview-title {
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.workflow-ai-preview-flow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.workflow-ai-preview-flow i {
|
||||
flex: 0 0 auto;
|
||||
color: var(--text-secondary);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.workflow-ai-preview-node {
|
||||
display: grid;
|
||||
flex: 0 0 auto;
|
||||
gap: 2px;
|
||||
width: 128px;
|
||||
min-height: 48px;
|
||||
padding: 7px 9px;
|
||||
border: 1px solid color-mix(in srgb, var(--accent-color) 22%, var(--border-color));
|
||||
border-radius: 8px;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.workflow-ai-preview-node small {
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.workflow-ai-preview-node strong {
|
||||
overflow: hidden;
|
||||
color: var(--text-primary);
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.workflow-ai-preview-node.is-risky {
|
||||
border-color: rgba(249, 115, 22, 0.62);
|
||||
background: rgba(249, 115, 22, 0.08);
|
||||
}
|
||||
|
||||
.workflow-ai-result-grid span,
|
||||
.workflow-ai-result-section {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.workflow-ai-result-grid span {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 10px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.workflow-ai-result-grid strong {
|
||||
color: var(--text-primary);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.workflow-ai-result-section {
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.workflow-ai-result-section p {
|
||||
margin: 4px 0 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.workflow-ai-result-section ul {
|
||||
margin: 8px 0 0;
|
||||
padding-left: 18px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.workflow-ai-result-section.is-warning {
|
||||
border-color: rgba(245, 158, 11, 0.45);
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
}
|
||||
|
||||
.workflow-ai-result-section.is-attention {
|
||||
border-color: rgba(14, 165, 233, 0.42);
|
||||
background: rgba(14, 165, 233, 0.08);
|
||||
}
|
||||
|
||||
.workflow-ai-modal-footer {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.workflow-ai-progress {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.workflow-ai-result-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .workflow-dry-run-modal-icon {
|
||||
color: #bfdbfe;
|
||||
background: rgba(59, 130, 246, 0.14);
|
||||
@@ -40364,6 +40605,27 @@ html[data-theme="dark"] .workflow-toolbar #workflow-connect-btn[aria-pressed="tr
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.workflow-ai-canvas-status {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: 50%;
|
||||
z-index: 2;
|
||||
transform: translateX(-50%);
|
||||
padding: 9px 14px;
|
||||
border: 1px solid color-mix(in srgb, var(--accent-color) 35%, var(--border-color));
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--bg-primary) 92%, var(--accent-color));
|
||||
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.12);
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.workflow-ai-canvas-status[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.workflow-properties {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -3532,6 +3532,81 @@
|
||||
"canvasTools": "Canvas tools",
|
||||
"moreActions": "More",
|
||||
"deleteWorkflow": "Delete workflow",
|
||||
"ai": {
|
||||
"open": "Create from natural language",
|
||||
"title": "Create Workflow from Natural Language",
|
||||
"subtitle": "AI generates an editable draft and will not save or run it automatically.",
|
||||
"promptLabel": "Workflow request",
|
||||
"promptPlaceholder": "Example: continuously monitor a domain's subdomains, certificates, and exposed pages, then generate a report when new assets appear",
|
||||
"examplesLabel": "Example requests",
|
||||
"exampleDomain": "Domain monitor",
|
||||
"exampleReport": "Report approval",
|
||||
"exampleTriage": "Asset triage",
|
||||
"includeObjective": "Generate objective configuration too",
|
||||
"allowSchedule": "Allow scheduled trigger suggestions",
|
||||
"allowHighRisk": "Allow high-risk node drafts",
|
||||
"allowFallback": "Allow deterministic fallback if AI fails",
|
||||
"promptRequired": "Describe the workflow request first.",
|
||||
"generateFailed": "Generation failed",
|
||||
"generatedWithIssues": "The draft still has validation issues. Adjust the request and try again.",
|
||||
"generate": "Generate draft",
|
||||
"generating": "Generating…",
|
||||
"apply": "Render to canvas",
|
||||
"rendering": "Rendering…",
|
||||
"applied": "Workflow draft generated. Review it before saving.",
|
||||
"canvasRendering": "AI is composing the canvas…",
|
||||
"generatorAI": "AI channel",
|
||||
"generatorLLM": "Model generated",
|
||||
"generatorServer": "Server draft generator",
|
||||
"generatorFallback": "Deterministic fallback",
|
||||
"generatorUnknown": "Unknown generator",
|
||||
"llmTitle": "Generated with the platform default AI channel and structurally validated",
|
||||
"serverTitle": "Generated on the server and structurally audited",
|
||||
"fallbackTitle": "Deterministic fallback was used",
|
||||
"serverFallbackNotice": "Model generation is unavailable: {{reason}}",
|
||||
"preview": "Flow preview",
|
||||
"resultNodes": "Nodes",
|
||||
"resultEdges": "Edges",
|
||||
"resultRepairs": "Repair rounds",
|
||||
"resultCapabilities": "Node capabilities",
|
||||
"resultTools": "Tool capabilities",
|
||||
"resultRisk": "Risk",
|
||||
"resultSaveState": "Savable",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"missingFields": "Missing configuration",
|
||||
"riskWarnings": "Risk warnings",
|
||||
"assumptions": "Assumptions",
|
||||
"capabilityTrace": "Capability toolchain",
|
||||
"nodeGenerated": "AI generated",
|
||||
"nodeNeedsInput": "Needs input",
|
||||
"riskLow": "Low",
|
||||
"riskMedium": "Medium",
|
||||
"riskHigh": "High",
|
||||
"steps": {
|
||||
"understand": "Understand request",
|
||||
"match": "Match tools",
|
||||
"draft": "Generate nodes",
|
||||
"audit": "Safety audit"
|
||||
},
|
||||
"examples": {
|
||||
"domainMonitor": "Continuously monitor a domain's subdomains, certificates, and exposed pages, then generate a report when new assets appear",
|
||||
"reportReview": "Collect threat intelligence every day, generate a report, and send it to the security owner for approval",
|
||||
"assetTriage": "Extract high-risk assets from scan results, deduplicate them, and output remediation suggestions"
|
||||
}
|
||||
},
|
||||
"audit": {
|
||||
"title": "Draft Audit",
|
||||
"ready": "No blocking items found in this draft",
|
||||
"review": "Review before saving or running",
|
||||
"aiNodes": "AI nodes",
|
||||
"needsInput": "Needs input",
|
||||
"highRisk": "High risk",
|
||||
"validation": "Structure issues",
|
||||
"nodeIssues": "Node notes",
|
||||
"validationIssues": "Structure checks",
|
||||
"saveConfirm": "This draft contains missing configuration or high-risk nodes. Save as draft anyway?"
|
||||
},
|
||||
"package": {
|
||||
"importLocal": "Import local package",
|
||||
"export": "Export",
|
||||
@@ -3641,7 +3716,7 @@
|
||||
"deleteSelected": "Delete selected",
|
||||
"autoLayout": "Auto layout",
|
||||
"dryRun": "Dry run",
|
||||
"canvasEmpty": "Drag nodes from the left onto the canvas, or click node buttons to add quickly",
|
||||
"canvasEmpty": "Drag nodes from the left onto the canvas, or generate a workflow from natural language",
|
||||
"properties": "Properties",
|
||||
"nodeProperties": "Node properties",
|
||||
"edgeProperties": "Edge properties",
|
||||
|
||||
@@ -3520,6 +3520,81 @@
|
||||
"canvasTools": "画布工具",
|
||||
"moreActions": "更多",
|
||||
"deleteWorkflow": "删除工作流",
|
||||
"ai": {
|
||||
"open": "用自然语言创建",
|
||||
"title": "用自然语言创建工作流",
|
||||
"subtitle": "AI 会生成可编辑草稿,不会自动保存或运行。",
|
||||
"promptLabel": "工作流需求",
|
||||
"promptPlaceholder": "例如:持续监控一个域名的子域名、证书和暴露页面,发现新增资产后生成报告",
|
||||
"examplesLabel": "示例需求",
|
||||
"exampleDomain": "域名监控",
|
||||
"exampleReport": "报告审批",
|
||||
"exampleTriage": "资产研判",
|
||||
"includeObjective": "同时生成 objective 配置",
|
||||
"allowSchedule": "允许生成定时触发建议",
|
||||
"allowHighRisk": "允许包含高风险节点草稿",
|
||||
"allowFallback": "AI 失败时允许确定性兜底",
|
||||
"promptRequired": "请先描述工作流需求。",
|
||||
"generateFailed": "生成失败",
|
||||
"generatedWithIssues": "草稿仍有校验问题,请调整需求后重试。",
|
||||
"generate": "生成草稿",
|
||||
"generating": "正在生成…",
|
||||
"apply": "渲染到画布",
|
||||
"rendering": "正在渲染…",
|
||||
"applied": "已生成工作流草稿,请检查后保存。",
|
||||
"canvasRendering": "AI 正在编排画布…",
|
||||
"generatorAI": "AI 通道",
|
||||
"generatorLLM": "大模型生成",
|
||||
"generatorServer": "服务端草稿生成器",
|
||||
"generatorFallback": "确定性兜底",
|
||||
"generatorUnknown": "未知生成器",
|
||||
"llmTitle": "已调用平台默认 AI 通道生成,并完成结构校验",
|
||||
"serverTitle": "已通过服务端生成并完成结构审计",
|
||||
"fallbackTitle": "已使用确定性兜底",
|
||||
"serverFallbackNotice": "大模型生成不可用:{{reason}}",
|
||||
"preview": "流程预览",
|
||||
"resultNodes": "节点",
|
||||
"resultEdges": "连线",
|
||||
"resultRepairs": "修复轮次",
|
||||
"resultCapabilities": "节点能力",
|
||||
"resultTools": "工具能力",
|
||||
"resultRisk": "风险",
|
||||
"resultSaveState": "可保存",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"missingFields": "缺失配置",
|
||||
"riskWarnings": "风险提示",
|
||||
"assumptions": "生成假设",
|
||||
"capabilityTrace": "能力工具链",
|
||||
"nodeGenerated": "AI 生成",
|
||||
"nodeNeedsInput": "需补配置",
|
||||
"riskLow": "低",
|
||||
"riskMedium": "中",
|
||||
"riskHigh": "高",
|
||||
"steps": {
|
||||
"understand": "理解需求",
|
||||
"match": "匹配工具",
|
||||
"draft": "生成节点",
|
||||
"audit": "安全审计"
|
||||
},
|
||||
"examples": {
|
||||
"domainMonitor": "持续监控一个域名的子域名、证书和暴露页面,发现新增资产后生成报告",
|
||||
"reportReview": "每天收集威胁情报,生成报告后交给安全负责人审批",
|
||||
"assetTriage": "从扫描结果中提取高风险资产,去重后输出处置建议"
|
||||
}
|
||||
},
|
||||
"audit": {
|
||||
"title": "草稿审计",
|
||||
"ready": "当前草稿未发现阻断项",
|
||||
"review": "保存或运行前建议检查",
|
||||
"aiNodes": "AI 节点",
|
||||
"needsInput": "需补配置",
|
||||
"highRisk": "高风险",
|
||||
"validation": "结构问题",
|
||||
"nodeIssues": "节点提示",
|
||||
"validationIssues": "结构校验",
|
||||
"saveConfirm": "当前草稿包含需补配置或高风险节点,确认仅保存为草稿?"
|
||||
},
|
||||
"package": {
|
||||
"importLocal": "导入本地包",
|
||||
"export": "导出",
|
||||
@@ -3629,7 +3704,7 @@
|
||||
"deleteSelected": "删除选中",
|
||||
"autoLayout": "自动布局",
|
||||
"dryRun": "试运行",
|
||||
"canvasEmpty": "从左侧拖拽节点到画布,或点击节点按钮快速添加",
|
||||
"canvasEmpty": "从左侧拖拽节点到画布,或用自然语言生成工作流",
|
||||
"properties": "属性",
|
||||
"nodeProperties": "节点属性",
|
||||
"edgeProperties": "连线属性",
|
||||
|
||||
+67
-7
@@ -3584,13 +3584,14 @@ function setPendingMcpExecutionIds(messageElement, executionIds) {
|
||||
|
||||
function normalizeToolExecutionSummaryForButton(raw) {
|
||||
const data = raw && typeof raw === 'object' ? raw : {};
|
||||
return {
|
||||
toolName: data.toolName || data.name || '',
|
||||
status: data.status || '',
|
||||
executionId: data.executionId || '',
|
||||
toolCallId: data.toolCallId || '',
|
||||
processDetailId: data.processDetailId || ''
|
||||
};
|
||||
return {
|
||||
toolName: data.toolName || data.name || '',
|
||||
status: data.status || '',
|
||||
executionId: data.executionId || '',
|
||||
toolCallId: data.toolCallId || '',
|
||||
processDetailId: data.processDetailId || '',
|
||||
resultDetailId: data.resultDetailId || ''
|
||||
};
|
||||
}
|
||||
|
||||
function cacheToolExecutionSummaries(messageElement, summaries) {
|
||||
@@ -3924,6 +3925,15 @@ async function fetchProcessDetailDataForModal(detailId) {
|
||||
return detail && detail.data ? detail.data : null;
|
||||
}
|
||||
|
||||
async function fetchProcessDetailForModal(detailId) {
|
||||
const id = detailId != null ? String(detailId).trim() : '';
|
||||
if (!id || typeof apiFetch !== 'function') return null;
|
||||
const res = await apiFetch('/api/process-details/' + encodeURIComponent(id));
|
||||
const j = await res.json().catch(() => ({}));
|
||||
if (!res.ok) return null;
|
||||
return j && j.processDetail ? j.processDetail : null;
|
||||
}
|
||||
|
||||
function processToolResultTextFromData(resultData) {
|
||||
if (!resultData) return '';
|
||||
const noResultText = typeof window.t === 'function' ? window.t('timeline.noResult') : '无结果';
|
||||
@@ -3950,6 +3960,56 @@ function processToolResultToMCPResult(resultData, rawText) {
|
||||
}
|
||||
|
||||
async function showMCPDetailFromProcessToolItem(messageElement, summary, index) {
|
||||
const item = normalizeToolExecutionSummaryForButton(summary);
|
||||
if (item.processDetailId) {
|
||||
const callDetail = await fetchProcessDetailForModal(item.processDetailId);
|
||||
const callData = callDetail && callDetail.data ? callDetail.data : null;
|
||||
if (callData) {
|
||||
const args = typeof window.parseToolCallArgsFromData === 'function'
|
||||
? window.parseToolCallArgsFromData(callData)
|
||||
: (callData.argumentsObj || {});
|
||||
let resultData = callData._mergedResult || null;
|
||||
let resultDetailId = item.resultDetailId || callData._mergedResultDetailId || '';
|
||||
let rawText = resultData ? processToolResultTextFromData(resultData) : '';
|
||||
const resultPayloadDeferred = resultData && resultData._payloadDeferred === true;
|
||||
const resultOnlyHasPreview = resultData && resultData.result == null && resultData.error == null && resultData.resultPreview != null;
|
||||
if (resultDetailId && (!resultData || resultPayloadDeferred || resultOnlyHasPreview)) {
|
||||
const resultDetail = await fetchProcessDetailForModal(resultDetailId);
|
||||
const fullResult = resultDetail && resultDetail.data ? resultDetail.data : null;
|
||||
if (fullResult) {
|
||||
resultData = fullResult;
|
||||
rawText = processToolResultTextFromData(fullResult);
|
||||
}
|
||||
}
|
||||
const displayState = resultData && typeof window.getToolResultDisplayState === 'function'
|
||||
? window.getToolResultDisplayState(resultData, { rawText: rawText })
|
||||
: null;
|
||||
const backgroundRunning = (displayState && displayState.kind === 'background_running') || String(item.status || '').toLowerCase() === 'background_running';
|
||||
const success = resultData
|
||||
? !(displayState ? displayState.isError : (resultData.isError || resultData.success === false))
|
||||
: String(item.status || '').toLowerCase() !== 'failed';
|
||||
const status = backgroundRunning
|
||||
? 'background_running'
|
||||
: (resultData || item.status
|
||||
? (success ? 'completed' : 'failed')
|
||||
: 'running');
|
||||
const exec = {
|
||||
id: (resultData && resultData.executionId) || item.executionId || callData.executionId || '',
|
||||
toolName: item.toolName || callData.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具'),
|
||||
status: status,
|
||||
startTime: callDetail.createdAt || '',
|
||||
arguments: args || {},
|
||||
result: processToolResultToMCPResult(resultData, rawText),
|
||||
error: resultData && resultData.error ? String(resultData.error) : ''
|
||||
};
|
||||
openAppModal('mcp-detail-modal', { focus: false });
|
||||
deferModalContent(function () {
|
||||
renderMCPDetailModal(exec);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const target = await findToolExecutionTimelineItem(messageElement, summary, index);
|
||||
if (!target) {
|
||||
alert(typeof window.t === 'function'
|
||||
|
||||
@@ -5681,7 +5681,7 @@ function updateMonitorTimelineSection() {
|
||||
}
|
||||
|
||||
|
||||
const MCP_STATS_TOP_N = 3;
|
||||
const MCP_STATS_TOP_N = 6;
|
||||
const MCP_TIMELINE_RANGES = ['24h', '7d', '30d'];
|
||||
|
||||
function getMcpMonitorTimelineRange() {
|
||||
|
||||
+586
-4
@@ -37,6 +37,12 @@
|
||||
requestSignature: '',
|
||||
dragDepth: 0
|
||||
};
|
||||
const workflowAiState = {
|
||||
draft: null,
|
||||
result: null,
|
||||
activeStep: '',
|
||||
animating: false
|
||||
};
|
||||
const WORKFLOW_PACKAGE_INSPECTION_STORAGE_KEY = 'csai.workflow-package.inspection-id';
|
||||
const WORKFLOW_PACKAGE_IMPORT_STORAGE_KEY = 'csai.workflow-package.import-id';
|
||||
|
||||
@@ -67,6 +73,18 @@
|
||||
const NODE_PLACEMENT_PADDING = 20;
|
||||
|
||||
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>';
|
||||
const WORKFLOW_AI_TOOL_HINTS = [
|
||||
{ keywords: ['子域名', 'subdomain', 'subfinder', 'amass'], tools: ['subfinder', 'amass'], label: '子域名发现' },
|
||||
{ keywords: ['端口', 'port', 'nmap', 'rustscan', 'masscan'], tools: ['nmap', 'rustscan', 'masscan'], label: '端口扫描' },
|
||||
{ keywords: ['漏洞', 'vuln', '漏洞扫描', 'nuclei', 'nikto', 'zap'], tools: ['nuclei', 'nikto', 'zap'], label: '漏洞扫描' },
|
||||
{ keywords: ['目录', '路径', '暴露页面', 'dir', 'ffuf', 'gobuster', 'feroxbuster'], tools: ['ffuf', 'gobuster', 'feroxbuster', 'dirsearch'], label: '暴露面探测' },
|
||||
{ keywords: ['证书', 'certificate', 'crt'], tools: ['subfinder'], label: '证书与域名线索收集' },
|
||||
{ keywords: ['云', 'cloud', '配置审计', 'prowler', 'scout'], tools: ['prowler', 'scout-suite'], label: '云配置审计' },
|
||||
{ keywords: ['容器', '镜像', 'k8s', 'kubernetes', 'trivy', 'kube'], tools: ['trivy', 'kube-bench', 'kube-hunter'], label: '容器安全检查' },
|
||||
{ keywords: ['情报', '威胁情报', 'threat', 'ioc', 'virustotal', 'shodan', 'fofa'], tools: ['virustotal_search', 'shodan_search', 'fofa_search'], label: '威胁情报收集' }
|
||||
];
|
||||
const WORKFLOW_AI_HIGH_RISK_RE = /(隔离|封禁|加固|修复|执行|命令|脚本|删除|清理|阻断|封锁|攻击|利用|getshell|shell|payload|exploit|isolate|block|execute|script|delete|exploit|payload)/i;
|
||||
const WORKFLOW_AI_PROGRESS_STEPS = ['understand', 'match', 'draft', 'audit'];
|
||||
|
||||
function esc(text) {
|
||||
if (typeof escapeHtml === 'function') return escapeHtml(text == null ? '' : String(text));
|
||||
@@ -166,6 +184,10 @@
|
||||
function graphToElements(graph) {
|
||||
const nodes = (graph.nodes || []).map((node, index) => ({
|
||||
group: 'nodes',
|
||||
classes: [
|
||||
node.config && node.config.generated_by === 'natural_language' ? 'ai-generated' : '',
|
||||
node.config && (node.config.risk_level === 'high' || node.config.requires_human_confirmation === 'true') ? 'high-risk' : ''
|
||||
].filter(Boolean).join(' '),
|
||||
data: {
|
||||
id: node.id || `node-${index + 1}`,
|
||||
label: node.label || wfNodeLabel(node.type) || node.id || _t('workflows.nodeFallback', { n: index + 1 }),
|
||||
@@ -265,6 +287,8 @@
|
||||
{ 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: 'node.ai-generated', style: { 'border-width': 2, 'border-color': '#38bdf8' } },
|
||||
{ selector: 'node.high-risk', style: { 'border-width': 3, 'border-color': '#f97316' } },
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
@@ -1350,6 +1374,559 @@
|
||||
}
|
||||
}
|
||||
|
||||
function workflowAiOption(id) {
|
||||
const el = document.getElementById(id);
|
||||
return !!(el && el.checked);
|
||||
}
|
||||
|
||||
function workflowAiSleep(ms) {
|
||||
return new Promise(function (resolve) { setTimeout(resolve, ms); });
|
||||
}
|
||||
|
||||
function workflowAiStepLabel(step) {
|
||||
return _t('workflows.ai.steps.' + step);
|
||||
}
|
||||
|
||||
function workflowAiSetProgress(activeStep, done) {
|
||||
workflowAiState.activeStep = activeStep || '';
|
||||
const wrap = document.getElementById('workflow-ai-progress');
|
||||
if (!wrap) return;
|
||||
if (!activeStep && !done) {
|
||||
wrap.hidden = true;
|
||||
wrap.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
wrap.hidden = false;
|
||||
const activeIndex = WORKFLOW_AI_PROGRESS_STEPS.indexOf(activeStep);
|
||||
wrap.innerHTML = WORKFLOW_AI_PROGRESS_STEPS.map(function (step, index) {
|
||||
const complete = done || (activeIndex >= 0 && index < activeIndex);
|
||||
const active = !done && step === activeStep;
|
||||
return `<span class="${complete ? 'is-complete' : ''} ${active ? 'is-active' : ''}">
|
||||
<b>${complete ? '✓' : index + 1}</b>
|
||||
<small>${esc(workflowAiStepLabel(step))}</small>
|
||||
</span>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function workflowAiPreviewItems(graph) {
|
||||
const nodes = (graph.nodes || []).slice().sort(function (a, b) {
|
||||
const ax = a.position && typeof a.position.x === 'number' ? a.position.x : 0;
|
||||
const bx = b.position && typeof b.position.x === 'number' ? b.position.x : 0;
|
||||
const ay = a.position && typeof a.position.y === 'number' ? a.position.y : 0;
|
||||
const by = b.position && typeof b.position.y === 'number' ? b.position.y : 0;
|
||||
return ax === bx ? ay - by : ax - bx;
|
||||
});
|
||||
return nodes.slice(0, 9);
|
||||
}
|
||||
|
||||
function workflowAiPreviewHtml(graph) {
|
||||
const items = workflowAiPreviewItems(graph);
|
||||
if (!items.length) return '';
|
||||
return `<div class="workflow-ai-preview" aria-label="${esc(_t('workflows.ai.preview'))}">
|
||||
<div class="workflow-ai-preview-title">${esc(_t('workflows.ai.preview'))}</div>
|
||||
<div class="workflow-ai-preview-flow">
|
||||
${items.map(function (node, index) {
|
||||
const type = node.type || 'tool';
|
||||
const cfg = node.config || {};
|
||||
const risky = cfg.risk_level === 'high' || cfg.requires_human_confirmation === 'true';
|
||||
return `<span class="workflow-ai-preview-node is-${esc(type)} ${risky ? 'is-risky' : ''}">
|
||||
<small>${esc(wfNodeLabel(type))}</small>
|
||||
<strong>${esc(node.label || wfNodeLabel(type))}</strong>
|
||||
</span>${index < items.length - 1 ? '<i aria-hidden="true">→</i>' : ''}`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function workflowAiSlug(text) {
|
||||
const raw = String(text || '').trim().toLowerCase();
|
||||
const ascii = raw.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
||||
if (ascii) return ascii.slice(0, 48);
|
||||
let hash = 0;
|
||||
for (let i = 0; i < raw.length; i++) hash = ((hash << 5) - hash + raw.charCodeAt(i)) | 0;
|
||||
return 'ai-workflow-' + Math.abs(hash || Date.now()).toString(36);
|
||||
}
|
||||
|
||||
function workflowAiMatchTool(toolNames) {
|
||||
if (!workflowToolOptions.length) return '';
|
||||
const enabled = workflowToolOptions.filter(tool => tool.enabled !== false);
|
||||
const all = enabled.length ? enabled : workflowToolOptions;
|
||||
for (const wanted of toolNames || []) {
|
||||
const lower = String(wanted || '').toLowerCase();
|
||||
const found = all.find(tool => String(tool.key || '').toLowerCase().includes(lower));
|
||||
if (found) return found.key;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function workflowAiDetectCapabilities(prompt) {
|
||||
const lower = String(prompt || '').toLowerCase();
|
||||
const capabilities = [];
|
||||
WORKFLOW_AI_TOOL_HINTS.forEach(function (hint) {
|
||||
if (hint.keywords.some(keyword => lower.indexOf(String(keyword).toLowerCase()) !== -1)) {
|
||||
capabilities.push({
|
||||
label: hint.label,
|
||||
tool_name: workflowAiMatchTool(hint.tools),
|
||||
tool_candidates: hint.tools.slice()
|
||||
});
|
||||
}
|
||||
});
|
||||
if (!capabilities.length) {
|
||||
capabilities.push({ label: _t('workflows.ai.resultCapabilities'), tool_name: '', tool_candidates: [] });
|
||||
}
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
function workflowAiNode(id, type, label, x, y, config) {
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
label,
|
||||
position: { x, y },
|
||||
config: Object.assign(configWithDefaults(type, {}), config || {}, {
|
||||
generated_by: 'natural_language',
|
||||
needs_review: 'true'
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
function workflowAiEdge(id, source, target, label, config) {
|
||||
return {
|
||||
id,
|
||||
source,
|
||||
target,
|
||||
label: label || '',
|
||||
config: config || {}
|
||||
};
|
||||
}
|
||||
|
||||
function workflowAiBuildDraft(prompt, options) {
|
||||
const capabilities = workflowAiDetectCapabilities(prompt);
|
||||
const wantsApproval = /(审批|确认|审核|负责人|人工|review|approve|approval|human)/i.test(prompt);
|
||||
const wantsReport = /(报告|汇总|输出|通知|任务|工单|report|summary|notify|ticket)/i.test(prompt);
|
||||
const wantsCondition = /(如果|发现|存在|高危|新增|失败|通过|否则|if|when|high|critical|new|fail)/i.test(prompt);
|
||||
const highRisk = WORKFLOW_AI_HIGH_RISK_RE.test(prompt);
|
||||
const nodes = [];
|
||||
const edges = [];
|
||||
const assumptions = [];
|
||||
const riskWarnings = [];
|
||||
let x = 120;
|
||||
const y = 150;
|
||||
let nodeIndex = 1;
|
||||
let edgeIndex = 1;
|
||||
let openConditionId = '';
|
||||
|
||||
function nextId(prefix) {
|
||||
const id = prefix + '-' + nodeIndex;
|
||||
nodeIndex += 1;
|
||||
return id;
|
||||
}
|
||||
function add(type, label, config, yy) {
|
||||
const id = nextId(type);
|
||||
nodes.push(workflowAiNode(id, type, label, x, yy || y, config));
|
||||
x += 210;
|
||||
return id;
|
||||
}
|
||||
function connect(source, target, label, config) {
|
||||
edges.push(workflowAiEdge('edge-ai-' + edgeIndex, source, target, label, config));
|
||||
edgeIndex += 1;
|
||||
}
|
||||
|
||||
const start = add('start', wfNodeLabel('start'), { input_keys: 'message, conversationId, projectId, target' });
|
||||
let previous = start;
|
||||
capabilities.forEach(function (capability) {
|
||||
const hasTool = !!capability.tool_name;
|
||||
const id = add(hasTool ? 'tool' : 'agent', capability.label, hasTool ? {
|
||||
tool_name: capability.tool_name,
|
||||
arguments: '{"target":"{{inputs.target}}","message":"{{inputs.message}}"}',
|
||||
timeout_seconds: '120',
|
||||
join_strategy: 'all_merge'
|
||||
} : {
|
||||
agent_mode: 'eino_single',
|
||||
input_binding: { from: 'previous', field: 'output' },
|
||||
instruction: capability.label + '。根据用户需求执行安全流程步骤,并输出结构化结果:' + prompt,
|
||||
output_key: 'agent_result',
|
||||
join_strategy: 'all_merge',
|
||||
missing_tool_candidates: capability.tool_candidates.join(', ')
|
||||
});
|
||||
connect(previous, id);
|
||||
previous = id;
|
||||
if (!hasTool && capability.tool_candidates.length) {
|
||||
assumptions.push(capability.label + ' 未匹配到已启用工具,已生成 Agent 草稿节点。');
|
||||
}
|
||||
});
|
||||
|
||||
if (wantsCondition) {
|
||||
const condition = add('condition', highRisk ? '是否需要高风险处置' : '是否满足触发条件', {
|
||||
expression: highRisk ? '{{previous.output}} contains "高危"' : '{{previous.output}} != ""',
|
||||
join_strategy: 'all_merge'
|
||||
});
|
||||
connect(previous, condition);
|
||||
openConditionId = condition;
|
||||
const report = add('output', wantsReport ? '输出报告' : wfNodeLabel('output'), {
|
||||
output_key: 'result',
|
||||
source_binding: { from: 'previous', field: 'output' },
|
||||
static_value: '',
|
||||
join_strategy: 'all_merge'
|
||||
}, y + 130);
|
||||
connect(condition, report, _t('workflows.edges.no'), { condition: '{{previous.matched}} == "false"', branch: 'false' });
|
||||
previous = condition;
|
||||
}
|
||||
|
||||
if (highRisk) {
|
||||
let insertedApproval = false;
|
||||
if (!options.allowHighRisk || wantsApproval) {
|
||||
const approval = add('hitl', '人工审批', {
|
||||
prompt: '请确认是否允许继续执行高风险处置:' + prompt,
|
||||
prompt_binding: { from: 'previous', field: 'output' },
|
||||
reviewer: 'human',
|
||||
join_strategy: 'all_merge',
|
||||
risk_level: 'high'
|
||||
});
|
||||
connect(previous, approval, previous === openConditionId ? _t('workflows.edges.yes') : '', previous === openConditionId ? { condition: '{{previous.matched}} == "true"', branch: 'true' } : {});
|
||||
if (previous === openConditionId) openConditionId = '';
|
||||
previous = approval;
|
||||
insertedApproval = true;
|
||||
}
|
||||
const action = add('agent', '执行受控处置', {
|
||||
agent_mode: 'eino_single',
|
||||
input_binding: { from: 'previous', field: 'output' },
|
||||
instruction: '仅在授权范围内生成处置步骤草稿;实际执行前必须由人工确认。用户需求:' + prompt,
|
||||
output_key: 'remediation_plan',
|
||||
join_strategy: 'all_merge',
|
||||
risk_level: 'high',
|
||||
requires_human_confirmation: 'true'
|
||||
});
|
||||
connect(previous, action, previous === openConditionId ? _t('workflows.edges.yes') : '', previous === openConditionId ? { condition: '{{previous.matched}} == "true"', branch: 'true' } : {});
|
||||
if (previous === openConditionId) openConditionId = '';
|
||||
previous = action;
|
||||
riskWarnings.push(insertedApproval
|
||||
? '检测到高风险动作,已加入人工确认与 requires_human_confirmation 标记。'
|
||||
: '检测到高风险动作,已保留为草稿并添加 requires_human_confirmation 标记。');
|
||||
} else if (wantsApproval && !nodes.some(node => node.type === 'hitl')) {
|
||||
const approval = add('hitl', '人工审批', {
|
||||
prompt: '请审核工作流阶段结果:' + prompt,
|
||||
prompt_binding: { from: 'previous', field: 'output' },
|
||||
reviewer: 'human',
|
||||
join_strategy: 'all_merge'
|
||||
});
|
||||
connect(previous, approval);
|
||||
previous = approval;
|
||||
}
|
||||
|
||||
const output = add('output', wantsReport ? '输出报告' : wfNodeLabel('output'), {
|
||||
output_key: 'result',
|
||||
source_binding: { from: 'previous', field: 'output' },
|
||||
static_value: '',
|
||||
join_strategy: 'all_merge'
|
||||
});
|
||||
connect(previous, output, previous === openConditionId ? _t('workflows.edges.yes') : '', previous === openConditionId ? { condition: '{{previous.matched}} == "true"', branch: 'true' } : {});
|
||||
|
||||
const graph = {
|
||||
nodes,
|
||||
edges,
|
||||
config: {
|
||||
schema_version: 1,
|
||||
generated_by: 'natural_language',
|
||||
source_prompt: prompt,
|
||||
objective: options.includeObjective ? prompt : ''
|
||||
}
|
||||
};
|
||||
if (options.allowSchedule && /(每天|每周|定时|周期|持续|daily|weekly|schedule|monitor)/i.test(prompt)) {
|
||||
graph.config.trigger_suggestion = /(每天|daily)/i.test(prompt) ? 'daily' : 'scheduled';
|
||||
assumptions.push('已记录定时触发建议;保存后仍需在触发器或角色绑定处配置。');
|
||||
}
|
||||
return {
|
||||
graph,
|
||||
meta: {
|
||||
id: workflowAiSlug(prompt),
|
||||
name: prompt.length > 22 ? prompt.slice(0, 22) + '...' : prompt,
|
||||
description: prompt,
|
||||
enabled: true
|
||||
},
|
||||
generator: 'deterministic-fallback',
|
||||
audit: {
|
||||
risk_warnings: riskWarnings,
|
||||
assumptions: assumptions,
|
||||
high_risk: highRisk,
|
||||
needs_hitl: nodes.some(node => node.type === 'hitl'),
|
||||
savable: validateWorkflowGraph(graph).length === 0
|
||||
},
|
||||
assumptions,
|
||||
riskWarnings,
|
||||
capabilities,
|
||||
stats: { nodes: nodes.length, edges: edges.length }
|
||||
};
|
||||
}
|
||||
|
||||
function workflowAiAvailableToolsPayload() {
|
||||
return (workflowToolOptions || []).map(function (tool) {
|
||||
return {
|
||||
key: tool.key || tool.name || '',
|
||||
name: tool.name || '',
|
||||
enabled: tool.enabled !== false
|
||||
};
|
||||
}).filter(function (tool) {
|
||||
return tool.key || tool.name;
|
||||
});
|
||||
}
|
||||
|
||||
async function workflowAiGenerateDraftOnServer(prompt, options) {
|
||||
const response = await apiFetch('/api/workflows/generate-draft', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
prompt: prompt,
|
||||
options: {
|
||||
include_objective: !!options.includeObjective,
|
||||
allow_schedule: !!options.allowSchedule,
|
||||
allow_high_risk: !!options.allowHighRisk
|
||||
},
|
||||
available_tools: workflowAiAvailableToolsPayload()
|
||||
})
|
||||
});
|
||||
const data = await response.json().catch(function () { return {}; });
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || _t('workflows.ai.generateFailed'));
|
||||
}
|
||||
const result = data.result || data;
|
||||
if (!result || !result.graph) {
|
||||
throw new Error(_t('workflows.ai.generateFailed'));
|
||||
}
|
||||
result.generator = result.generator || 'server';
|
||||
return result;
|
||||
}
|
||||
|
||||
function workflowAiRenderResult(result) {
|
||||
const target = document.getElementById('workflow-ai-result');
|
||||
const applyBtn = document.getElementById('workflow-ai-apply-btn');
|
||||
if (!target) return;
|
||||
if (!result) {
|
||||
target.innerHTML = '';
|
||||
if (applyBtn) applyBtn.disabled = true;
|
||||
return;
|
||||
}
|
||||
const graph = result.graph || defaultGraph();
|
||||
const errors = validateWorkflowGraph(graph);
|
||||
const audit = result.audit || {};
|
||||
const risks = audit.risk_warnings || result.riskWarnings || [];
|
||||
const assumptions = audit.assumptions || result.assumptions || [];
|
||||
const missing = audit.missing_fields || [];
|
||||
const stats = result.stats || {};
|
||||
const generator = String(result.generator || '');
|
||||
const generatorKey = generator === 'llm'
|
||||
? 'workflows.ai.generatorLLM'
|
||||
: (generator === 'deterministic'
|
||||
? 'workflows.ai.generatorServer'
|
||||
: (generator.indexOf('fallback') !== -1 ? 'workflows.ai.generatorFallback' : 'workflows.ai.generatorUnknown'));
|
||||
const capabilityText = (result.capabilities || []).map(function (cap) {
|
||||
return cap.label + (cap.tool_name ? ' -> ' + cap.tool_name : (cap.toolName ? ' -> ' + cap.toolName : ''));
|
||||
});
|
||||
target.innerHTML = `
|
||||
${workflowAiPreviewHtml(graph)}
|
||||
<div class="workflow-ai-result-grid">
|
||||
<span>${esc(_t('workflows.ai.resultNodes'))}<strong>${stats.nodes || (graph.nodes || []).length}</strong></span>
|
||||
<span>${esc(_t('workflows.ai.resultEdges'))}<strong>${stats.edges || (graph.edges || []).length}</strong></span>
|
||||
<span>${esc(_t('workflows.ai.resultRisk'))}<strong>${risks.length ? esc(_t('workflows.ai.riskHigh')) : esc(_t('workflows.ai.riskLow'))}</strong></span>
|
||||
<span>${esc(_t('workflows.ai.resultSaveState'))}<strong>${errors.length ? esc(_t('workflows.ai.no')) : esc(_t('workflows.ai.yes'))}</strong></span>
|
||||
</div>
|
||||
<div class="workflow-ai-result-section"><strong>${esc(_t(generatorKey))}</strong><p>${esc(generator === 'llm' ? _t('workflows.ai.llmTitle') : (generator === 'deterministic' ? _t('workflows.ai.serverTitle') : _t('workflows.ai.fallbackTitle')))}</p></div>
|
||||
${capabilityText.length ? '<div class="workflow-ai-result-section"><strong>' + esc(_t('workflows.ai.capabilityTrace')) + '</strong><ul>' + capabilityText.map(item => '<li>' + esc(item) + '</li>').join('') + '</ul></div>' : ''}
|
||||
${missing.length ? '<div class="workflow-ai-result-section is-attention"><strong>' + esc(_t('workflows.ai.missingFields')) + '</strong><ul>' + missing.map(item => '<li>' + esc(item) + '</li>').join('') + '</ul></div>' : ''}
|
||||
${assumptions.length ? '<div class="workflow-ai-result-section"><strong>' + esc(_t('workflows.ai.assumptions')) + '</strong><ul>' + assumptions.map(item => '<li>' + esc(item) + '</li>').join('') + '</ul></div>' : ''}
|
||||
${risks.length ? '<div class="workflow-ai-result-section is-warning"><strong>' + esc(_t('workflows.ai.riskWarnings')) + '</strong><ul>' + risks.map(item => '<li>' + esc(item) + '</li>').join('') + '</ul></div>' : ''}
|
||||
${errors.length ? '<div class="workflow-ai-result-section is-warning"><strong>' + esc(_t('workflows.audit.validationIssues')) + '</strong><ul>' + errors.map(item => '<li>' + esc(item) + '</li>').join('') + '</ul></div>' : ''}
|
||||
`;
|
||||
if (applyBtn) applyBtn.disabled = !!errors.length;
|
||||
}
|
||||
|
||||
function workflowAiApplyGraph(result) {
|
||||
if (!result || !result.graph) return;
|
||||
fillWorkflowForm({
|
||||
id: '',
|
||||
name: result.meta.name,
|
||||
description: result.meta.description,
|
||||
enabled: true,
|
||||
graph_json: result.graph
|
||||
});
|
||||
syncWorkflowMetaIdField(false, result.meta.id);
|
||||
updateWorkflowCanvasTitle();
|
||||
if (cy && cy.nodes().length) {
|
||||
cy.fit(cy.elements(), 60);
|
||||
const generated = cy.nodes('.ai-generated');
|
||||
if (generated.length) {
|
||||
generated.addClass('just-added');
|
||||
const risky = generated.filter('.high-risk');
|
||||
if (risky.length) risky.select();
|
||||
setTimeout(function () {
|
||||
if (cy) cy.nodes('.ai-generated').removeClass('just-added');
|
||||
}, 1200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function workflowAiSortedNodes(graph) {
|
||||
return (graph.nodes || []).slice().sort(function (a, b) {
|
||||
const ax = a.position && typeof a.position.x === 'number' ? a.position.x : 0;
|
||||
const bx = b.position && typeof b.position.x === 'number' ? b.position.x : 0;
|
||||
const ay = a.position && typeof a.position.y === 'number' ? a.position.y : 0;
|
||||
const by = b.position && typeof b.position.y === 'number' ? b.position.y : 0;
|
||||
return ax === bx ? ay - by : ax - bx;
|
||||
});
|
||||
}
|
||||
|
||||
async function workflowAiAnimateGraph(result) {
|
||||
if (!result || !result.graph || workflowAiState.animating) return;
|
||||
workflowAiState.animating = true;
|
||||
const status = document.getElementById('workflow-ai-canvas-status');
|
||||
if (status) status.hidden = false;
|
||||
try {
|
||||
initCy();
|
||||
if (!cy) {
|
||||
workflowAiApplyGraph(result);
|
||||
return;
|
||||
}
|
||||
const graph = parseGraph(result.graph);
|
||||
syncWorkflowMetaForm({
|
||||
id: '',
|
||||
name: result.meta && result.meta.name ? result.meta.name : '',
|
||||
description: result.meta && result.meta.description ? result.meta.description : '',
|
||||
enabled: true
|
||||
});
|
||||
currentWorkflowId = '';
|
||||
syncWorkflowMetaIdField(false, result.meta && result.meta.id ? result.meta.id : '');
|
||||
updateWorkflowCanvasTitle();
|
||||
resetSequences(graph);
|
||||
cy.elements().remove();
|
||||
updateEmptyState();
|
||||
closeWorkflowDryRunPanel();
|
||||
renderWorkflowList();
|
||||
const nodeElements = graphToElements({ nodes: workflowAiSortedNodes(graph), edges: [] });
|
||||
const edgeElements = graphToElements({ nodes: [], edges: graph.edges || [] });
|
||||
for (const ele of nodeElements) {
|
||||
const added = cy.add(ele);
|
||||
selectWorkflowElement(added);
|
||||
added.addClass('just-added');
|
||||
cy.animate({ center: { eles: added }, duration: 180 });
|
||||
await workflowAiSleep(120);
|
||||
added.removeClass('just-added');
|
||||
}
|
||||
for (const edge of edgeElements) {
|
||||
const added = cy.add(edge);
|
||||
added.select();
|
||||
await workflowAiSleep(80);
|
||||
added.unselect();
|
||||
}
|
||||
if (cy.nodes().length) {
|
||||
layoutWorkflowGraph(true);
|
||||
await workflowAiSleep(320);
|
||||
const risky = cy.nodes('.high-risk');
|
||||
if (risky.length) {
|
||||
selectWorkflowElement(risky.first());
|
||||
} else {
|
||||
selectWorkflowElement(null);
|
||||
}
|
||||
}
|
||||
updateEmptyState();
|
||||
} finally {
|
||||
workflowAiState.animating = false;
|
||||
if (status) status.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
window.openWorkflowAiModal = function () {
|
||||
if (typeof requirePermission === 'function' && !requirePermission('workflow:write')) return;
|
||||
workflowAiState.draft = null;
|
||||
workflowAiState.result = null;
|
||||
workflowAiSetProgress('', false);
|
||||
workflowAiRenderResult(null);
|
||||
const prompt = document.getElementById('workflow-ai-prompt');
|
||||
const generateBtn = document.getElementById('workflow-ai-generate-btn');
|
||||
if (generateBtn) generateBtn.disabled = false;
|
||||
if (typeof openAppModal === 'function') {
|
||||
openAppModal('workflow-ai-modal', { focusEl: prompt });
|
||||
}
|
||||
};
|
||||
|
||||
window.closeWorkflowAiModal = function () {
|
||||
if (typeof closeAppModal === 'function') closeAppModal('workflow-ai-modal');
|
||||
};
|
||||
|
||||
window.useWorkflowAiExample = function (key) {
|
||||
const prompt = document.getElementById('workflow-ai-prompt');
|
||||
if (!prompt) return;
|
||||
prompt.value = _t('workflows.ai.examples.' + key);
|
||||
prompt.focus();
|
||||
};
|
||||
|
||||
window.generateWorkflowFromNaturalLanguage = async function () {
|
||||
const promptEl = document.getElementById('workflow-ai-prompt');
|
||||
const generateBtn = document.getElementById('workflow-ai-generate-btn');
|
||||
const prompt = promptEl ? promptEl.value.trim() : '';
|
||||
if (!prompt) {
|
||||
if (typeof showNotification === 'function') showNotification(_t('workflows.ai.promptRequired'), 'warning');
|
||||
return;
|
||||
}
|
||||
if (generateBtn) {
|
||||
generateBtn.disabled = true;
|
||||
generateBtn.textContent = _t('workflows.ai.generating');
|
||||
}
|
||||
try {
|
||||
workflowAiRenderResult(null);
|
||||
workflowAiSetProgress('understand');
|
||||
await workflowAiSleep(140);
|
||||
await loadWorkflowTools();
|
||||
workflowAiSetProgress('match');
|
||||
await workflowAiSleep(140);
|
||||
const options = {
|
||||
includeObjective: workflowAiOption('workflow-ai-include-objective'),
|
||||
allowSchedule: workflowAiOption('workflow-ai-allow-schedule'),
|
||||
allowHighRisk: workflowAiOption('workflow-ai-allow-high-risk')
|
||||
};
|
||||
workflowAiSetProgress('draft');
|
||||
const result = await workflowAiGenerateDraftOnServer(prompt, options);
|
||||
workflowAiSetProgress('audit');
|
||||
await workflowAiSleep(120);
|
||||
workflowAiState.draft = result.graph;
|
||||
workflowAiState.result = result;
|
||||
workflowAiRenderResult(result);
|
||||
workflowAiSetProgress('', true);
|
||||
if (validateWorkflowGraph(result.graph).length && typeof showNotification === 'function') {
|
||||
showNotification(_t('workflows.ai.generatedWithIssues'), 'warning');
|
||||
}
|
||||
} catch (error) {
|
||||
workflowAiState.draft = null;
|
||||
workflowAiState.result = null;
|
||||
workflowAiSetProgress('', false);
|
||||
workflowAiRenderResult(null);
|
||||
if (typeof showNotification === 'function') showNotification(error.message || _t('workflows.ai.generateFailed'), 'error');
|
||||
} finally {
|
||||
if (generateBtn) {
|
||||
generateBtn.disabled = false;
|
||||
generateBtn.textContent = _t('workflows.ai.generate');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.applyWorkflowAiDraft = async function () {
|
||||
if (!workflowAiState.result) return;
|
||||
const applyBtn = document.getElementById('workflow-ai-apply-btn');
|
||||
if (applyBtn) {
|
||||
applyBtn.disabled = true;
|
||||
applyBtn.textContent = _t('workflows.ai.rendering');
|
||||
}
|
||||
closeWorkflowAiModal();
|
||||
try {
|
||||
await workflowAiAnimateGraph(workflowAiState.result);
|
||||
if (typeof showNotification === 'function') showNotification(_t('workflows.ai.applied'), 'success');
|
||||
} finally {
|
||||
if (applyBtn) {
|
||||
applyBtn.disabled = false;
|
||||
applyBtn.textContent = _t('workflows.ai.apply');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.refreshWorkflows = async function () {
|
||||
initCy();
|
||||
const list = document.getElementById('workflow-list');
|
||||
@@ -1818,15 +2395,20 @@
|
||||
|
||||
window.layoutWorkflowGraph = function (animate) {
|
||||
if (!cy || !cy.nodes().length) return;
|
||||
cy.layout({
|
||||
const layout = cy.layout({
|
||||
name: 'breadthfirst',
|
||||
directed: true,
|
||||
padding: 40,
|
||||
spacingFactor: 1.25,
|
||||
animate: animate !== false,
|
||||
animationDuration: 250
|
||||
}).run();
|
||||
cy.fit(undefined, 40);
|
||||
});
|
||||
layout.one('layoutstop', function () {
|
||||
if (cy && cy.elements().length) cy.fit(cy.elements(), 40);
|
||||
});
|
||||
layout.run();
|
||||
if (animate === false && cy.elements().length) cy.fit(cy.elements(), 40);
|
||||
return layout;
|
||||
};
|
||||
|
||||
window.updateWorkflowSelectedProperty = function () {
|
||||
@@ -2458,7 +3040,7 @@
|
||||
if (page && typeof window.applyTranslations === 'function') {
|
||||
window.applyTranslations(page);
|
||||
}
|
||||
['workflow-dry-run-modal', 'workflow-package-import-modal', 'workflow-package-overwrite-modal'].forEach(function (id) {
|
||||
['workflow-dry-run-modal', 'workflow-ai-modal', 'workflow-package-import-modal', 'workflow-package-overwrite-modal'].forEach(function (id) {
|
||||
const modal = document.getElementById(id);
|
||||
if (modal && typeof window.applyTranslations === 'function') window.applyTranslations(modal);
|
||||
});
|
||||
|
||||
@@ -3088,6 +3088,7 @@
|
||||
<div class="page-header-actions">
|
||||
<button class="btn-secondary" onclick="refreshWorkflows()" data-i18n="common.refresh">刷新</button>
|
||||
<button class="btn-secondary" data-require-permission="workflow:write" type="button" onclick="openWorkflowPackageImportModal()" data-i18n="workflows.package.importLocal">导入本地包</button>
|
||||
<button class="btn-secondary" data-require-permission="workflow:write" type="button" onclick="openWorkflowAiModal()" data-i18n="workflows.ai.open">用自然语言创建</button>
|
||||
<button class="btn-primary" onclick="newWorkflowDraft()" data-i18n="workflows.newGraph">新建工作流</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3139,6 +3140,10 @@
|
||||
<svg aria-hidden="true" width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5.4v13.2a1 1 0 0 0 1.55.84l9.2-6.6a1 1 0 0 0 0-1.68l-9.2-6.6A1 1 0 0 0 8 5.4Z"/></svg>
|
||||
<span class="workflow-toolbar-label" data-i18n="workflows.dryRun">试运行</span>
|
||||
</button>
|
||||
<button class="btn-secondary btn-small workflow-toolbar-button workflow-ai-button" data-require-permission="workflow:write" type="button" onclick="openWorkflowAiModal()">
|
||||
<svg aria-hidden="true" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5L12 3Z"/><path d="M19 14l.8 2.2L22 17l-2.2.8L19 20l-.8-2.2L16 17l2.2-.8L19 14Z"/></svg>
|
||||
<span class="workflow-toolbar-label" data-i18n="workflows.ai.generate">生成草稿</span>
|
||||
</button>
|
||||
<details class="workflow-more-actions" id="workflow-more-actions" data-require-permission-any="workflow:read workflow:delete">
|
||||
<summary class="btn-secondary btn-small workflow-more-actions-trigger" aria-haspopup="menu">
|
||||
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="1.8"/><circle cx="12" cy="12" r="1.8"/><circle cx="19" cy="12" r="1.8"/></svg>
|
||||
@@ -3164,7 +3169,8 @@
|
||||
</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" data-i18n="workflows.canvasEmpty">从左侧拖拽节点到画布,或点击节点按钮快速添加</div>
|
||||
<div id="workflow-canvas-empty" class="workflow-canvas-empty" data-i18n="workflows.canvasEmpty">从左侧拖拽节点到画布,或用自然语言生成工作流</div>
|
||||
<div id="workflow-ai-canvas-status" class="workflow-ai-canvas-status" hidden data-i18n="workflows.ai.canvasRendering">AI 正在编排画布…</div>
|
||||
</section>
|
||||
</main>
|
||||
<aside class="workflow-properties">
|
||||
@@ -5568,6 +5574,40 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div id="workflow-ai-modal" class="modal" style="display: none;" role="dialog" aria-modal="true" aria-labelledby="workflow-ai-modal-title" onclick="if(event.target===this)closeWorkflowAiModal()" onkeydown="if(event.key==='Escape')closeWorkflowAiModal()">
|
||||
<form class="modal-content workflow-ai-modal-content" onsubmit="event.preventDefault(); generateWorkflowFromNaturalLanguage()" onclick="event.stopPropagation()">
|
||||
<div class="modal-header workflow-ai-modal-header">
|
||||
<div>
|
||||
<h2 id="workflow-ai-modal-title" data-i18n="workflows.ai.title">用自然语言创建工作流</h2>
|
||||
<p class="workflow-ai-subtitle" data-i18n="workflows.ai.subtitle">AI 会生成可编辑草稿,不会自动保存或运行。</p>
|
||||
</div>
|
||||
<button type="button" class="modal-close" onclick="closeWorkflowAiModal()" data-i18n="common.close" data-i18n-attr="aria-label" data-i18n-skip-text="true" aria-label="关闭"></button>
|
||||
</div>
|
||||
<div class="modal-body workflow-ai-modal-body">
|
||||
<div class="form-group workflow-ai-prompt-field">
|
||||
<label for="workflow-ai-prompt" data-i18n="workflows.ai.promptLabel">工作流需求</label>
|
||||
<textarea id="workflow-ai-prompt" class="form-input" rows="5" data-i18n="workflows.ai.promptPlaceholder" data-i18n-attr="placeholder" placeholder="例如:持续监控一个域名的子域名、证书和暴露页面,发现新增资产后生成报告"></textarea>
|
||||
</div>
|
||||
<div class="workflow-ai-examples" aria-label="示例需求" data-i18n="workflows.ai.examplesLabel" data-i18n-attr="aria-label">
|
||||
<button type="button" class="btn-secondary btn-small" onclick="useWorkflowAiExample('domainMonitor')" data-i18n="workflows.ai.exampleDomain">域名监控</button>
|
||||
<button type="button" class="btn-secondary btn-small" onclick="useWorkflowAiExample('reportReview')" data-i18n="workflows.ai.exampleReport">报告审批</button>
|
||||
<button type="button" class="btn-secondary btn-small" onclick="useWorkflowAiExample('assetTriage')" data-i18n="workflows.ai.exampleTriage">资产研判</button>
|
||||
</div>
|
||||
<div class="workflow-ai-options">
|
||||
<label><input type="checkbox" id="workflow-ai-include-objective" checked> <span data-i18n="workflows.ai.includeObjective">同时生成 objective 配置</span></label>
|
||||
<label><input type="checkbox" id="workflow-ai-allow-schedule"> <span data-i18n="workflows.ai.allowSchedule">允许生成定时触发建议</span></label>
|
||||
<label><input type="checkbox" id="workflow-ai-allow-high-risk"> <span data-i18n="workflows.ai.allowHighRisk">允许包含高风险节点草稿</span></label>
|
||||
</div>
|
||||
<div id="workflow-ai-progress" class="workflow-ai-progress" hidden aria-live="polite"></div>
|
||||
<div id="workflow-ai-result" class="workflow-ai-result" aria-live="polite"></div>
|
||||
</div>
|
||||
<div class="modal-footer workflow-ai-modal-footer">
|
||||
<button type="button" class="btn-secondary btn-small" onclick="closeWorkflowAiModal()" data-i18n="common.cancel">取消</button>
|
||||
<button id="workflow-ai-apply-btn" type="button" class="btn-secondary btn-small" onclick="applyWorkflowAiDraft()" disabled data-i18n="workflows.ai.apply">渲染到画布</button>
|
||||
<button id="workflow-ai-generate-btn" type="submit" class="btn-primary btn-small" data-i18n="workflows.ai.generate">生成草稿</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div id="workflow-package-import-modal" class="modal workflow-package-modal" style="display: none;" role="dialog" aria-modal="true" aria-labelledby="workflow-package-import-title">
|
||||
<div class="modal-content workflow-package-modal-content">
|
||||
<div class="modal-header workflow-package-modal-header">
|
||||
|
||||
Reference in New Issue
Block a user