const progressTaskState = new Map();
let activeTaskInterval = null;
const ACTIVE_TASK_REFRESH_INTERVAL = 10000; // 10秒检查一次
const TASK_FINAL_STATUSES = new Set(['failed', 'timeout', 'cancelled', 'completed']);
// 存储工具调用ID到DOM元素的映射,用于更新执行状态
const toolCallStatusMap = new Map();
const conversationExecutionTracker = {
activeConversations: new Set(),
update(tasks = []) {
this.activeConversations.clear();
tasks.forEach(task => {
if (
task &&
task.conversationId &&
!TASK_FINAL_STATUSES.has(task.status)
) {
this.activeConversations.add(task.conversationId);
}
});
},
isRunning(conversationId) {
return !!conversationId && this.activeConversations.has(conversationId);
}
};
function isConversationTaskRunning(conversationId) {
return conversationExecutionTracker.isRunning(conversationId);
}
function registerProgressTask(progressId, conversationId = null) {
const state = progressTaskState.get(progressId) || {};
state.conversationId = conversationId !== undefined && conversationId !== null
? conversationId
: (state.conversationId ?? currentConversationId);
state.cancelling = false;
progressTaskState.set(progressId, state);
const progressElement = document.getElementById(progressId);
if (progressElement) {
progressElement.dataset.conversationId = state.conversationId || '';
}
}
function updateProgressConversation(progressId, conversationId) {
if (!conversationId) {
return;
}
registerProgressTask(progressId, conversationId);
}
function markProgressCancelling(progressId) {
const state = progressTaskState.get(progressId);
if (state) {
state.cancelling = true;
}
}
function finalizeProgressTask(progressId, finalLabel = '已完成') {
const stopBtn = document.getElementById(`${progressId}-stop-btn`);
if (stopBtn) {
stopBtn.disabled = true;
stopBtn.textContent = finalLabel;
}
progressTaskState.delete(progressId);
}
async function requestCancel(conversationId) {
const response = await apiFetch('/api/agent-loop/cancel', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ conversationId }),
});
const result = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(result.error || '取消失败');
}
return result;
}
function addProgressMessage() {
const messagesDiv = document.getElementById('chat-messages');
const messageDiv = document.createElement('div');
messageCounter++;
const id = 'progress-' + Date.now() + '-' + messageCounter;
messageDiv.id = id;
messageDiv.className = 'message system progress-message';
const contentWrapper = document.createElement('div');
contentWrapper.className = 'message-content';
const bubble = document.createElement('div');
bubble.className = 'message-bubble progress-container';
bubble.innerHTML = `
`;
contentWrapper.appendChild(bubble);
messageDiv.appendChild(contentWrapper);
messageDiv.dataset.conversationId = currentConversationId || '';
messagesDiv.appendChild(messageDiv);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
return id;
}
// 切换进度详情显示
function toggleProgressDetails(progressId) {
const timeline = document.getElementById(progressId + '-timeline');
const toggleBtn = document.querySelector(`#${progressId} .progress-toggle`);
if (!timeline || !toggleBtn) return;
if (timeline.classList.contains('expanded')) {
timeline.classList.remove('expanded');
toggleBtn.textContent = '展开详情';
} else {
timeline.classList.add('expanded');
toggleBtn.textContent = '收起详情';
}
}
// 折叠所有进度详情
function collapseAllProgressDetails(assistantMessageId, progressId) {
// 折叠集成到MCP区域的详情
if (assistantMessageId) {
const detailsId = 'process-details-' + assistantMessageId;
const detailsContainer = document.getElementById(detailsId);
if (detailsContainer) {
const timeline = detailsContainer.querySelector('.progress-timeline');
if (timeline) {
// 确保移除expanded类(无论是否包含)
timeline.classList.remove('expanded');
const btn = document.querySelector(`#${assistantMessageId} .process-detail-btn`);
if (btn) {
btn.innerHTML = '展开详情';
}
}
}
}
// 折叠独立的详情组件(通过convertProgressToDetails创建的)
// 查找所有以details-开头的详情组件
const allDetails = document.querySelectorAll('[id^="details-"]');
allDetails.forEach(detail => {
const timeline = detail.querySelector('.progress-timeline');
const toggleBtn = detail.querySelector('.progress-toggle');
if (timeline) {
timeline.classList.remove('expanded');
if (toggleBtn) {
toggleBtn.textContent = '展开详情';
}
}
});
// 折叠原始的进度消息(如果还存在)
if (progressId) {
const progressTimeline = document.getElementById(progressId + '-timeline');
const progressToggleBtn = document.querySelector(`#${progressId} .progress-toggle`);
if (progressTimeline) {
progressTimeline.classList.remove('expanded');
if (progressToggleBtn) {
progressToggleBtn.textContent = '展开详情';
}
}
}
}
// 获取当前助手消息ID(用于done事件)
function getAssistantId() {
// 从最近的助手消息中获取ID
const messages = document.querySelectorAll('.message.assistant');
if (messages.length > 0) {
return messages[messages.length - 1].id;
}
return null;
}
// 将进度详情集成到工具调用区域
function integrateProgressToMCPSection(progressId, assistantMessageId) {
const progressElement = document.getElementById(progressId);
if (!progressElement) return;
// 获取时间线内容
const timeline = document.getElementById(progressId + '-timeline');
let timelineHTML = '';
if (timeline) {
timelineHTML = timeline.innerHTML;
}
// 获取助手消息元素
const assistantElement = document.getElementById(assistantMessageId);
if (!assistantElement) {
removeMessage(progressId);
return;
}
// 查找MCP调用区域
const mcpSection = assistantElement.querySelector('.mcp-call-section');
if (!mcpSection) {
// 如果没有MCP区域,创建详情组件放在消息下方
convertProgressToDetails(progressId, assistantMessageId);
return;
}
// 获取时间线内容
const hasContent = timelineHTML.trim().length > 0;
// 检查时间线中是否有错误项
const hasError = timeline && timeline.querySelector('.timeline-item-error');
// 确保按钮容器存在
let buttonsContainer = mcpSection.querySelector('.mcp-call-buttons');
if (!buttonsContainer) {
buttonsContainer = document.createElement('div');
buttonsContainer.className = 'mcp-call-buttons';
mcpSection.appendChild(buttonsContainer);
}
// 创建详情容器,放在MCP按钮区域下方(统一结构)
const detailsId = 'process-details-' + assistantMessageId;
let detailsContainer = document.getElementById(detailsId);
if (!detailsContainer) {
detailsContainer = document.createElement('div');
detailsContainer.id = detailsId;
detailsContainer.className = 'process-details-container';
// 确保容器在按钮容器之后
if (buttonsContainer.nextSibling) {
mcpSection.insertBefore(detailsContainer, buttonsContainer.nextSibling);
} else {
mcpSection.appendChild(detailsContainer);
}
}
// 设置详情内容(如果有错误,默认折叠;否则默认折叠)
detailsContainer.innerHTML = `
${hasContent ? `
${timelineHTML}
` : '
暂无过程详情
'}
`;
// 确保初始状态是折叠的(默认折叠,特别是错误时)
if (hasContent) {
const timeline = document.getElementById(detailsId + '-timeline');
if (timeline) {
// 如果有错误,确保折叠;否则也默认折叠
timeline.classList.remove('expanded');
}
// 更新按钮文本为"展开详情"(因为默认折叠)
const processDetailBtn = buttonsContainer.querySelector('.process-detail-btn');
if (processDetailBtn) {
processDetailBtn.innerHTML = '展开详情';
}
}
// 移除原来的进度消息
removeMessage(progressId);
}
// 切换过程详情显示
function toggleProcessDetails(progressId, assistantMessageId) {
const detailsId = 'process-details-' + assistantMessageId;
const detailsContainer = document.getElementById(detailsId);
if (!detailsContainer) return;
const content = detailsContainer.querySelector('.process-details-content');
const timeline = detailsContainer.querySelector('.progress-timeline');
const btn = document.querySelector(`#${assistantMessageId} .process-detail-btn`);
if (content && timeline) {
if (timeline.classList.contains('expanded')) {
timeline.classList.remove('expanded');
if (btn) btn.innerHTML = '展开详情';
} else {
timeline.classList.add('expanded');
if (btn) btn.innerHTML = '收起详情';
}
} else if (timeline) {
// 如果只有timeline,直接切换
if (timeline.classList.contains('expanded')) {
timeline.classList.remove('expanded');
if (btn) btn.innerHTML = '展开详情';
} else {
timeline.classList.add('expanded');
if (btn) btn.innerHTML = '收起详情';
}
}
// 滚动到展开的详情位置,而不是滚动到底部
if (timeline && timeline.classList.contains('expanded')) {
setTimeout(() => {
// 使用 scrollIntoView 滚动到详情容器位置
detailsContainer.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}, 100);
}
}
// 停止当前进度对应的任务
async function cancelProgressTask(progressId) {
const state = progressTaskState.get(progressId);
const stopBtn = document.getElementById(`${progressId}-stop-btn`);
if (!state || !state.conversationId) {
if (stopBtn) {
stopBtn.disabled = true;
setTimeout(() => {
stopBtn.disabled = false;
}, 1500);
}
alert('任务信息尚未同步,请稍后再试。');
return;
}
if (state.cancelling) {
return;
}
markProgressCancelling(progressId);
if (stopBtn) {
stopBtn.disabled = true;
stopBtn.textContent = '取消中...';
}
try {
await requestCancel(state.conversationId);
loadActiveTasks();
} catch (error) {
console.error('取消任务失败:', error);
alert('取消任务失败: ' + error.message);
if (stopBtn) {
stopBtn.disabled = false;
stopBtn.textContent = '停止任务';
}
const currentState = progressTaskState.get(progressId);
if (currentState) {
currentState.cancelling = false;
}
}
}
// 将进度消息转换为可折叠的详情组件
function convertProgressToDetails(progressId, assistantMessageId) {
const progressElement = document.getElementById(progressId);
if (!progressElement) return;
// 获取时间线内容
const timeline = document.getElementById(progressId + '-timeline');
// 即使时间线不存在,也创建详情组件(显示空状态)
let timelineHTML = '';
if (timeline) {
timelineHTML = timeline.innerHTML;
}
// 获取助手消息元素
const assistantElement = document.getElementById(assistantMessageId);
if (!assistantElement) {
removeMessage(progressId);
return;
}
// 创建详情组件
const detailsId = 'details-' + Date.now() + '-' + messageCounter++;
const detailsDiv = document.createElement('div');
detailsDiv.id = detailsId;
detailsDiv.className = 'message system progress-details';
const contentWrapper = document.createElement('div');
contentWrapper.className = 'message-content';
const bubble = document.createElement('div');
bubble.className = 'message-bubble progress-container completed';
// 获取时间线HTML内容
const hasContent = timelineHTML.trim().length > 0;
// 检查时间线中是否有错误项
const hasError = timeline && timeline.querySelector('.timeline-item-error');
// 如果有错误,默认折叠;否则默认展开
const shouldExpand = !hasError;
const expandedClass = shouldExpand ? 'expanded' : '';
const toggleText = shouldExpand ? '收起详情' : '展开详情';
// 总是显示详情组件,即使没有内容也显示
bubble.innerHTML = `
${hasContent ? `${timelineHTML}
` : '暂无过程详情(可能执行过快或未触发详细事件)
'}
`;
contentWrapper.appendChild(bubble);
detailsDiv.appendChild(contentWrapper);
// 将详情组件插入到助手消息之后
const messagesDiv = document.getElementById('chat-messages');
// assistantElement 是消息div,需要插入到它的下一个兄弟节点之前
if (assistantElement.nextSibling) {
messagesDiv.insertBefore(detailsDiv, assistantElement.nextSibling);
} else {
// 如果没有下一个兄弟节点,直接追加
messagesDiv.appendChild(detailsDiv);
}
// 移除原来的进度消息
removeMessage(progressId);
// 滚动到底部
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
// 处理流式事件
function handleStreamEvent(event, progressElement, progressId,
getAssistantId, setAssistantId, getMcpIds, setMcpIds) {
const timeline = document.getElementById(progressId + '-timeline');
if (!timeline) return;
switch (event.type) {
case 'conversation':
if (event.data && event.data.conversationId) {
// 在更新之前,先获取任务对应的原始对话ID
const taskState = progressTaskState.get(progressId);
const originalConversationId = taskState?.conversationId;
// 更新任务状态
updateProgressConversation(progressId, event.data.conversationId);
// 如果用户已经开始了新对话(currentConversationId 为 null),
// 且这个 conversation 事件来自旧对话,就不更新 currentConversationId
if (currentConversationId === null && originalConversationId !== null) {
// 用户已经开始了新对话,忽略旧对话的 conversation 事件
// 但仍然更新任务状态,以便正确显示任务信息
break;
}
// 更新当前对话ID
currentConversationId = event.data.conversationId;
updateActiveConversation();
addAttackChainButton(currentConversationId);
loadActiveTasks();
// 延迟刷新对话列表,确保用户消息已保存,updated_at已更新
// 这样新对话才能正确显示在最近对话列表的顶部
// 使用loadConversationsWithGroups确保分组映射缓存正确加载,无论是否有分组都能立即显示
setTimeout(() => {
if (typeof loadConversationsWithGroups === 'function') {
loadConversationsWithGroups();
} else if (typeof loadConversations === 'function') {
loadConversations();
}
}, 200);
}
break;
case 'iteration':
// 添加迭代标记
addTimelineItem(timeline, 'iteration', {
title: `第 ${event.data?.iteration || 1} 轮迭代`,
message: event.message,
data: event.data
});
break;
case 'thinking':
// 显示AI思考内容
addTimelineItem(timeline, 'thinking', {
title: '🤔 AI思考',
message: event.message,
data: event.data
});
break;
case 'tool_calls_detected':
// 工具调用检测
addTimelineItem(timeline, 'tool_calls_detected', {
title: `🔧 检测到 ${event.data?.count || 0} 个工具调用`,
message: event.message,
data: event.data
});
break;
case 'tool_call':
// 显示工具调用信息
const toolInfo = event.data || {};
const toolName = toolInfo.toolName || '未知工具';
const index = toolInfo.index || 0;
const total = toolInfo.total || 0;
const toolCallId = toolInfo.toolCallId || null;
// 添加工具调用项,并标记为执行中
const toolCallItemId = addTimelineItem(timeline, 'tool_call', {
title: `🔧 调用工具: ${escapeHtml(toolName)} (${index}/${total})`,
message: event.message,
data: toolInfo,
expanded: false
});
// 如果有toolCallId,存储映射关系以便后续更新状态
if (toolCallId && toolCallItemId) {
toolCallStatusMap.set(toolCallId, {
itemId: toolCallItemId,
timeline: timeline
});
// 添加执行中状态指示器
updateToolCallStatus(toolCallId, 'running');
}
break;
case 'tool_result':
// 显示工具执行结果
const resultInfo = event.data || {};
const resultToolName = resultInfo.toolName || '未知工具';
const success = resultInfo.success !== false;
const statusIcon = success ? '✅' : '❌';
const resultToolCallId = resultInfo.toolCallId || null;
// 如果有关联的toolCallId,更新工具调用项的状态
if (resultToolCallId && toolCallStatusMap.has(resultToolCallId)) {
updateToolCallStatus(resultToolCallId, success ? 'completed' : 'failed');
// 从映射中移除(已完成)
toolCallStatusMap.delete(resultToolCallId);
}
addTimelineItem(timeline, 'tool_result', {
title: `${statusIcon} 工具 ${escapeHtml(resultToolName)} 执行${success ? '完成' : '失败'}`,
message: event.message,
data: resultInfo,
expanded: false
});
break;
case 'progress':
// 更新进度状态
const progressTitle = document.querySelector(`#${progressId} .progress-title`);
if (progressTitle) {
progressTitle.textContent = '🔍 ' + event.message;
}
break;
case 'cancelled':
// 显示错误
addTimelineItem(timeline, 'cancelled', {
title: '⛔ 任务已取消',
message: event.message,
data: event.data
});
// 更新进度标题为取消状态
const cancelTitle = document.querySelector(`#${progressId} .progress-title`);
if (cancelTitle) {
cancelTitle.textContent = '⛔ 任务已取消';
}
// 更新进度容器为已完成状态(添加completed类)
const cancelProgressContainer = document.querySelector(`#${progressId} .progress-container`);
if (cancelProgressContainer) {
cancelProgressContainer.classList.add('completed');
}
// 完成进度任务(标记为已取消)
if (progressTaskState.has(progressId)) {
finalizeProgressTask(progressId, '已取消');
}
// 如果取消事件包含messageId,说明有助手消息,需要显示取消内容
if (event.data && event.data.messageId) {
// 检查助手消息是否已存在
let assistantId = event.data.messageId;
let assistantElement = document.getElementById(assistantId);
// 如果助手消息不存在,创建它
if (!assistantElement) {
assistantId = addMessage('assistant', event.message, null, progressId);
setAssistantId(assistantId);
assistantElement = document.getElementById(assistantId);
} else {
// 如果已存在,更新内容
const bubble = assistantElement.querySelector('.message-bubble');
if (bubble) {
bubble.innerHTML = escapeHtml(event.message).replace(/\n/g, '
');
}
}
// 将进度详情集成到工具调用区域(如果还没有)
if (assistantElement) {
const detailsId = 'process-details-' + assistantId;
if (!document.getElementById(detailsId)) {
integrateProgressToMCPSection(progressId, assistantId);
}
// 立即折叠详情(取消时应该默认折叠)
setTimeout(() => {
collapseAllProgressDetails(assistantId, progressId);
}, 100);
}
} else {
// 如果没有messageId,创建助手消息并集成详情
const assistantId = addMessage('assistant', event.message, null, progressId);
setAssistantId(assistantId);
// 将进度详情集成到工具调用区域
setTimeout(() => {
integrateProgressToMCPSection(progressId, assistantId);
// 确保详情默认折叠
collapseAllProgressDetails(assistantId, progressId);
}, 100);
}
// 立即刷新任务状态
loadActiveTasks();
break;
case 'response':
// 在更新之前,先获取任务对应的原始对话ID
const responseTaskState = progressTaskState.get(progressId);
const responseOriginalConversationId = responseTaskState?.conversationId;
// 先添加助手回复
const responseData = event.data || {};
const mcpIds = responseData.mcpExecutionIds || [];
setMcpIds(mcpIds);
// 更新对话ID
if (responseData.conversationId) {
// 如果用户已经开始了新对话(currentConversationId 为 null),
// 且这个 response 事件来自旧对话,就不更新 currentConversationId 也不添加消息
if (currentConversationId === null && responseOriginalConversationId !== null) {
// 用户已经开始了新对话,忽略旧对话的 response 事件
// 但仍然更新任务状态,以便正确显示任务信息
updateProgressConversation(progressId, responseData.conversationId);
break;
}
currentConversationId = responseData.conversationId;
updateActiveConversation();
addAttackChainButton(currentConversationId);
updateProgressConversation(progressId, responseData.conversationId);
loadActiveTasks();
}
// 添加助手回复,并传入进度ID以便集成详情
const assistantId = addMessage('assistant', event.message, mcpIds, progressId);
setAssistantId(assistantId);
// 将进度详情集成到工具调用区域
integrateProgressToMCPSection(progressId, assistantId);
// 延迟自动折叠详情(3秒后)
setTimeout(() => {
collapseAllProgressDetails(assistantId, progressId);
}, 3000);
// 延迟刷新对话列表,确保助手消息已保存,updated_at已更新
setTimeout(() => {
loadConversations();
}, 200);
break;
case 'error':
// 显示错误
addTimelineItem(timeline, 'error', {
title: '❌ 错误',
message: event.message,
data: event.data
});
// 更新进度标题为错误状态
const errorTitle = document.querySelector(`#${progressId} .progress-title`);
if (errorTitle) {
errorTitle.textContent = '❌ 执行失败';
}
// 更新进度容器为已完成状态(添加completed类)
const progressContainer = document.querySelector(`#${progressId} .progress-container`);
if (progressContainer) {
progressContainer.classList.add('completed');
}
// 完成进度任务(标记为失败)
if (progressTaskState.has(progressId)) {
finalizeProgressTask(progressId, '已失败');
}
// 如果错误事件包含messageId,说明有助手消息,需要显示错误内容
if (event.data && event.data.messageId) {
// 检查助手消息是否已存在
let assistantId = event.data.messageId;
let assistantElement = document.getElementById(assistantId);
// 如果助手消息不存在,创建它
if (!assistantElement) {
assistantId = addMessage('assistant', event.message, null, progressId);
setAssistantId(assistantId);
assistantElement = document.getElementById(assistantId);
} else {
// 如果已存在,更新内容
const bubble = assistantElement.querySelector('.message-bubble');
if (bubble) {
bubble.innerHTML = escapeHtml(event.message).replace(/\n/g, '
');
}
}
// 将进度详情集成到工具调用区域(如果还没有)
if (assistantElement) {
const detailsId = 'process-details-' + assistantId;
if (!document.getElementById(detailsId)) {
integrateProgressToMCPSection(progressId, assistantId);
}
// 立即折叠详情(错误时应该默认折叠)
setTimeout(() => {
collapseAllProgressDetails(assistantId, progressId);
}, 100);
}
} else {
// 如果没有messageId(比如任务已运行时的错误),创建助手消息并集成详情
const assistantId = addMessage('assistant', event.message, null, progressId);
setAssistantId(assistantId);
// 将进度详情集成到工具调用区域
setTimeout(() => {
integrateProgressToMCPSection(progressId, assistantId);
// 确保详情默认折叠
collapseAllProgressDetails(assistantId, progressId);
}, 100);
}
// 立即刷新任务状态(执行失败时任务状态会更新)
loadActiveTasks();
break;
case 'done':
// 完成,更新进度标题(如果进度消息还存在)
const doneTitle = document.querySelector(`#${progressId} .progress-title`);
if (doneTitle) {
doneTitle.textContent = '✅ 渗透测试完成';
}
// 更新对话ID
if (event.data && event.data.conversationId) {
currentConversationId = event.data.conversationId;
updateActiveConversation();
addAttackChainButton(currentConversationId);
updateProgressConversation(progressId, event.data.conversationId);
}
if (progressTaskState.has(progressId)) {
finalizeProgressTask(progressId, '已完成');
}
// 检查时间线中是否有错误项
const hasError = timeline && timeline.querySelector('.timeline-item-error');
// 立即刷新任务状态(确保任务状态同步)
loadActiveTasks();
// 延迟再次刷新任务状态(确保后端已完成状态更新)
setTimeout(() => {
loadActiveTasks();
}, 200);
// 完成时自动折叠所有详情(延迟一下确保response事件已处理)
setTimeout(() => {
const assistantIdFromDone = getAssistantId();
if (assistantIdFromDone) {
collapseAllProgressDetails(assistantIdFromDone, progressId);
} else {
// 如果无法获取助手ID,尝试折叠所有详情
collapseAllProgressDetails(null, progressId);
}
// 如果有错误,确保详情是折叠的(错误时应该默认折叠)
if (hasError) {
// 再次确保折叠(延迟一点确保DOM已更新)
setTimeout(() => {
collapseAllProgressDetails(assistantIdFromDone || null, progressId);
}, 200);
}
}, 500);
break;
}
// 自动滚动到底部
const messagesDiv = document.getElementById('chat-messages');
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
// 更新工具调用状态
function updateToolCallStatus(toolCallId, status) {
const mapping = toolCallStatusMap.get(toolCallId);
if (!mapping) return;
const item = document.getElementById(mapping.itemId);
if (!item) return;
const titleElement = item.querySelector('.timeline-item-title');
if (!titleElement) return;
// 移除之前的状态类
item.classList.remove('tool-call-running', 'tool-call-completed', 'tool-call-failed');
// 根据状态更新样式和文本
let statusText = '';
if (status === 'running') {
item.classList.add('tool-call-running');
statusText = ' 执行中...';
} else if (status === 'completed') {
item.classList.add('tool-call-completed');
statusText = ' ✅ 已完成';
} else if (status === 'failed') {
item.classList.add('tool-call-failed');
statusText = ' ❌ 执行失败';
}
// 更新标题(保留原有文本,追加状态)
const originalText = titleElement.innerHTML;
// 移除之前可能存在的状态标记
const cleanText = originalText.replace(/\s*
${time}
${escapeHtml(options.title || '')}
`;
// 根据类型添加详细内容
if (type === 'thinking' && options.message) {
content += `${formatMarkdown(options.message)}
`;
} else if (type === 'tool_call' && options.data) {
const data = options.data;
const args = data.argumentsObj || (data.arguments ? JSON.parse(data.arguments) : {});
content += `
`;
} else if (type === 'tool_result' && options.data) {
const data = options.data;
const isError = data.isError || !data.success;
const result = data.result || data.error || '无结果';
// 确保 result 是字符串
const resultStr = typeof result === 'string' ? result : JSON.stringify(result);
content += `
`;
} else if (type === 'cancelled') {
content += `
${escapeHtml(options.message || '任务已取消')}
`;
}
item.innerHTML = content;
timeline.appendChild(item);
// 自动展开详情
const expanded = timeline.classList.contains('expanded');
if (!expanded && (type === 'tool_call' || type === 'tool_result')) {
// 对于工具调用和结果,默认显示摘要
}
// 返回item ID以便后续更新
return itemId;
}
// 加载活跃任务列表
async function loadActiveTasks(showErrors = false) {
const bar = document.getElementById('active-tasks-bar');
try {
const response = await apiFetch('/api/agent-loop/tasks');
const result = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(result.error || '获取活跃任务失败');
}
renderActiveTasks(result.tasks || []);
} catch (error) {
console.error('获取活跃任务失败:', error);
if (showErrors && bar) {
bar.style.display = 'block';
bar.innerHTML = `无法获取任务状态:${escapeHtml(error.message)}
`;
}
}
}
function renderActiveTasks(tasks) {
const bar = document.getElementById('active-tasks-bar');
if (!bar) return;
const normalizedTasks = Array.isArray(tasks) ? tasks : [];
conversationExecutionTracker.update(normalizedTasks);
if (typeof updateAttackChainAvailability === 'function') {
updateAttackChainAvailability();
}
if (normalizedTasks.length === 0) {
bar.style.display = 'none';
bar.innerHTML = '';
return;
}
bar.style.display = 'flex';
bar.innerHTML = '';
normalizedTasks.forEach(task => {
const item = document.createElement('div');
item.className = 'active-task-item';
const startedTime = task.startedAt ? new Date(task.startedAt) : null;
const timeText = startedTime && !isNaN(startedTime.getTime())
? startedTime.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
: '';
// 根据任务状态显示不同的文本
const statusMap = {
'running': '执行中',
'cancelling': '取消中',
'failed': '执行失败',
'timeout': '执行超时',
'cancelled': '已取消',
'completed': '已完成'
};
const statusText = statusMap[task.status] || '执行中';
const isFinalStatus = ['failed', 'timeout', 'cancelled', 'completed'].includes(task.status);
item.innerHTML = `
${statusText}
${escapeHtml(task.message || '未命名任务')}
${timeText ? `${timeText}` : ''}
${!isFinalStatus ? '' : ''}
`;
// 只有非最终状态的任务才显示停止按钮
if (!isFinalStatus) {
const cancelBtn = item.querySelector('.active-task-cancel');
if (cancelBtn) {
cancelBtn.onclick = () => cancelActiveTask(task.conversationId, cancelBtn);
if (task.status === 'cancelling') {
cancelBtn.disabled = true;
cancelBtn.textContent = '取消中...';
}
}
}
bar.appendChild(item);
});
}
async function cancelActiveTask(conversationId, button) {
if (!conversationId) return;
const originalText = button.textContent;
button.disabled = true;
button.textContent = '取消中...';
try {
await requestCancel(conversationId);
loadActiveTasks();
} catch (error) {
console.error('取消任务失败:', error);
alert('取消任务失败: ' + error.message);
button.disabled = false;
button.textContent = originalText;
}
}
// 监控面板状态
const monitorState = {
executions: [],
stats: {},
lastFetchedAt: null,
pagination: {
page: 1,
pageSize: (() => {
// 从 localStorage 读取保存的每页显示数量,默认为 20
const saved = localStorage.getItem('monitorPageSize');
return saved ? parseInt(saved, 10) : 20;
})(),
total: 0,
totalPages: 0
}
};
function openMonitorPanel() {
// 切换到MCP监控页面
if (typeof switchPage === 'function') {
switchPage('mcp-monitor');
}
// 初始化每页显示数量选择器
initializeMonitorPageSize();
}
// 初始化每页显示数量选择器
function initializeMonitorPageSize() {
const pageSizeSelect = document.getElementById('monitor-page-size');
if (pageSizeSelect) {
pageSizeSelect.value = monitorState.pagination.pageSize;
}
}
// 改变每页显示数量
function changeMonitorPageSize() {
const pageSizeSelect = document.getElementById('monitor-page-size');
if (!pageSizeSelect) {
return;
}
const newPageSize = parseInt(pageSizeSelect.value, 10);
if (isNaN(newPageSize) || newPageSize <= 0) {
return;
}
// 保存到 localStorage
localStorage.setItem('monitorPageSize', newPageSize.toString());
// 更新状态
monitorState.pagination.pageSize = newPageSize;
monitorState.pagination.page = 1; // 重置到第一页
// 刷新数据
refreshMonitorPanel(1);
}
function closeMonitorPanel() {
// 不再需要关闭功能,因为现在是页面而不是模态框
// 如果需要,可以切换回对话页面
if (typeof switchPage === 'function') {
switchPage('chat');
}
}
async function refreshMonitorPanel(page = null) {
const statsContainer = document.getElementById('monitor-stats');
const execContainer = document.getElementById('monitor-executions');
try {
// 如果指定了页码,使用指定页码,否则使用当前页码
const currentPage = page !== null ? page : monitorState.pagination.page;
const pageSize = monitorState.pagination.pageSize;
// 获取当前的筛选条件
const statusFilter = document.getElementById('monitor-status-filter');
const toolFilter = document.getElementById('monitor-tool-filter');
const currentStatusFilter = statusFilter ? statusFilter.value : 'all';
const currentToolFilter = toolFilter ? (toolFilter.value.trim() || 'all') : 'all';
// 构建请求 URL
let url = `/api/monitor?page=${currentPage}&page_size=${pageSize}`;
if (currentStatusFilter && currentStatusFilter !== 'all') {
url += `&status=${encodeURIComponent(currentStatusFilter)}`;
}
if (currentToolFilter && currentToolFilter !== 'all') {
url += `&tool=${encodeURIComponent(currentToolFilter)}`;
}
const response = await apiFetch(url, { method: 'GET' });
const result = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(result.error || '获取监控数据失败');
}
monitorState.executions = Array.isArray(result.executions) ? result.executions : [];
monitorState.stats = result.stats || {};
monitorState.lastFetchedAt = new Date();
// 更新分页信息
if (result.total !== undefined) {
monitorState.pagination = {
page: result.page || currentPage,
pageSize: result.page_size || pageSize,
total: result.total || 0,
totalPages: result.total_pages || 1
};
}
renderMonitorStats(monitorState.stats, monitorState.lastFetchedAt);
renderMonitorExecutions(monitorState.executions, currentStatusFilter);
renderMonitorPagination();
// 初始化每页显示数量选择器
initializeMonitorPageSize();
} catch (error) {
console.error('刷新监控面板失败:', error);
if (statsContainer) {
statsContainer.innerHTML = `${escapeHtml(typeof window.t === 'function' ? window.t('mcpMonitor.loadStatsError') : '无法加载统计信息')}:${escapeHtml(error.message)}
`;
}
if (execContainer) {
execContainer.innerHTML = `${escapeHtml(typeof window.t === 'function' ? window.t('mcpMonitor.loadExecutionsError') : '无法加载执行记录')}:${escapeHtml(error.message)}
`;
}
}
}
// 处理工具搜索输入(防抖)
let toolFilterDebounceTimer = null;
function handleToolFilterInput() {
// 清除之前的定时器
if (toolFilterDebounceTimer) {
clearTimeout(toolFilterDebounceTimer);
}
// 设置新的定时器,500ms后执行筛选
toolFilterDebounceTimer = setTimeout(() => {
applyMonitorFilters();
}, 500);
}
async function applyMonitorFilters() {
const statusFilter = document.getElementById('monitor-status-filter');
const toolFilter = document.getElementById('monitor-tool-filter');
const status = statusFilter ? statusFilter.value : 'all';
const tool = toolFilter ? (toolFilter.value.trim() || 'all') : 'all';
// 当筛选条件改变时,从后端重新获取数据
await refreshMonitorPanelWithFilter(status, tool);
}
async function refreshMonitorPanelWithFilter(statusFilter = 'all', toolFilter = 'all') {
const statsContainer = document.getElementById('monitor-stats');
const execContainer = document.getElementById('monitor-executions');
try {
const currentPage = 1; // 筛选时重置到第一页
const pageSize = monitorState.pagination.pageSize;
// 构建请求 URL
let url = `/api/monitor?page=${currentPage}&page_size=${pageSize}`;
if (statusFilter && statusFilter !== 'all') {
url += `&status=${encodeURIComponent(statusFilter)}`;
}
if (toolFilter && toolFilter !== 'all') {
url += `&tool=${encodeURIComponent(toolFilter)}`;
}
const response = await apiFetch(url, { method: 'GET' });
const result = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(result.error || '获取监控数据失败');
}
monitorState.executions = Array.isArray(result.executions) ? result.executions : [];
monitorState.stats = result.stats || {};
monitorState.lastFetchedAt = new Date();
// 更新分页信息
if (result.total !== undefined) {
monitorState.pagination = {
page: result.page || currentPage,
pageSize: result.page_size || pageSize,
total: result.total || 0,
totalPages: result.total_pages || 1
};
}
renderMonitorStats(monitorState.stats, monitorState.lastFetchedAt);
renderMonitorExecutions(monitorState.executions, statusFilter);
renderMonitorPagination();
// 初始化每页显示数量选择器
initializeMonitorPageSize();
} catch (error) {
console.error('刷新监控面板失败:', error);
if (statsContainer) {
statsContainer.innerHTML = `${escapeHtml(typeof window.t === 'function' ? window.t('mcpMonitor.loadStatsError') : '无法加载统计信息')}:${escapeHtml(error.message)}
`;
}
if (execContainer) {
execContainer.innerHTML = `${escapeHtml(typeof window.t === 'function' ? window.t('mcpMonitor.loadExecutionsError') : '无法加载执行记录')}:${escapeHtml(error.message)}
`;
}
}
}
function renderMonitorStats(statsMap = {}, lastFetchedAt = null) {
const container = document.getElementById('monitor-stats');
if (!container) {
return;
}
const entries = Object.values(statsMap);
if (entries.length === 0) {
const noStats = typeof window.t === 'function' ? window.t('mcpMonitor.noStatsData') : '暂无统计数据';
container.innerHTML = '' + escapeHtml(noStats) + '
';
return;
}
// 计算总体汇总
const totals = entries.reduce(
(acc, item) => {
acc.total += item.totalCalls || 0;
acc.success += item.successCalls || 0;
acc.failed += item.failedCalls || 0;
const lastCall = item.lastCallTime ? new Date(item.lastCallTime) : null;
if (lastCall && (!acc.lastCallTime || lastCall > acc.lastCallTime)) {
acc.lastCallTime = lastCall;
}
return acc;
},
{ total: 0, success: 0, failed: 0, lastCallTime: null }
);
const successRate = totals.total > 0 ? ((totals.success / totals.total) * 100).toFixed(1) : '0.0';
const locale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : undefined;
const lastUpdatedText = lastFetchedAt ? (lastFetchedAt.toLocaleString ? lastFetchedAt.toLocaleString(locale || 'en-US') : String(lastFetchedAt)) : 'N/A';
const noCallsYet = typeof window.t === 'function' ? window.t('mcpMonitor.noCallsYet') : '暂无调用';
const lastCallText = totals.lastCallTime ? (totals.lastCallTime.toLocaleString ? totals.lastCallTime.toLocaleString(locale || 'en-US') : String(totals.lastCallTime)) : noCallsYet;
const totalCallsLabel = typeof window.t === 'function' ? window.t('mcpMonitor.totalCalls') : '总调用次数';
const successFailedLabel = typeof window.t === 'function' ? window.t('mcpMonitor.successFailed', { success: totals.success, failed: totals.failed }) : `成功 ${totals.success} / 失败 ${totals.failed}`;
const successRateLabel = typeof window.t === 'function' ? window.t('mcpMonitor.successRate') : '成功率';
const statsFromAll = typeof window.t === 'function' ? window.t('mcpMonitor.statsFromAllTools') : '统计自全部工具调用';
const lastCallLabel = typeof window.t === 'function' ? window.t('mcpMonitor.lastCall') : '最近一次调用';
const lastRefreshLabel = typeof window.t === 'function' ? window.t('mcpMonitor.lastRefreshTime') : '最后刷新时间';
let html = `
${escapeHtml(totalCallsLabel)}
${totals.total}
${escapeHtml(successFailedLabel)}
${escapeHtml(successRateLabel)}
${successRate}%
${escapeHtml(statsFromAll)}
${escapeHtml(lastCallLabel)}
${escapeHtml(lastCallText)}
${escapeHtml(lastRefreshLabel)}:${escapeHtml(lastUpdatedText)}
`;
// 显示最多前4个工具的统计(过滤掉 totalCalls 为 0 的工具)
const topTools = entries
.filter(tool => (tool.totalCalls || 0) > 0)
.slice()
.sort((a, b) => (b.totalCalls || 0) - (a.totalCalls || 0))
.slice(0, 4);
const unknownToolLabel = typeof window.t === 'function' ? window.t('mcpMonitor.unknownTool') : '未知工具';
topTools.forEach(tool => {
const toolSuccessRate = tool.totalCalls > 0 ? ((tool.successCalls || 0) / tool.totalCalls * 100).toFixed(1) : '0.0';
const toolMeta = typeof window.t === 'function' ? window.t('mcpMonitor.successFailedRate', { success: tool.successCalls || 0, failed: tool.failedCalls || 0, rate: toolSuccessRate }) : `成功 ${tool.successCalls || 0} / 失败 ${tool.failedCalls || 0} · 成功率 ${toolSuccessRate}%`;
html += `
${escapeHtml(tool.toolName || unknownToolLabel)}
${tool.totalCalls || 0}
${escapeHtml(toolMeta)}
`;
});
container.innerHTML = `${html}
`;
}
function renderMonitorExecutions(executions = [], statusFilter = 'all') {
const container = document.getElementById('monitor-executions');
if (!container) {
return;
}
if (!Array.isArray(executions) || executions.length === 0) {
// 根据是否有筛选条件显示不同的提示
const toolFilter = document.getElementById('monitor-tool-filter');
const currentToolFilter = toolFilter ? toolFilter.value : 'all';
const hasFilter = (statusFilter && statusFilter !== 'all') || (currentToolFilter && currentToolFilter !== 'all');
const noRecordsFilter = typeof window.t === 'function' ? window.t('mcpMonitor.noRecordsWithFilter') : '当前筛选条件下暂无记录';
const noExecutions = typeof window.t === 'function' ? window.t('mcpMonitor.noExecutions') : '暂无执行记录';
if (hasFilter) {
container.innerHTML = '' + escapeHtml(noRecordsFilter) + '
';
} else {
container.innerHTML = '' + escapeHtml(noExecutions) + '
';
}
// 隐藏批量操作栏
const batchActions = document.getElementById('monitor-batch-actions');
if (batchActions) {
batchActions.style.display = 'none';
}
return;
}
// 由于筛选已经在后端完成,这里直接使用所有传入的执行记录
// 不再需要前端再次筛选,因为后端已经返回了筛选后的数据
const unknownLabel = typeof window.t === 'function' ? window.t('mcpMonitor.unknown') : '未知';
const unknownToolLabel = typeof window.t === 'function' ? window.t('mcpMonitor.unknownTool') : '未知工具';
const viewDetailLabel = typeof window.t === 'function' ? window.t('mcpMonitor.viewDetail') : '查看详情';
const deleteLabel = typeof window.t === 'function' ? window.t('mcpMonitor.delete') : '删除';
const deleteExecTitle = typeof window.t === 'function' ? window.t('mcpMonitor.deleteExecTitle') : '删除此执行记录';
const statusKeyMap = { pending: 'statusPending', running: 'statusRunning', completed: 'statusCompleted', failed: 'statusFailed' };
const locale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : undefined;
const rows = executions
.map(exec => {
const status = (exec.status || 'unknown').toLowerCase();
const statusClass = `monitor-status-chip ${status}`;
const statusKey = statusKeyMap[status];
const statusLabel = (typeof window.t === 'function' && statusKey) ? window.t('mcpMonitor.' + statusKey) : getStatusText(status);
const startTime = exec.startTime ? (new Date(exec.startTime).toLocaleString ? new Date(exec.startTime).toLocaleString(locale || 'en-US') : String(exec.startTime)) : unknownLabel;
const duration = formatExecutionDuration(exec.startTime, exec.endTime);
const toolName = escapeHtml(exec.toolName || unknownToolLabel);
const executionId = escapeHtml(exec.id || '');
return `
|
|
${toolName} |
${escapeHtml(statusLabel)} |
${escapeHtml(startTime)} |
${escapeHtml(duration)} |
|
`;
})
.join('');
// 先移除旧的表格容器和加载提示(保留分页控件)
const oldTableContainer = container.querySelector('.monitor-table-container');
if (oldTableContainer) {
oldTableContainer.remove();
}
// 清除"加载中..."等提示信息
const oldEmpty = container.querySelector('.monitor-empty');
if (oldEmpty) {
oldEmpty.remove();
}
// 创建表格容器
const tableContainer = document.createElement('div');
tableContainer.className = 'monitor-table-container';
const colTool = typeof window.t === 'function' ? window.t('mcpMonitor.columnTool') : '工具';
const colStatus = typeof window.t === 'function' ? window.t('mcpMonitor.columnStatus') : '状态';
const colStartTime = typeof window.t === 'function' ? window.t('mcpMonitor.columnStartTime') : '开始时间';
const colDuration = typeof window.t === 'function' ? window.t('mcpMonitor.columnDuration') : '耗时';
const colActions = typeof window.t === 'function' ? window.t('mcpMonitor.columnActions') : '操作';
tableContainer.innerHTML = `
`;
// 在分页控件之前插入表格(如果存在分页控件)
const existingPagination = container.querySelector('.monitor-pagination');
if (existingPagination) {
container.insertBefore(tableContainer, existingPagination);
} else {
container.appendChild(tableContainer);
}
// 更新批量操作状态
updateBatchActionsState();
}
// 渲染监控面板分页控件
function renderMonitorPagination() {
const container = document.getElementById('monitor-executions');
if (!container) return;
// 移除旧的分页控件
const oldPagination = container.querySelector('.monitor-pagination');
if (oldPagination) {
oldPagination.remove();
}
const { page, totalPages, total, pageSize } = monitorState.pagination;
// 始终显示分页控件
const pagination = document.createElement('div');
pagination.className = 'monitor-pagination';
// 处理没有数据的情况
const startItem = total === 0 ? 0 : (page - 1) * pageSize + 1;
const endItem = total === 0 ? 0 : Math.min(page * pageSize, total);
const paginationInfoText = typeof window.t === 'function' ? window.t('mcpMonitor.paginationInfo', { start: startItem, end: endItem, total: total }) : `显示 ${startItem}-${endItem} / 共 ${total} 条记录`;
const perPageLabel = typeof window.t === 'function' ? window.t('mcpMonitor.perPageLabel') : '每页显示';
const firstPageLabel = typeof window.t === 'function' ? window.t('mcp.firstPage') : '首页';
const prevPageLabel = typeof window.t === 'function' ? window.t('mcp.prevPage') : '上一页';
const pageInfoText = typeof window.t === 'function' ? window.t('mcp.pageInfo', { page: page, total: totalPages || 1 }) : `第 ${page} / ${totalPages || 1} 页`;
const nextPageLabel = typeof window.t === 'function' ? window.t('mcp.nextPage') : '下一页';
const lastPageLabel = typeof window.t === 'function' ? window.t('mcp.lastPage') : '末页';
pagination.innerHTML = `
`;
container.appendChild(pagination);
// 初始化每页显示数量选择器
initializeMonitorPageSize();
}
// 删除执行记录
async function deleteExecution(executionId) {
if (!executionId) {
return;
}
const deleteConfirmMsg = typeof window.t === 'function' ? window.t('mcpMonitor.deleteExecConfirmSingle') : '确定要删除此执行记录吗?此操作不可恢复。';
if (!confirm(deleteConfirmMsg)) {
return;
}
try {
const response = await apiFetch(`/api/monitor/execution/${executionId}`, {
method: 'DELETE'
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
const deleteFailedMsg = typeof window.t === 'function' ? window.t('mcpMonitor.deleteExecFailed') : '删除执行记录失败';
throw new Error(error.error || deleteFailedMsg);
}
// 删除成功后刷新当前页面
const currentPage = monitorState.pagination.page;
await refreshMonitorPanel(currentPage);
const execDeletedMsg = typeof window.t === 'function' ? window.t('mcpMonitor.execDeleted') : '执行记录已删除';
alert(execDeletedMsg);
} catch (error) {
console.error('删除执行记录失败:', error);
const deleteFailedMsg = typeof window.t === 'function' ? window.t('mcpMonitor.deleteExecFailed') : '删除执行记录失败';
alert(deleteFailedMsg + ': ' + error.message);
}
}
// 更新批量操作状态
function updateBatchActionsState() {
const checkboxes = document.querySelectorAll('.monitor-execution-checkbox:checked');
const selectedCount = checkboxes.length;
const batchActions = document.getElementById('monitor-batch-actions');
const selectedCountSpan = document.getElementById('monitor-selected-count');
if (selectedCount > 0) {
if (batchActions) {
batchActions.style.display = 'flex';
}
if (selectedCountSpan) {
selectedCountSpan.textContent = typeof window.t === 'function' ? window.t('mcp.selectedCount', { count: selectedCount }) : `已选择 ${selectedCount} 项`;
}
} else {
if (batchActions) {
batchActions.style.display = 'none';
}
}
// 更新全选复选框状态
const selectAllCheckbox = document.getElementById('monitor-select-all');
if (selectAllCheckbox) {
const allCheckboxes = document.querySelectorAll('.monitor-execution-checkbox');
const allChecked = allCheckboxes.length > 0 && Array.from(allCheckboxes).every(cb => cb.checked);
selectAllCheckbox.checked = allChecked;
selectAllCheckbox.indeterminate = selectedCount > 0 && selectedCount < allCheckboxes.length;
}
}
// 切换全选
function toggleSelectAll(checkbox) {
const checkboxes = document.querySelectorAll('.monitor-execution-checkbox');
checkboxes.forEach(cb => {
cb.checked = checkbox.checked;
});
updateBatchActionsState();
}
// 全选
function selectAllExecutions() {
const checkboxes = document.querySelectorAll('.monitor-execution-checkbox');
checkboxes.forEach(cb => {
cb.checked = true;
});
const selectAllCheckbox = document.getElementById('monitor-select-all');
if (selectAllCheckbox) {
selectAllCheckbox.checked = true;
selectAllCheckbox.indeterminate = false;
}
updateBatchActionsState();
}
// 取消全选
function deselectAllExecutions() {
const checkboxes = document.querySelectorAll('.monitor-execution-checkbox');
checkboxes.forEach(cb => {
cb.checked = false;
});
const selectAllCheckbox = document.getElementById('monitor-select-all');
if (selectAllCheckbox) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
}
updateBatchActionsState();
}
// 批量删除执行记录
async function batchDeleteExecutions() {
const checkboxes = document.querySelectorAll('.monitor-execution-checkbox:checked');
if (checkboxes.length === 0) {
const selectFirstMsg = typeof window.t === 'function' ? window.t('mcpMonitor.selectExecFirst') : '请先选择要删除的执行记录';
alert(selectFirstMsg);
return;
}
const ids = Array.from(checkboxes).map(cb => cb.value);
const count = ids.length;
const batchConfirmMsg = typeof window.t === 'function' ? window.t('mcpMonitor.batchDeleteConfirm', { count: count }) : `确定要删除选中的 ${count} 条执行记录吗?此操作不可恢复。`;
if (!confirm(batchConfirmMsg)) {
return;
}
try {
const response = await apiFetch('/api/monitor/executions', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ ids: ids })
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
const batchFailedMsg = typeof window.t === 'function' ? window.t('mcp.batchDeleteFailed') : '批量删除执行记录失败';
throw new Error(error.error || batchFailedMsg);
}
const result = await response.json().catch(() => ({}));
const deletedCount = result.deleted || count;
// 删除成功后刷新当前页面
const currentPage = monitorState.pagination.page;
await refreshMonitorPanel(currentPage);
const batchSuccessMsg = typeof window.t === 'function' ? window.t('mcpMonitor.batchDeleteSuccess', { count: deletedCount }) : `成功删除 ${deletedCount} 条执行记录`;
alert(batchSuccessMsg);
} catch (error) {
console.error('批量删除执行记录失败:', error);
const batchFailedMsg = typeof window.t === 'function' ? window.t('mcp.batchDeleteFailed') : '批量删除执行记录失败';
alert(batchFailedMsg + ': ' + error.message);
}
}
function formatExecutionDuration(start, end) {
const unknownLabel = typeof window.t === 'function' ? window.t('mcpMonitor.unknown') : '未知';
if (!start) {
return unknownLabel;
}
const startTime = new Date(start);
const endTime = end ? new Date(end) : new Date();
if (Number.isNaN(startTime.getTime()) || Number.isNaN(endTime.getTime())) {
return unknownLabel;
}
const diffMs = Math.max(0, endTime - startTime);
const seconds = Math.floor(diffMs / 1000);
if (seconds < 60) {
return typeof window.t === 'function' ? window.t('mcpMonitor.durationSeconds', { n: seconds }) : seconds + ' 秒';
}
const minutes = Math.floor(seconds / 60);
if (minutes < 60) {
const remain = seconds % 60;
if (remain > 0) {
return typeof window.t === 'function' ? window.t('mcpMonitor.durationMinutes', { minutes: minutes, seconds: remain }) : minutes + ' 分 ' + remain + ' 秒';
}
return typeof window.t === 'function' ? window.t('mcpMonitor.durationMinutesOnly', { minutes: minutes }) : minutes + ' 分';
}
const hours = Math.floor(minutes / 60);
const remainMinutes = minutes % 60;
if (remainMinutes > 0) {
return typeof window.t === 'function' ? window.t('mcpMonitor.durationHours', { hours: hours, minutes: remainMinutes }) : hours + ' 小时 ' + remainMinutes + ' 分';
}
return typeof window.t === 'function' ? window.t('mcpMonitor.durationHoursOnly', { hours: hours }) : hours + ' 小时';
}