diff --git a/server/chat_flow_task_main.py b/server/chat_flow_task_main.py index c5720a6..658a0e5 100644 --- a/server/chat_flow_task_main.py +++ b/server/chat_flow_task_main.py @@ -462,9 +462,61 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac state["suppress_next"] = False # 添加到对话历史 + user_work_started_at = datetime.now().isoformat() + user_message_index = -1 + user_work_finalized = False history_len_before = len(getattr(web_terminal.context_manager, "conversation_history", []) or []) is_first_user_message = history_len_before == 0 - web_terminal.context_manager.add_conversation("user", message, images=images, videos=videos) + web_terminal.context_manager.add_conversation( + "user", + message, + images=images, + videos=videos, + metadata={ + "work_timer": { + "status": "working", + "started_at": user_work_started_at + } + } + ) + try: + user_message_index = len(getattr(web_terminal.context_manager, "conversation_history", []) or []) - 1 + except Exception: + user_message_index = -1 + + def finalize_user_work_timer(): + nonlocal user_work_finalized + if user_work_finalized: + return + history = getattr(web_terminal.context_manager, "conversation_history", None) or [] + if user_message_index < 0 or user_message_index >= len(history): + return + target_msg = history[user_message_index] or {} + if target_msg.get("role") != "user": + return + metadata = target_msg.get("metadata") or {} + timer = metadata.get("work_timer") + if not isinstance(timer, dict): + timer = {} + started_at = timer.get("started_at") or target_msg.get("timestamp") or user_work_started_at + start_ts = None + try: + start_ts = datetime.fromisoformat(str(started_at).replace("Z", "+00:00")).timestamp() + except Exception: + start_ts = None + now_ts = time.time() + duration_ms = int(max(0.0, (now_ts - start_ts) * 1000.0)) if start_ts is not None else 0 + timer.update({ + "status": "completed", + "started_at": started_at, + "finished_at": datetime.now().isoformat(), + "duration_ms": duration_ms + }) + metadata["work_timer"] = timer + target_msg["metadata"] = metadata + history[user_message_index] = target_msg + web_terminal.context_manager.auto_save_conversation(force=True) + user_work_finalized = True # Skill 提示系统:检测关键词并在用户消息之后插入 system 消息 try: @@ -535,6 +587,7 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac 'status_code': 400, 'error_type': 'context_overflow' }) + finalize_user_work_timer() return usage_percent = (current_tokens / max_context_tokens) * 100 warned = web_terminal.context_manager.conversation_metadata.get("context_warning_sent", False) @@ -649,6 +702,7 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac 'message': "配额已达到上限,暂时无法继续调用模型。", 'quota': quota_info }) + finalize_user_work_timer() return tool_call_limit_label = MAX_TOTAL_TOOL_CALLS if MAX_TOTAL_TOOL_CALLS is not None else "∞" @@ -687,6 +741,7 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac accumulated_response=accumulated_response, ) if stream_result.get("stopped"): + finalize_user_work_timer() return full_response = stream_result["full_response"] @@ -859,6 +914,7 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac ) last_tool_call_time = tool_loop_result.get("last_tool_call_time", last_tool_call_time) if tool_loop_result.get("stopped"): + finalize_user_work_timer() return # 标记不再是第一次迭代 @@ -923,6 +979,8 @@ async def handle_task_with_sender(terminal: WebTerminal, workspace: UserWorkspac socketio.start_background_task(run_poll) # 发送完成事件(如果有子智能体在运行,前端会保持等待状态) + if not has_running_sub_agents: + finalize_user_work_timer() sender('task_complete', { 'total_iterations': total_iterations, 'total_tool_calls': total_tool_calls, diff --git a/static/src/app/methods/history.ts b/static/src/app/methods/history.ts index 2588e8b..eb82300 100644 --- a/static/src/app/methods/history.ts +++ b/static/src/app/methods/history.ts @@ -165,7 +165,8 @@ export const historyMethods = { role: 'user', content: message.content || '', images, - videos + videos, + metadata: message.metadata || {} }); debugLog('添加用户消息:', message.content?.substring(0, 50) + '...'); diff --git a/static/src/app/methods/taskPolling.ts b/static/src/app/methods/taskPolling.ts index dbd505e..de7f713 100644 --- a/static/src/app/methods/taskPolling.ts +++ b/static/src/app/methods/taskPolling.ts @@ -653,6 +653,34 @@ export const taskPollingMethods = { this.conditionalScrollToBottom(); }, + markLatestUserWorkCompleted() { + if (!Array.isArray(this.messages) || this.messages.length === 0) { + return; + } + for (let i = this.messages.length - 1; i >= 0; i -= 1) { + const msg = this.messages[i]; + if (!msg || msg.role !== 'user') { + continue; + } + msg.metadata = msg.metadata || {}; + msg.metadata.work_timer = msg.metadata.work_timer || {}; + const timer = msg.metadata.work_timer; + if (timer.status === 'completed' && typeof timer.duration_ms === 'number') { + return; + } + const nowIso = new Date().toISOString(); + const startedAt = timer.started_at || msg.timestamp || nowIso; + const startMs = Date.parse(startedAt); + const endMs = Date.now(); + const durationMs = Number.isFinite(startMs) ? Math.max(0, endMs - startMs) : 0; + timer.status = 'completed'; + timer.started_at = startedAt; + timer.finished_at = nowIso; + timer.duration_ms = durationMs; + return; + } + }, + handleTaskComplete(data: any) { const hasRunningSubAgents = !!data?.has_running_sub_agents; if (hasRunningSubAgents) { @@ -664,6 +692,9 @@ export const taskPollingMethods = { // 同步处理状态更新 this.streamingMessage = false; this.stopRequested = false; + if (!hasRunningSubAgents) { + this.markLatestUserWorkCompleted(); + } if (hasRunningSubAgents) { this.taskInProgress = true; @@ -688,6 +719,7 @@ export const taskPollingMethods = { handleTaskStopped(data: any, eventIdx: number) { debugLog('[TaskPolling] 任务已停止, idx:', eventIdx, data); + this.markLatestUserWorkCompleted(); this.streamingMessage = false; this.taskInProgress = false; this.stopRequested = false; @@ -790,6 +822,7 @@ export const taskPollingMethods = { }); // 清理状态 + this.markLatestUserWorkCompleted(); this.streamingMessage = false; this.taskInProgress = false; this.stopRequested = false; diff --git a/static/src/components/chat/ChatArea.vue b/static/src/components/chat/ChatArea.vue index 35fe352..ae5cab6 100644 --- a/static/src/components/chat/ChatArea.vue +++ b/static/src/components/chat/ChatArea.vue @@ -27,6 +27,7 @@