diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json
index 23d6b21c..c91db344 100644
--- a/web/static/i18n/en-US.json
+++ b/web/static/i18n/en-US.json
@@ -178,7 +178,6 @@
"taskCancelled": "Task cancelled",
"unknownTool": "Unknown tool",
"einoAgentReplyTitle": "Sub-agent reply",
- "einoRecoveryTitle": "🔄 Invalid tool JSON · run {{n}}/{{max}} (hint appended)",
"einoStreamErrorTitle": "⚠️ Eino stream interrupted ({{agent}})",
"einoStreamErrorMessage": "Streaming read failed; the system will retry or terminate according to policy.",
"iterationLimitReachedTitle": "⛔ Iteration limit reached",
diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json
index be506859..2da9a2e8 100644
--- a/web/static/i18n/zh-CN.json
+++ b/web/static/i18n/zh-CN.json
@@ -178,7 +178,6 @@
"taskCancelled": "任务已取消",
"unknownTool": "未知工具",
"einoAgentReplyTitle": "子代理回复",
- "einoRecoveryTitle": "🔄 工具参数无效 · 第 {{n}}/{{max}} 轮(已追加提示)",
"einoStreamErrorTitle": "⚠️ Eino 流式中断({{agent}})",
"einoStreamErrorMessage": "流式读取异常,系统将按策略重试或结束。",
"iterationLimitReachedTitle": "⛔ 达到迭代上限",
diff --git a/web/static/js/chat.js b/web/static/js/chat.js
index 3797d91c..89408057 100644
--- a/web/static/js/chat.js
+++ b/web/static/js/chat.js
@@ -2226,10 +2226,6 @@ function renderProcessDetails(messageId, processDetails) {
itemTitle = agPx + execLine;
} else if (eventType === 'eino_agent_reply') {
itemTitle = agPx + '💬 ' + (typeof window.t === 'function' ? window.t('chat.einoAgentReplyTitle') : '子代理回复');
- } else if (eventType === 'eino_recovery') {
- const ri = data.runIndex != null ? data.runIndex : (data.einoRetry != null ? data.einoRetry + 1 : 1);
- const mx = data.maxRuns != null ? data.maxRuns : 3;
- itemTitle = (typeof window.t === 'function' ? window.t('chat.einoRecoveryTitle', { n: ri, max: mx }) : ('🔄 第 ' + ri + '/' + mx + ' 轮(已追加提示)'));
} else if (eventType === 'knowledge_retrieval') {
itemTitle = '📚 ' + (typeof window.t === 'function' ? window.t('chat.knowledgeRetrieval') : '知识检索');
} else if (eventType === 'error') {
diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js
index 5d3e102b..61081bff 100644
--- a/web/static/js/monitor.js
+++ b/web/static/js/monitor.js
@@ -1133,24 +1133,6 @@ function handleStreamEvent(event, progressElement, progressId,
});
break;
- case 'eino_recovery': {
- const d = event.data || {};
- const runIdx = d.runIndex != null ? d.runIndex : (d.einoRetry != null ? d.einoRetry + 1 : 1);
- const maxRuns = d.maxRuns != null ? d.maxRuns : 3;
- const title = typeof window.t === 'function'
- ? window.t('chat.einoRecoveryTitle', { n: runIdx, max: maxRuns })
- : ('🔄 工具参数无效 · 第 ' + runIdx + '/' + maxRuns + ' 轮(已追加提示)');
- addTimelineItem(timeline, 'eino_recovery', {
- title: title,
- message: event.message || '',
- data: event.data
- });
- // If the backend triggers a recovery run, any "running" tool_call items in this progress
- // should be closed to avoid being stuck forever.
- finalizeOutstandingToolCallsForProgress(progressId, 'failed');
- break;
- }
-
case 'eino_stream_error': {
const d = event.data || {};
const agent = d.einoAgent ? String(d.einoAgent) : '';
@@ -2190,15 +2172,6 @@ function addTimelineItem(timeline, type, options) {
if (type === 'progress' && options.message) {
item.dataset.progressMessage = options.message;
}
- if (type === 'eino_recovery' && options.data) {
- const d = options.data;
- if (d.runIndex != null) {
- item.dataset.recoveryRunIndex = String(d.runIndex);
- }
- if (d.maxRuns != null) {
- item.dataset.recoveryMaxRuns = String(d.maxRuns);
- }
- }
if (type === 'tool_calls_detected' && options.data && options.data.count != null) {
item.dataset.toolCallsCount = String(options.data.count);
}
@@ -2309,12 +2282,6 @@ function addTimelineItem(timeline, type, options) {
`;
- } else if (type === 'eino_recovery' && options.message) {
- content += `
-
- ${escapeHtml(options.message).replace(/\n/g, '
')}
-
- `;
} else if (type === 'cancelled') {
const taskCancelledLabel = typeof window.t === 'function' ? window.t('chat.taskCancelled') : '任务已取消';
content += `
@@ -3197,10 +3164,6 @@ function refreshProgressAndTimelineI18n() {
titleSpan.textContent = ap + icon + (success ? _t('chat.toolExecComplete', { name: name }) : _t('chat.toolExecFailed', { name: name }));
} else if (type === 'eino_agent_reply') {
titleSpan.textContent = ap + '\uD83D\uDCAC ' + _t('chat.einoAgentReplyTitle');
- } else if (type === 'eino_recovery' && item.dataset.recoveryRunIndex) {
- const n = parseInt(item.dataset.recoveryRunIndex, 10) || 1;
- const mx = parseInt(item.dataset.recoveryMaxRuns, 10) || 3;
- titleSpan.textContent = _t('chat.einoRecoveryTitle', { n: n, max: mx });
} else if (type === 'cancelled') {
titleSpan.textContent = '\u26D4 ' + _t('chat.taskCancelled');
} else if (type === 'progress' && item.dataset.progressMessage !== undefined) {
diff --git a/web/static/js/vulnerability.js b/web/static/js/vulnerability.js
index 59590192..1628c447 100644
--- a/web/static/js/vulnerability.js
+++ b/web/static/js/vulnerability.js
@@ -10,6 +10,9 @@ let currentVulnerabilityId = null;
let vulnerabilityFilters = {
id: '',
conversation_id: '',
+ task_id: '',
+ conversation_tag: '',
+ task_tag: '',
severity: '',
status: ''
};
@@ -106,6 +109,15 @@ async function loadVulnerabilities(page = null) {
if (vulnerabilityFilters.conversation_id) {
params.append('conversation_id', vulnerabilityFilters.conversation_id);
}
+ if (vulnerabilityFilters.task_id) {
+ params.append('task_id', vulnerabilityFilters.task_id);
+ }
+ if (vulnerabilityFilters.conversation_tag) {
+ params.append('conversation_tag', vulnerabilityFilters.conversation_tag);
+ }
+ if (vulnerabilityFilters.task_tag) {
+ params.append('task_tag', vulnerabilityFilters.task_tag);
+ }
if (vulnerabilityFilters.severity) {
params.append('severity', vulnerabilityFilters.severity);
}
@@ -241,6 +253,10 @@ function renderVulnerabilities(vulnerabilities) {
${vuln.type ? `类型: ${escapeHtml(vuln.type)}
` : ''}
${vuln.target ? `目标: ${escapeHtml(vuln.target)}
` : ''}
会话ID: ${escapeHtml(vuln.conversation_id)}
+ ${vuln.task_id ? `任务ID: ${escapeHtml(vuln.task_id)}
` : ''}
+ ${vuln.task_queue_id ? `任务队列ID: ${escapeHtml(vuln.task_queue_id)}
` : ''}
+ ${vuln.conversation_tag ? `对话标签: ${escapeHtml(vuln.conversation_tag)}
` : ''}
+ ${vuln.task_tag ? `任务标签: ${escapeHtml(vuln.task_tag)}
` : ''}
${vuln.proof ? `证明:${escapeHtml(vuln.proof)} ` : ''}
${vuln.impact ? `影响: ${escapeHtml(vuln.impact)}
` : ''}
@@ -338,6 +354,8 @@ function showAddVulnerabilityModal() {
// 清空表单
document.getElementById('vulnerability-conversation-id').value = '';
+ document.getElementById('vulnerability-conversation-tag').value = '';
+ document.getElementById('vulnerability-task-tag').value = '';
document.getElementById('vulnerability-title').value = '';
document.getElementById('vulnerability-description').value = '';
document.getElementById('vulnerability-severity').value = '';
@@ -363,6 +381,8 @@ async function editVulnerability(id) {
// 填充表单
document.getElementById('vulnerability-conversation-id').value = vuln.conversation_id || '';
+ document.getElementById('vulnerability-conversation-tag').value = vuln.conversation_tag || '';
+ document.getElementById('vulnerability-task-tag').value = vuln.task_tag || '';
document.getElementById('vulnerability-title').value = vuln.title || '';
document.getElementById('vulnerability-description').value = vuln.description || '';
document.getElementById('vulnerability-severity').value = vuln.severity || '';
@@ -393,6 +413,8 @@ async function saveVulnerability() {
const data = {
conversation_id: conversationId,
+ conversation_tag: document.getElementById('vulnerability-conversation-tag').value.trim(),
+ task_tag: document.getElementById('vulnerability-task-tag').value.trim(),
title: title,
description: document.getElementById('vulnerability-description').value.trim(),
severity: severity,
@@ -472,6 +494,9 @@ function closeVulnerabilityModal() {
function filterVulnerabilities() {
vulnerabilityFilters.id = document.getElementById('vulnerability-id-filter').value.trim();
vulnerabilityFilters.conversation_id = document.getElementById('vulnerability-conversation-filter').value.trim();
+ vulnerabilityFilters.task_id = document.getElementById('vulnerability-task-filter').value.trim();
+ vulnerabilityFilters.conversation_tag = document.getElementById('vulnerability-conversation-tag-filter').value.trim();
+ vulnerabilityFilters.task_tag = document.getElementById('vulnerability-task-tag-filter').value.trim();
vulnerabilityFilters.severity = document.getElementById('vulnerability-severity-filter').value;
vulnerabilityFilters.status = document.getElementById('vulnerability-status-filter').value;
@@ -486,12 +511,18 @@ function filterVulnerabilities() {
function clearVulnerabilityFilters() {
document.getElementById('vulnerability-id-filter').value = '';
document.getElementById('vulnerability-conversation-filter').value = '';
+ document.getElementById('vulnerability-task-filter').value = '';
+ document.getElementById('vulnerability-conversation-tag-filter').value = '';
+ document.getElementById('vulnerability-task-tag-filter').value = '';
document.getElementById('vulnerability-severity-filter').value = '';
document.getElementById('vulnerability-status-filter').value = '';
vulnerabilityFilters = {
id: '',
conversation_id: '',
+ task_id: '',
+ conversation_tag: '',
+ task_tag: '',
severity: '',
status: ''
};
@@ -565,6 +596,18 @@ function formatVulnerabilityAsMarkdown(vuln) {
markdown += `- **目标**: ${vuln.target}\n`;
}
markdown += `- **会话ID**: \`${vuln.conversation_id}\`\n`;
+ if (vuln.task_id) {
+ markdown += `- **任务ID**: \`${vuln.task_id}\`\n`;
+ }
+ if (vuln.task_queue_id) {
+ markdown += `- **任务队列ID**: \`${vuln.task_queue_id}\`\n`;
+ }
+ if (vuln.conversation_tag) {
+ markdown += `- **对话标签**: ${vuln.conversation_tag}\n`;
+ }
+ if (vuln.task_tag) {
+ markdown += `- **任务标签**: ${vuln.task_tag}\n`;
+ }
markdown += `- **创建时间**: ${createdDate}\n`;
markdown += `- **更新时间**: ${updatedDate}\n\n`;
@@ -587,6 +630,58 @@ function formatVulnerabilityAsMarkdown(vuln) {
return markdown;
}
+function buildVulnerabilityFilterParams() {
+ const params = new URLSearchParams();
+ const keys = ['id', 'conversation_id', 'task_id', 'conversation_tag', 'task_tag', 'severity', 'status'];
+ keys.forEach((k) => {
+ if (vulnerabilityFilters[k]) {
+ params.append(k, vulnerabilityFilters[k]);
+ }
+ });
+ return params;
+}
+
+function triggerTextDownload(fileName, content) {
+ const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = fileName;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+}
+
+async function exportVulnerabilityReports() {
+ try {
+ const params = buildVulnerabilityFilterParams();
+ params.set('mode', 'summary');
+ params.set('group_by', 'conversation');
+ const response = await apiFetch(`/api/vulnerabilities/export?${params.toString()}`);
+ if (!response.ok) {
+ const error = await response.json().catch(() => ({ error: '导出失败' }));
+ throw new Error(error.error || '导出失败');
+ }
+ const data = await response.json();
+ const files = Array.isArray(data.files) ? data.files : [];
+ if (!files.length) {
+ alert('当前筛选条件下无可导出漏洞');
+ return;
+ }
+ files.forEach((file, idx) => {
+ setTimeout(() => triggerTextDownload(file.filename || `vulnerability-export-${idx + 1}.md`, file.content || ''), idx * 120);
+ });
+ if (files.length > 1) {
+ alert(`已开始下载 ${files.length} 份报告`);
+ }
+ } catch (error) {
+ console.error('导出漏洞报告失败:', error);
+ alert('导出漏洞报告失败: ' + error.message);
+ }
+}
+
+
// 下载漏洞为Markdown格式
async function downloadVulnerabilityAsMarkdown(id, event) {
try {
diff --git a/web/static/js/webshell.js b/web/static/js/webshell.js
index eef8dc13..3d3ba16b 100644
--- a/web/static/js/webshell.js
+++ b/web/static/js/webshell.js
@@ -2881,17 +2881,6 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
} else if (_et === 'warning') {
appendTimelineItem('warning', '⚠️ ' + (_em || ''), '', _ed);
- // ─── Eino recovery ───
- } else if (_et === 'eino_recovery') {
- var runIdx = _ed.runIndex != null ? _ed.runIndex : (_ed.einoRetry != null ? _ed.einoRetry + 1 : 1);
- var maxRuns = _ed.maxRuns != null ? _ed.maxRuns : 3;
- var recTitle = wsTOr('chat.einoRecoveryTitle', '') ||
- ('🔄 工具参数无效 · 第 ' + runIdx + '/' + maxRuns + ' 轮(已追加提示)');
- if (typeof window.t === 'function') {
- try { recTitle = window.t('chat.einoRecoveryTitle', { n: runIdx, max: maxRuns }); } catch (e) { /* */ }
- }
- appendTimelineItem('eino_recovery', recTitle, _em, _ed);
-
// ─── Tool calls ───
} else if (_et === 'tool_calls_detected' && _ed) {
var count = _ed.count || 0;
diff --git a/web/templates/index.html b/web/templates/index.html
index 66f96e7a..cacc3193 100644
--- a/web/templates/index.html
+++ b/web/templates/index.html
@@ -1097,6 +1097,18 @@
会话ID
+
+
+
+
@@ -2599,6 +2612,14 @@
+
+
+
+
+
+
+
+