From a4df77fc137d90a852f9299a48860a09ee45aa86 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=85=AC=E6=98=8E?=
<83812544+Ed1s0nZ@users.noreply.github.com>
Date: Sun, 16 Nov 2025 22:35:51 +0800
Subject: [PATCH] Add files via upload
---
internal/handler/agent.go | 13 ++++
internal/handler/task_manager.go | 15 +++++
web/static/js/app.js | 110 +++++++++++++++++++++++++++----
3 files changed, 127 insertions(+), 11 deletions(-)
diff --git a/internal/handler/agent.go b/internal/handler/agent.go
index 372108ec..065fa9d2 100644
--- a/internal/handler/agent.go
+++ b/internal/handler/agent.go
@@ -10,6 +10,7 @@ import (
"cyberstrike-ai/internal/agent"
"cyberstrike-ai/internal/database"
+
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
@@ -318,6 +319,10 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
case errors.Is(cause, ErrTaskCancelled):
taskStatus = "cancelled"
cancelMsg := "任务已被用户取消,后续操作已停止。"
+
+ // 在发送事件前更新任务状态,确保前端能及时看到状态变化
+ h.tasks.UpdateTaskStatus(conversationID, taskStatus)
+
if assistantMessageID != "" {
if _, updateErr := h.db.Exec(
"UPDATE messages SET content = ? WHERE id = ?",
@@ -339,6 +344,10 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
case errors.Is(err, context.DeadlineExceeded) || errors.Is(cause, context.DeadlineExceeded):
taskStatus = "timeout"
timeoutMsg := "任务执行超时,已自动终止。"
+
+ // 在发送事件前更新任务状态,确保前端能及时看到状态变化
+ h.tasks.UpdateTaskStatus(conversationID, taskStatus)
+
if assistantMessageID != "" {
if _, updateErr := h.db.Exec(
"UPDATE messages SET content = ? WHERE id = ?",
@@ -360,6 +369,10 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
default:
taskStatus = "failed"
errorMsg := "执行失败: " + err.Error()
+
+ // 在发送事件前更新任务状态,确保前端能及时看到状态变化
+ h.tasks.UpdateTaskStatus(conversationID, taskStatus)
+
if assistantMessageID != "" {
if _, updateErr := h.db.Exec(
"UPDATE messages SET content = ? WHERE id = ?",
diff --git a/internal/handler/task_manager.go b/internal/handler/task_manager.go
index 02a549ad..7c44a338 100644
--- a/internal/handler/task_manager.go
+++ b/internal/handler/task_manager.go
@@ -89,6 +89,21 @@ func (m *AgentTaskManager) CancelTask(conversationID string, cause error) (bool,
return true, nil
}
+// UpdateTaskStatus 更新任务状态但不删除任务(用于在发送事件前更新状态)
+func (m *AgentTaskManager) UpdateTaskStatus(conversationID string, status string) {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ task, exists := m.tasks[conversationID]
+ if !exists {
+ return
+ }
+
+ if status != "" {
+ task.Status = status
+ }
+}
+
// FinishTask 完成任务并从管理器中移除
func (m *AgentTaskManager) FinishTask(conversationID string, finalStatus string) {
m.mu.Lock()
diff --git a/web/static/js/app.js b/web/static/js/app.js
index e0c14ece..6054c26d 100644
--- a/web/static/js/app.js
+++ b/web/static/js/app.js
@@ -558,6 +558,9 @@ function integrateProgressToMCPSection(progressId, assistantMessageId) {
// 获取时间线内容
const hasContent = timelineHTML.trim().length > 0;
+ // 检查时间线中是否有错误项
+ const hasError = timeline && timeline.querySelector('.timeline-item-error');
+
// 确保按钮容器存在
let buttonsContainer = mcpSection.querySelector('.mcp-call-buttons');
if (!buttonsContainer) {
@@ -582,19 +585,25 @@ function integrateProgressToMCPSection(progressId, assistantMessageId) {
}
}
- // 设置详情内容(默认折叠状态)
+ // 设置详情内容(如果有错误,默认折叠;否则默认折叠)
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 = '展开详情';
+ }
}
// 移除原来的进度消息
@@ -717,13 +726,21 @@ function convertProgressToDetails(progressId, assistantMessageId) {
// 获取时间线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}
` : '暂无过程详情(可能执行过快或未触发详细事件)
'}
+ ${hasContent ? `${timelineHTML}
` : '暂无过程详情(可能执行过快或未触发详细事件)
'}
`;
contentWrapper.appendChild(bubble);
@@ -877,6 +894,41 @@ function handleStreamEvent(event, progressElement, progressId,
message: event.message,
data: event.data
});
+
+ // 如果错误事件包含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);
+ }
+ }
+
+ // 立即刷新任务状态(执行失败时任务状态会更新)
+ loadActiveTasks();
break;
case 'done':
@@ -894,7 +946,18 @@ function handleStreamEvent(event, progressElement, progressId,
if (progressTaskState.has(progressId)) {
finalizeProgressTask(progressId, '已完成');
}
+
+ // 检查时间线中是否有错误项
+ const hasError = timeline && timeline.querySelector('.timeline-item-error');
+
+ // 立即刷新任务状态(确保任务状态同步)
loadActiveTasks();
+
+ // 延迟再次刷新任务状态(确保后端已完成状态更新)
+ setTimeout(() => {
+ loadActiveTasks();
+ }, 200);
+
// 完成时自动折叠所有详情(延迟一下确保response事件已处理)
setTimeout(() => {
const assistantIdFromDone = getAssistantId();
@@ -904,6 +967,14 @@ function handleStreamEvent(event, progressElement, progressId,
// 如果无法获取助手ID,尝试折叠所有详情
collapseAllProgressDetails(null, progressId);
}
+
+ // 如果有错误,确保详情是折叠的(错误时应该默认折叠)
+ if (hasError) {
+ // 再次确保折叠(延迟一点确保DOM已更新)
+ setTimeout(() => {
+ collapseAllProgressDetails(assistantIdFromDone || null, progressId);
+ }, 200);
+ }
}, 500);
break;
}
@@ -1646,22 +1717,39 @@ function renderActiveTasks(tasks) {
? 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 = `
- ${task.status === 'cancelling' ? '取消中' : '执行中'}
+ ${statusText}
${escapeHtml(task.message || '未命名任务')}
${timeText ? `${timeText}` : ''}
-
+ ${!isFinalStatus ? '' : ''}
`;
- const cancelBtn = item.querySelector('.active-task-cancel');
- cancelBtn.onclick = () => cancelActiveTask(task.conversationId, cancelBtn);
- if (task.status === 'cancelling') {
- cancelBtn.disabled = true;
- cancelBtn.textContent = '取消中...';
+ // 只有非最终状态的任务才显示停止按钮
+ 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);