feat: 添加 TaskStarted 事件,支持任务开始通知功能
This commit is contained in:
parent
1d4ebb27a7
commit
e8a3a47ac7
@ -293,6 +293,7 @@ pub enum OutboundEventKind {
|
|||||||
ToolPending,
|
ToolPending,
|
||||||
SchedulerNotification,
|
SchedulerNotification,
|
||||||
ErrorNotification,
|
ErrorNotification,
|
||||||
|
TaskStarted,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OutboundMessage {
|
impl OutboundMessage {
|
||||||
|
|||||||
@ -145,6 +145,12 @@ pub enum WsOutbound {
|
|||||||
},
|
},
|
||||||
#[serde(rename = "error")]
|
#[serde(rename = "error")]
|
||||||
Error { code: String, message: String },
|
Error { code: String, message: String },
|
||||||
|
#[serde(rename = "task_started")]
|
||||||
|
TaskStarted {
|
||||||
|
task_id: String,
|
||||||
|
description: String,
|
||||||
|
subagent_type: String,
|
||||||
|
},
|
||||||
#[serde(rename = "session_established")]
|
#[serde(rename = "session_established")]
|
||||||
SessionEstablished { session_id: String },
|
SessionEstablished { session_id: String },
|
||||||
#[serde(rename = "session_created")]
|
#[serde(rename = "session_created")]
|
||||||
|
|||||||
@ -141,6 +141,11 @@ pub(crate) fn ws_outbound_from_outbound_message(message: &OutboundMessage) -> Ve
|
|||||||
code: "AGENT_ERROR".to_string(),
|
code: "AGENT_ERROR".to_string(),
|
||||||
message: message.content.clone(),
|
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::agent::{AgentLoop, AgentRuntimeConfig, EmittedMessageHandler, PersistingEmittedMessageHandler, SystemPrompt, SystemPromptContext, SystemPromptProvider};
|
||||||
use crate::bus::ChatMessage;
|
use crate::bus::ChatMessage;
|
||||||
use crate::bus::message::OutboundMessage;
|
use crate::bus::message::{OutboundMessage, OutboundEventKind};
|
||||||
use crate::bus::MessageBus;
|
use crate::bus::MessageBus;
|
||||||
use crate::config::{LLMProviderConfig, SubagentsConfig};
|
use crate::config::{LLMProviderConfig, SubagentsConfig};
|
||||||
use crate::storage::ConversationRepository;
|
use crate::storage::ConversationRepository;
|
||||||
@ -418,6 +418,33 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
|
|||||||
);
|
);
|
||||||
self.task_repository.save_task_session(&session).await?;
|
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. 构建子代理系统提示词
|
// 6. 构建子代理系统提示词
|
||||||
let system_prompt = SubagentPromptBuilder::build(
|
let system_prompt = SubagentPromptBuilder::build(
|
||||||
&def,
|
&def,
|
||||||
|
|||||||
@ -1,9 +1,60 @@
|
|||||||
import { useState } from 'react'
|
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 ReactMarkdown from 'react-markdown'
|
||||||
import remarkGfm from 'remark-gfm'
|
import remarkGfm from 'remark-gfm'
|
||||||
import type { ChatMessage, Attachment, TaskToolResult } from '../../types/protocol'
|
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 {
|
interface MessageBubbleProps {
|
||||||
message: ChatMessage
|
message: ChatMessage
|
||||||
onNavigateToSubAgent?: (taskId: string, description: string) => void
|
onNavigateToSubAgent?: (taskId: string, description: string) => void
|
||||||
@ -156,25 +207,22 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
|
|||||||
const statusConfig = {
|
const statusConfig = {
|
||||||
calling: {
|
calling: {
|
||||||
dot: 'bg-amber-400 animate-pulse',
|
dot: 'bg-amber-400 animate-pulse',
|
||||||
label: '执行中',
|
|
||||||
fullBorder: 'border-amber-500/30',
|
fullBorder: 'border-amber-500/30',
|
||||||
labelColor: 'text-amber-400',
|
iconColor: 'text-amber-400',
|
||||||
avatarBg: 'bg-amber-500/20',
|
avatarBg: 'bg-amber-500/20',
|
||||||
avatarIcon: 'text-amber-400',
|
avatarIcon: 'text-amber-400',
|
||||||
},
|
},
|
||||||
result: {
|
result: {
|
||||||
dot: 'bg-emerald-400',
|
dot: 'bg-emerald-400',
|
||||||
label: '已完成',
|
|
||||||
fullBorder: 'border-emerald-500/30',
|
fullBorder: 'border-emerald-500/30',
|
||||||
labelColor: 'text-emerald-400',
|
iconColor: 'text-emerald-400',
|
||||||
avatarBg: 'bg-emerald-500/20',
|
avatarBg: 'bg-emerald-500/20',
|
||||||
avatarIcon: 'text-emerald-400',
|
avatarIcon: 'text-emerald-400',
|
||||||
},
|
},
|
||||||
pending: {
|
pending: {
|
||||||
dot: 'bg-orange-400 animate-pulse',
|
dot: 'bg-orange-400 animate-pulse',
|
||||||
label: '待确认',
|
|
||||||
fullBorder: 'border-orange-500/30',
|
fullBorder: 'border-orange-500/30',
|
||||||
labelColor: 'text-orange-400',
|
iconColor: 'text-orange-400',
|
||||||
avatarBg: 'bg-orange-500/20',
|
avatarBg: 'bg-orange-500/20',
|
||||||
avatarIcon: 'text-orange-400',
|
avatarIcon: 'text-orange-400',
|
||||||
},
|
},
|
||||||
@ -215,9 +263,9 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
|
|||||||
|
|
||||||
// task tool 专用的状态配色
|
// task tool 专用的状态配色
|
||||||
const taskStatusConfig = {
|
const taskStatusConfig = {
|
||||||
success: { dot: 'bg-emerald-400', label: '成功', borderColor: 'border-emerald-500/40', labelColor: 'text-emerald-400' },
|
success: { dot: 'bg-emerald-400', borderColor: 'border-emerald-500/40', iconColor: 'text-emerald-400' },
|
||||||
failed: { dot: 'bg-red-400', label: '失败', borderColor: 'border-red-500/40', labelColor: 'text-red-400' },
|
failed: { dot: 'bg-red-400', borderColor: 'border-red-500/40', iconColor: 'text-red-400' },
|
||||||
timeout: { dot: 'bg-amber-400', label: '超时', borderColor: 'border-amber-500/40', labelColor: 'text-amber-400' },
|
timeout: { dot: 'bg-amber-400', borderColor: 'border-amber-500/40', iconColor: 'text-amber-400' },
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -260,10 +308,14 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
|
|||||||
<span className="text-sm font-medium text-zinc-300 truncate">
|
<span className="text-sm font-medium text-zinc-300 truncate">
|
||||||
{isTaskTool ? (taskDescription || '子智能体任务') : (message.toolName || 'Tool')}
|
{isTaskTool ? (taskDescription || '子智能体任务') : (message.toolName || 'Tool')}
|
||||||
</span>
|
</span>
|
||||||
<span className={`text-xs flex-shrink-0 transition-colors duration-500 ${
|
<span className={`flex-shrink-0 transition-all duration-300 ${
|
||||||
taskResult ? taskStatusConfig[taskResult.status].labelColor : statusConfig.labelColor
|
taskResult ? taskStatusConfig[taskResult.status].iconColor : statusConfig.iconColor
|
||||||
}`}>
|
}`}>
|
||||||
{taskResult ? taskStatusConfig[taskResult.status].label : statusConfig.label}
|
{taskResult ? (
|
||||||
|
<StatusIcon status={taskResult.status} />
|
||||||
|
) : (
|
||||||
|
<StatusIcon status={status} />
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
{status === 'result' && message.durationMs != null && (
|
{status === 'result' && message.durationMs != null && (
|
||||||
<span className="text-xs text-zinc-600 flex-shrink-0 tabular-nums ml-1">
|
<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}
|
{taskResult.summary}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{taskResult ? (
|
{taskResult && (
|
||||||
<>
|
<div className="px-3 pb-1">
|
||||||
<div className="px-3 pb-1">
|
<button
|
||||||
<button
|
onClick={(e) => {
|
||||||
onClick={(e) => {
|
e.stopPropagation()
|
||||||
e.stopPropagation()
|
onNavigateToSubAgent?.(taskResult.task_id, taskDescription || '子智能体任务')
|
||||||
onNavigateToSubAgent?.(taskResult.task_id, taskDescription || '子智能体任务')
|
}}
|
||||||
}}
|
className="text-xs text-[#00f0ff] hover:text-[#00f0ff]/80 hover:underline transition-colors flex items-center gap-1"
|
||||||
className="text-xs text-[#00f0ff] hover:text-[#00f0ff]/80 hover:underline transition-colors flex items-center gap-1"
|
>
|
||||||
>
|
<span>查看完整会话</span>
|
||||||
<span>查看完整会话</span>
|
<span>→</span>
|
||||||
<span>→</span>
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
)}
|
||||||
<div className="px-3 pb-2 text-xs text-[#00f0ff]/50 flex items-center gap-1 select-none">
|
{isTaskTool && message.subagentTaskId && !taskResult && (
|
||||||
<span>点击查看子智能体输出</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : isTaskTool && message.subagentTaskId ? (
|
|
||||||
<div className="px-3 pb-1">
|
<div className="px-3 pb-1">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@ -330,13 +378,15 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
|
|||||||
<span>→</span>
|
<span>→</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : hasResult ? (
|
)}
|
||||||
<div className="px-3 pb-2 text-xs text-[#00f0ff]/50 flex items-center gap-1 select-none">
|
{!taskResult && !isTaskTool && !hasResult && (
|
||||||
<span>点击查看工具结果</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="px-3 pb-2 text-xs text-zinc-500">
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import type {
|
|||||||
TopicSummary,
|
TopicSummary,
|
||||||
Session,
|
Session,
|
||||||
TaskMessagesLoaded,
|
TaskMessagesLoaded,
|
||||||
|
TaskStarted,
|
||||||
Attachment,
|
Attachment,
|
||||||
SchedulerJobList,
|
SchedulerJobList,
|
||||||
SchedulerJobSummary,
|
SchedulerJobSummary,
|
||||||
@ -289,6 +290,22 @@ export function useChat(): UseChatReturn {
|
|||||||
break
|
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': {
|
case 'session_list': {
|
||||||
const msg = message as SessionList
|
const msg = message as SessionList
|
||||||
console.log('Session list received:', msg)
|
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 {
|
.animate-slide-in {
|
||||||
animation: slide-in 0.3s ease-out;
|
animation: slide-in 0.3s ease-out;
|
||||||
}
|
}
|
||||||
@ -157,6 +171,10 @@ body {
|
|||||||
animation: fade-in 0.2s ease-out;
|
animation: fade-in 0.2s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.animate-scale-in {
|
||||||
|
animation: scale-in 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
.typing-indicator span {
|
.typing-indicator span {
|
||||||
animation: typing-dot 1.4s infinite;
|
animation: typing-dot 1.4s infinite;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|||||||
@ -84,6 +84,13 @@ export interface WsError {
|
|||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TaskStarted {
|
||||||
|
type: 'task_started'
|
||||||
|
task_id: string
|
||||||
|
description: string
|
||||||
|
subagent_type: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface SessionEstablished {
|
export interface SessionEstablished {
|
||||||
type: 'session_established'
|
type: 'session_established'
|
||||||
session_id: string
|
session_id: string
|
||||||
@ -203,6 +210,7 @@ export type WsOutbound =
|
|||||||
| ToolResult
|
| ToolResult
|
||||||
| ToolPending
|
| ToolPending
|
||||||
| WsError
|
| WsError
|
||||||
|
| TaskStarted
|
||||||
| SessionEstablished
|
| SessionEstablished
|
||||||
| SessionCreated
|
| SessionCreated
|
||||||
| SessionList
|
| SessionList
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user