feat: 添加 TaskStarted 事件,支持任务开始通知功能
This commit is contained in:
parent
1d4ebb27a7
commit
e8a3a47ac7
@ -293,6 +293,7 @@ pub enum OutboundEventKind {
|
||||
ToolPending,
|
||||
SchedulerNotification,
|
||||
ErrorNotification,
|
||||
TaskStarted,
|
||||
}
|
||||
|
||||
impl OutboundMessage {
|
||||
|
||||
@ -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")]
|
||||
|
||||
@ -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(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 (
|
||||
<Loader2
|
||||
className={`${iconClass} animate-spin`}
|
||||
style={{ width: size, height: size }}
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
)
|
||||
case 'result':
|
||||
case 'success':
|
||||
return (
|
||||
<CheckCircle
|
||||
className={`${iconClass} animate-scale-in`}
|
||||
style={{ width: size, height: size }}
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
)
|
||||
case 'failed':
|
||||
return (
|
||||
<XCircle
|
||||
className={`${iconClass} animate-scale-in`}
|
||||
style={{ width: size, height: size }}
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
)
|
||||
case 'timeout':
|
||||
return (
|
||||
<Clock
|
||||
className={`${iconClass} animate-scale-in`}
|
||||
style={{ width: size, height: size }}
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
)
|
||||
case 'pending':
|
||||
return (
|
||||
<Loader
|
||||
className={`${iconClass} animate-spin`}
|
||||
style={{ width: size, height: size }}
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
)
|
||||
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
|
||||
<span className="text-sm font-medium text-zinc-300 truncate">
|
||||
{isTaskTool ? (taskDescription || '子智能体任务') : (message.toolName || 'Tool')}
|
||||
</span>
|
||||
<span className={`text-xs flex-shrink-0 transition-colors duration-500 ${
|
||||
taskResult ? taskStatusConfig[taskResult.status].labelColor : statusConfig.labelColor
|
||||
<span className={`flex-shrink-0 transition-all duration-300 ${
|
||||
taskResult ? taskStatusConfig[taskResult.status].iconColor : statusConfig.iconColor
|
||||
}`}>
|
||||
{taskResult ? taskStatusConfig[taskResult.status].label : statusConfig.label}
|
||||
{taskResult ? (
|
||||
<StatusIcon status={taskResult.status} />
|
||||
) : (
|
||||
<StatusIcon status={status} />
|
||||
)}
|
||||
</span>
|
||||
{status === 'result' && message.durationMs != null && (
|
||||
<span className="text-xs text-zinc-600 flex-shrink-0 tabular-nums ml-1">
|
||||
@ -299,25 +351,21 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
|
||||
{taskResult.summary}
|
||||
</div>
|
||||
)}
|
||||
{taskResult ? (
|
||||
<>
|
||||
<div className="px-3 pb-1">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onNavigateToSubAgent?.(taskResult.task_id, taskDescription || '子智能体任务')
|
||||
}}
|
||||
className="text-xs text-[#00f0ff] hover:text-[#00f0ff]/80 hover:underline transition-colors flex items-center gap-1"
|
||||
>
|
||||
<span>查看完整会话</span>
|
||||
<span>→</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-3 pb-2 text-xs text-[#00f0ff]/50 flex items-center gap-1 select-none">
|
||||
<span>点击查看子智能体输出</span>
|
||||
</div>
|
||||
</>
|
||||
) : isTaskTool && message.subagentTaskId ? (
|
||||
{taskResult && (
|
||||
<div className="px-3 pb-1">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onNavigateToSubAgent?.(taskResult.task_id, taskDescription || '子智能体任务')
|
||||
}}
|
||||
className="text-xs text-[#00f0ff] hover:text-[#00f0ff]/80 hover:underline transition-colors flex items-center gap-1"
|
||||
>
|
||||
<span>查看完整会话</span>
|
||||
<span>→</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{isTaskTool && message.subagentTaskId && !taskResult && (
|
||||
<div className="px-3 pb-1">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
@ -330,13 +378,15 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
|
||||
<span>→</span>
|
||||
</button>
|
||||
</div>
|
||||
) : hasResult ? (
|
||||
<div className="px-3 pb-2 text-xs text-[#00f0ff]/50 flex items-center gap-1 select-none">
|
||||
<span>点击查看工具结果</span>
|
||||
</div>
|
||||
) : (
|
||||
)}
|
||||
{!taskResult && !isTaskTool && !hasResult && (
|
||||
<div className="px-3 pb-2 text-xs text-zinc-500">
|
||||
{isTaskTool ? '子智能体正在执行...' : '等待工具执行...'}
|
||||
等待工具执行...
|
||||
</div>
|
||||
)}
|
||||
{isTaskTool && !taskResult && !message.subagentTaskId && (
|
||||
<div className="px-3 pb-2 text-xs text-zinc-500">
|
||||
子智能体正在执行...
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user