From e8a3a47ac7d93d003c9a8cb4777d7ed3a6076cb1 Mon Sep 17 00:00:00 2001 From: ooodc <549496103@qq.com> Date: Wed, 3 Jun 2026 21:53:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20TaskStarted=20?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=EF=BC=8C=E6=94=AF=E6=8C=81=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E5=BC=80=E5=A7=8B=E9=80=9A=E7=9F=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bus/message.rs | 1 + src/protocol/mod.rs | 6 ++ src/protocol/ws_adapter.rs | 5 + src/tools/task/runtime.rs | 29 ++++- web/src/components/Chat/MessageBubble.tsx | 126 +++++++++++++++------- web/src/hooks/useChat.ts | 17 +++ web/src/index.css | 18 ++++ web/src/types/protocol.ts | 8 ++ 8 files changed, 171 insertions(+), 39 deletions(-) diff --git a/src/bus/message.rs b/src/bus/message.rs index 5f2d3b7..87f5778 100644 --- a/src/bus/message.rs +++ b/src/bus/message.rs @@ -293,6 +293,7 @@ pub enum OutboundEventKind { ToolPending, SchedulerNotification, ErrorNotification, + TaskStarted, } impl OutboundMessage { diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index 2094a34..fbc2616 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -145,6 +145,12 @@ pub enum WsOutbound { }, #[serde(rename = "error")] Error { code: String, message: String }, + #[serde(rename = "task_started")] + TaskStarted { + task_id: String, + description: String, + subagent_type: String, + }, #[serde(rename = "session_established")] SessionEstablished { session_id: String }, #[serde(rename = "session_created")] diff --git a/src/protocol/ws_adapter.rs b/src/protocol/ws_adapter.rs index c92d501..4f2bc71 100644 --- a/src/protocol/ws_adapter.rs +++ b/src/protocol/ws_adapter.rs @@ -141,6 +141,11 @@ pub(crate) fn ws_outbound_from_outbound_message(message: &OutboundMessage) -> Ve code: "AGENT_ERROR".to_string(), message: message.content.clone(), }], + OutboundEventKind::TaskStarted => vec![WsOutbound::TaskStarted { + task_id: message.metadata.get("task_id").cloned().unwrap_or_default(), + description: message.metadata.get("task_description").cloned().unwrap_or_default(), + subagent_type: message.metadata.get("task_subagent_type").cloned().unwrap_or_default(), + }], } } diff --git a/src/tools/task/runtime.rs b/src/tools/task/runtime.rs index ec629d1..94f5ddd 100644 --- a/src/tools/task/runtime.rs +++ b/src/tools/task/runtime.rs @@ -9,7 +9,7 @@ use serde::Deserialize; use crate::agent::{AgentLoop, AgentRuntimeConfig, EmittedMessageHandler, PersistingEmittedMessageHandler, SystemPrompt, SystemPromptContext, SystemPromptProvider}; use crate::bus::ChatMessage; -use crate::bus::message::OutboundMessage; +use crate::bus::message::{OutboundMessage, OutboundEventKind}; use crate::bus::MessageBus; use crate::config::{LLMProviderConfig, SubagentsConfig}; use crate::storage::ConversationRepository; @@ -418,6 +418,33 @@ impl SubAgentRuntime for DefaultSubAgentRuntime { ); self.task_repository.save_task_session(&session).await?; + // 5.1 立即通知前端 task_id(让前端可以显示"查看实时进度"按钮) + if let Some(bus) = &self.bus { + let mut metadata = HashMap::new(); + metadata.insert("task_id".to_string(), session.id.clone()); + metadata.insert("task_description".to_string(), session.description.clone()); + metadata.insert("task_subagent_type".to_string(), session.subagent_type.clone()); + + let event = OutboundMessage { + channel: session.parent_channel_name.clone(), + chat_id: session.parent_chat_id.clone(), + session_id: Some(session.parent_session_id.clone()), + content: String::new(), + reply_to: None, + media: Vec::new(), + metadata, + event_kind: OutboundEventKind::TaskStarted, + role: "system".to_string(), + tool_call_id: None, + tool_name: None, + tool_arguments: None, + }; + + if let Err(e) = bus.publish_outbound(event).await { + tracing::warn!(error = %e, task_id = %session.id, "Failed to publish TaskStarted event"); + } + } + // 6. 构建子代理系统提示词 let system_prompt = SubagentPromptBuilder::build( &def, diff --git a/web/src/components/Chat/MessageBubble.tsx b/web/src/components/Chat/MessageBubble.tsx index 2be8fb6..80a25f5 100644 --- a/web/src/components/Chat/MessageBubble.tsx +++ b/web/src/components/Chat/MessageBubble.tsx @@ -1,9 +1,60 @@ import { useState } from 'react' -import { User, Bot, Wrench, CheckCircle, AlertCircle, Terminal, File, Image, FileText, Music, Video, Download, ChevronDown, ChevronRight, Copy, Check } from 'lucide-react' +import { User, Bot, Wrench, CheckCircle, AlertCircle, Terminal, File, Image, FileText, Music, Video, Download, ChevronDown, ChevronRight, Copy, Check, Loader2, XCircle, Clock, Loader } from 'lucide-react' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import type { ChatMessage, Attachment, TaskToolResult } from '../../types/protocol' +// 状态图标组件 +function StatusIcon({ status, size = 14 }: { status: 'calling' | 'result' | 'pending' | 'success' | 'failed' | 'timeout', size?: number }) { + const iconClass = `transition-all duration-300` + + switch (status) { + case 'calling': + return ( + + ) + case 'result': + case 'success': + return ( + + ) + case 'failed': + return ( + + ) + case 'timeout': + return ( + + ) + case 'pending': + return ( + + ) + default: + return null + } +} + interface MessageBubbleProps { message: ChatMessage onNavigateToSubAgent?: (taskId: string, description: string) => void @@ -156,25 +207,22 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr const statusConfig = { calling: { dot: 'bg-amber-400 animate-pulse', - label: '执行中', fullBorder: 'border-amber-500/30', - labelColor: 'text-amber-400', + iconColor: 'text-amber-400', avatarBg: 'bg-amber-500/20', avatarIcon: 'text-amber-400', }, result: { dot: 'bg-emerald-400', - label: '已完成', fullBorder: 'border-emerald-500/30', - labelColor: 'text-emerald-400', + iconColor: 'text-emerald-400', avatarBg: 'bg-emerald-500/20', avatarIcon: 'text-emerald-400', }, pending: { dot: 'bg-orange-400 animate-pulse', - label: '待确认', fullBorder: 'border-orange-500/30', - labelColor: 'text-orange-400', + iconColor: 'text-orange-400', avatarBg: 'bg-orange-500/20', avatarIcon: 'text-orange-400', }, @@ -215,9 +263,9 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr // task tool 专用的状态配色 const taskStatusConfig = { - success: { dot: 'bg-emerald-400', label: '成功', borderColor: 'border-emerald-500/40', labelColor: 'text-emerald-400' }, - failed: { dot: 'bg-red-400', label: '失败', borderColor: 'border-red-500/40', labelColor: 'text-red-400' }, - timeout: { dot: 'bg-amber-400', label: '超时', borderColor: 'border-amber-500/40', labelColor: 'text-amber-400' }, + success: { dot: 'bg-emerald-400', borderColor: 'border-emerald-500/40', iconColor: 'text-emerald-400' }, + failed: { dot: 'bg-red-400', borderColor: 'border-red-500/40', iconColor: 'text-red-400' }, + timeout: { dot: 'bg-amber-400', borderColor: 'border-amber-500/40', iconColor: 'text-amber-400' }, } as const return ( @@ -260,10 +308,14 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr {isTaskTool ? (taskDescription || '子智能体任务') : (message.toolName || 'Tool')} - - {taskResult ? taskStatusConfig[taskResult.status].label : statusConfig.label} + {taskResult ? ( + + ) : ( + + )} {status === 'result' && message.durationMs != null && ( @@ -299,25 +351,21 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr {taskResult.summary} )} - {taskResult ? ( - <> -
- -
-
- 点击查看子智能体输出 -
- - ) : isTaskTool && message.subagentTaskId ? ( + {taskResult && ( +
+ +
+ )} + {isTaskTool && message.subagentTaskId && !taskResult && (
- ) : hasResult ? ( -
- 点击查看工具结果 -
- ) : ( + )} + {!taskResult && !isTaskTool && !hasResult && (
- {isTaskTool ? '子智能体正在执行...' : '等待工具执行...'} + 等待工具执行... +
+ )} + {isTaskTool && !taskResult && !message.subagentTaskId && ( +
+ 子智能体正在执行...
)} diff --git a/web/src/hooks/useChat.ts b/web/src/hooks/useChat.ts index 09edfb4..49c7340 100644 --- a/web/src/hooks/useChat.ts +++ b/web/src/hooks/useChat.ts @@ -14,6 +14,7 @@ import type { TopicSummary, Session, TaskMessagesLoaded, + TaskStarted, Attachment, SchedulerJobList, SchedulerJobSummary, @@ -289,6 +290,22 @@ export function useChat(): UseChatReturn { break } + case 'task_started': { + const msg = message as TaskStarted + // 立即更新对应的 task tool_call,让用户可以点击查看实时进度 + setMessages((prev) => { + for (let i = prev.length - 1; i >= 0; i--) { + if (prev[i].type === 'tool_call' && prev[i].toolName === 'task' && !prev[i].subagentTaskId) { + const updated = [...prev] + updated[i] = { ...updated[i], subagentTaskId: msg.task_id } + return updated + } + } + return prev + }) + break + } + case 'session_list': { const msg = message as SessionList console.log('Session list received:', msg) diff --git a/web/src/index.css b/web/src/index.css index 4eae44b..8b1e012 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -149,6 +149,20 @@ body { } } +@keyframes scale-in { + 0% { + opacity: 0; + transform: scale(0.5); + } + 50% { + transform: scale(1.1); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + .animate-slide-in { animation: slide-in 0.3s ease-out; } @@ -157,6 +171,10 @@ body { animation: fade-in 0.2s ease-out; } +.animate-scale-in { + animation: scale-in 0.3s ease-out; +} + .typing-indicator span { animation: typing-dot 1.4s infinite; display: inline-block; diff --git a/web/src/types/protocol.ts b/web/src/types/protocol.ts index 3b3e7ee..eb5dd27 100644 --- a/web/src/types/protocol.ts +++ b/web/src/types/protocol.ts @@ -84,6 +84,13 @@ export interface WsError { message: string } +export interface TaskStarted { + type: 'task_started' + task_id: string + description: string + subagent_type: string +} + export interface SessionEstablished { type: 'session_established' session_id: string @@ -203,6 +210,7 @@ export type WsOutbound = | ToolResult | ToolPending | WsError + | TaskStarted | SessionEstablished | SessionCreated | SessionList