chore: 清理 P0 技术债
- 删除 web/src/components/Sidebar/.deprecated 目录(死代码) - 清理 useChat.ts/useWebSocket.ts 中 13 条调试 console.log - 修复 load_task_messages.rs 硬编码 task 状态: 新增 TaskSessionState::Unknown, DB 重建时返回 Unknown 而非误报 Completed, 前端显示"未知"
This commit is contained in:
parent
f264a7b307
commit
7eb2933ca5
@ -149,6 +149,15 @@ fn reconstruct_task_from_db(
|
|||||||
|
|
||||||
let now = record.updated_at;
|
let now = record.updated_at;
|
||||||
|
|
||||||
|
// DB 未持久化 task 状态字段,无法可靠区分 Running/Completed/Failed/Timeout。
|
||||||
|
// 用 Unknown 表示"重启后从 DB 重建,真实状态不可知",避免把 failed/timeout
|
||||||
|
// 误报为 Completed 误导用户。前端会把 Unknown 显示为"未知"。
|
||||||
|
tracing::warn!(
|
||||||
|
task_id = %task_id,
|
||||||
|
session_id = %session_id,
|
||||||
|
"Reconstructing task from DB after restart; true state unknown, marking as Unknown"
|
||||||
|
);
|
||||||
|
|
||||||
Ok(Some(TaskSession {
|
Ok(Some(TaskSession {
|
||||||
id: task_id.to_string(),
|
id: task_id.to_string(),
|
||||||
session_id,
|
session_id,
|
||||||
@ -158,9 +167,7 @@ fn reconstruct_task_from_db(
|
|||||||
parent_channel_name: record.channel_name.clone(),
|
parent_channel_name: record.channel_name.clone(),
|
||||||
description,
|
description,
|
||||||
subagent_type,
|
subagent_type,
|
||||||
// TODO: DB 重建时无法可靠推断 task 状态,暂硬编码为 Completed。
|
state: TaskSessionState::Unknown,
|
||||||
// 实际 failed/timeout 的子智能体会被错误显示为 completed,需独立跟进。
|
|
||||||
state: TaskSessionState::Completed,
|
|
||||||
created_at: record.created_at,
|
created_at: record.created_at,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
summary: None,
|
summary: None,
|
||||||
|
|||||||
@ -14,6 +14,8 @@ pub enum TaskSessionState {
|
|||||||
Failed,
|
Failed,
|
||||||
/// 已超时
|
/// 已超时
|
||||||
Timeout,
|
Timeout,
|
||||||
|
/// 状态未知(如重启后从 DB 重建时无法可靠推断原状态)
|
||||||
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TaskSessionState {
|
impl Default for TaskSessionState {
|
||||||
|
|||||||
@ -753,6 +753,7 @@ function App() {
|
|||||||
level.status === 'timeout' ? '超时' :
|
level.status === 'timeout' ? '超时' :
|
||||||
level.status === 'running' ? '执行中' :
|
level.status === 'running' ? '执行中' :
|
||||||
level.status === 'loading' ? '加载中...' :
|
level.status === 'loading' ? '加载中...' :
|
||||||
|
level.status === 'unknown' ? '未知' :
|
||||||
level.status
|
level.status
|
||||||
const statusColor =
|
const statusColor =
|
||||||
level.status === 'completed' ? 'text-emerald-400' :
|
level.status === 'completed' ? 'text-emerald-400' :
|
||||||
|
|||||||
@ -1,127 +0,0 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
import { Monitor, Smartphone, MessageSquare, Hash, ChevronDown, Eye, Pencil } from 'lucide-react'
|
|
||||||
import type { Channel } from '../../types/protocol'
|
|
||||||
|
|
||||||
interface ChannelSelectorProps {
|
|
||||||
channels: Channel[]
|
|
||||||
selectedChannel: string | null
|
|
||||||
onSelectChannel: (channelId: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const CHANNEL_ICONS: Record<string, React.ReactNode> = {
|
|
||||||
cli: <Monitor className="h-4 w-4" />,
|
|
||||||
websocket: <MessageSquare className="h-4 w-4" />,
|
|
||||||
feishu: <Smartphone className="h-4 w-4" />,
|
|
||||||
weixin: <Smartphone className="h-4 w-4" />,
|
|
||||||
wechat: <Smartphone className="h-4 w-4" />,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ChannelSelector({
|
|
||||||
channels,
|
|
||||||
selectedChannel,
|
|
||||||
onSelectChannel,
|
|
||||||
}: ChannelSelectorProps) {
|
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
|
||||||
|
|
||||||
const selected = channels.find((c) => c.id === selectedChannel)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between border-b border-white/8 px-4 py-3">
|
|
||||||
<h2 className="font-semibold text-white flex items-center gap-2 text-sm">
|
|
||||||
<Hash className="h-4 w-4 text-[#00f0ff]" />
|
|
||||||
通道
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Channel Dropdown */}
|
|
||||||
<div className="px-3 py-2">
|
|
||||||
<button
|
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
|
||||||
className="w-full flex items-center justify-between rounded-lg border border-white/10 bg-[#1a1a25]/80 px-3 py-2.5 text-left hover:bg-[#1a1a25] transition-all"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2.5">
|
|
||||||
<span className="text-zinc-400">
|
|
||||||
{selected ? CHANNEL_ICONS[selected.id] || <MessageSquare className="h-4 w-4" /> : <MessageSquare className="h-4 w-4" />}
|
|
||||||
</span>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-sm font-medium text-white">
|
|
||||||
{selected?.name || '选择通道'}
|
|
||||||
</span>
|
|
||||||
{selected && (
|
|
||||||
<span className={`text-xs flex items-center gap-1 ${selected.isWritable ? 'text-emerald-400' : 'text-zinc-500'}`}>
|
|
||||||
{selected.isWritable ? (
|
|
||||||
<>
|
|
||||||
<Pencil className="h-3 w-3" />
|
|
||||||
可输入
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Eye className="h-3 w-3" />
|
|
||||||
只读
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ChevronDown
|
|
||||||
className={`h-4 w-4 text-zinc-500 transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Dropdown Menu */}
|
|
||||||
{isOpen && (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className="fixed inset-0 z-10"
|
|
||||||
onClick={() => setIsOpen(false)}
|
|
||||||
/>
|
|
||||||
<div className="absolute left-3 right-3 z-20 mt-1 rounded-lg border border-white/10 bg-[#1a1a25] shadow-xl shadow-black/50 overflow-hidden">
|
|
||||||
{channels.length === 0 ? (
|
|
||||||
<div className="px-3 py-3 text-sm text-zinc-500 text-center">
|
|
||||||
暂无可用通道
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
channels.map((channel) => (
|
|
||||||
<button
|
|
||||||
key={channel.id}
|
|
||||||
onClick={() => {
|
|
||||||
onSelectChannel(channel.id)
|
|
||||||
setIsOpen(false)
|
|
||||||
}}
|
|
||||||
className={`w-full flex items-center justify-between px-3 py-2.5 text-left hover:bg-white/5 transition-colors ${
|
|
||||||
channel.id === selectedChannel ? 'bg-[#00f0ff]/10' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2.5">
|
|
||||||
<span className={channel.id === selectedChannel ? 'text-[#00f0ff]' : 'text-zinc-400'}>
|
|
||||||
{CHANNEL_ICONS[channel.id] || <MessageSquare className="h-4 w-4" />}
|
|
||||||
</span>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className={`text-sm ${channel.id === selectedChannel ? 'text-white font-medium' : 'text-zinc-300'}`}>
|
|
||||||
{channel.name}
|
|
||||||
</span>
|
|
||||||
{channel.description && (
|
|
||||||
<span className="text-xs text-zinc-500">{channel.description}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className={`text-xs px-1.5 py-0.5 rounded ${
|
|
||||||
channel.isWritable
|
|
||||||
? 'bg-emerald-400/10 text-emerald-400'
|
|
||||||
: 'bg-zinc-500/10 text-zinc-500'
|
|
||||||
}`}>
|
|
||||||
{channel.isWritable ? '可输入' : '只读'}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,114 +0,0 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
import { FolderOpen, ChevronDown, Hash } from 'lucide-react'
|
|
||||||
import type { Session } from '../../hooks/useChat'
|
|
||||||
|
|
||||||
interface SessionSelectorProps {
|
|
||||||
sessions: Session[]
|
|
||||||
selectedSession: string | null
|
|
||||||
channelId: string // 使用 channelId 而不是 channelName
|
|
||||||
onSelectSession: (sessionId: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SessionSelector({
|
|
||||||
sessions,
|
|
||||||
selectedSession,
|
|
||||||
channelId,
|
|
||||||
onSelectSession,
|
|
||||||
}: SessionSelectorProps) {
|
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
|
||||||
|
|
||||||
const selected = sessions.find((s) => s.id === selectedSession)
|
|
||||||
|
|
||||||
// 按通道 ID 筛选 Session
|
|
||||||
const channelSessions = sessions.filter(
|
|
||||||
(s) => s.channel_name === channelId
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between border-b border-white/8 px-4 py-3">
|
|
||||||
<h2 className="font-semibold text-white flex items-center gap-2 text-sm">
|
|
||||||
<FolderOpen className="h-4 w-4 text-[#00f0ff]" />
|
|
||||||
Session
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Session Dropdown */}
|
|
||||||
<div className="px-3 py-2">
|
|
||||||
<button
|
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
|
||||||
className="w-full flex items-center justify-between rounded-lg border border-white/10 bg-[#1a1a25]/80 px-3 py-2.5 text-left hover:bg-[#1a1a25] transition-all"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2.5">
|
|
||||||
<span className="text-zinc-400">
|
|
||||||
<FolderOpen className="h-4 w-4" />
|
|
||||||
</span>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-sm font-medium text-white truncate max-w-[160px]">
|
|
||||||
{selected?.title || '选择 Session'}
|
|
||||||
</span>
|
|
||||||
{selected && (
|
|
||||||
<span className="text-xs text-zinc-500">
|
|
||||||
{selected.message_count} 条消息
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ChevronDown
|
|
||||||
className={`h-4 w-4 text-zinc-500 transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Dropdown Menu */}
|
|
||||||
{isOpen && (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className="fixed inset-0 z-10"
|
|
||||||
onClick={() => setIsOpen(false)}
|
|
||||||
/>
|
|
||||||
<div className="absolute left-3 right-3 z-20 mt-1 rounded-lg border border-white/10 bg-[#1a1a25] shadow-xl shadow-black/50 overflow-hidden">
|
|
||||||
{channelSessions.length === 0 ? (
|
|
||||||
<div className="px-3 py-3 text-sm text-zinc-500 text-center">
|
|
||||||
暂无 Session
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
channelSessions.map((session, index) => (
|
|
||||||
<button
|
|
||||||
key={session.id}
|
|
||||||
onClick={() => {
|
|
||||||
onSelectSession(session.id)
|
|
||||||
setIsOpen(false)
|
|
||||||
}}
|
|
||||||
className={`w-full flex items-center justify-between px-3 py-2.5 text-left hover:bg-white/5 transition-colors ${
|
|
||||||
session.id === selectedSession ? 'bg-[#00f0ff]/10' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2.5">
|
|
||||||
<span className={session.id === selectedSession ? 'text-[#00f0ff]' : 'text-zinc-400'}>
|
|
||||||
<Hash className="h-4 w-4" />
|
|
||||||
</span>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className={`text-sm truncate max-w-[140px] ${
|
|
||||||
session.id === selectedSession ? 'text-white font-medium' : 'text-zinc-300'
|
|
||||||
}`}>
|
|
||||||
{session.title}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-zinc-500">
|
|
||||||
{session.message_count} 条消息
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-zinc-600 font-mono">
|
|
||||||
{index + 1}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -497,8 +497,6 @@ export function useChat(): UseChatReturn {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleServerMessage = useCallback((message: WsOutbound) => {
|
const handleServerMessage = useCallback((message: WsOutbound) => {
|
||||||
console.log('Received message:', message)
|
|
||||||
|
|
||||||
// Route to scheduler job view if active
|
// Route to scheduler job view if active
|
||||||
const currentSchedulerView = schedulerViewRef.current
|
const currentSchedulerView = schedulerViewRef.current
|
||||||
if (currentSchedulerView) {
|
if (currentSchedulerView) {
|
||||||
@ -639,33 +637,27 @@ export function useChat(): UseChatReturn {
|
|||||||
case 'session_established': {
|
case 'session_established': {
|
||||||
const msg = message as SessionEstablished
|
const msg = message as SessionEstablished
|
||||||
setConnectionId(msg.session_id)
|
setConnectionId(msg.session_id)
|
||||||
console.log('Connection established:', msg.session_id)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'task_started': {
|
case 'task_started': {
|
||||||
const msg = message as TaskStarted
|
const msg = message as TaskStarted
|
||||||
console.log('[useChat] task_started received:', { task_id: msg.task_id, topic_id: msg.topic_id, parent_task_id: msg.parent_task_id, selectedTopic: selectedTopicRef.current })
|
|
||||||
// 只 backfill 当前话题的 task tool_call,避免跨话题串扰
|
// 只 backfill 当前话题的 task tool_call,避免跨话题串扰
|
||||||
if (msg.topic_id && msg.topic_id !== selectedTopicRef.current) {
|
if (msg.topic_id && msg.topic_id !== selectedTopicRef.current) {
|
||||||
console.log('[useChat] task_started filtered by topic_id')
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
// 孙智能体的 TaskStarted 不应 backfill 到主视图
|
// 孙智能体的 TaskStarted 不应 backfill 到主视图
|
||||||
if (msg.parent_task_id) {
|
if (msg.parent_task_id) {
|
||||||
console.log('[useChat] task_started filtered by parent_task_id')
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置 navigateToTaskId,让用户可以点击查看实时进度
|
// 设置 navigateToTaskId,让用户可以点击查看实时进度
|
||||||
setMessages((prev) => {
|
setMessages((prev) => {
|
||||||
console.log('[useChat] task_started searching messages for task tool_call, total messages:', prev.length, 'tool_call_id:', msg.tool_call_id)
|
|
||||||
// 优先:按 tool_call_id 精确匹配
|
// 优先:按 tool_call_id 精确匹配
|
||||||
if (msg.tool_call_id) {
|
if (msg.tool_call_id) {
|
||||||
const idx = prev.findIndex(m =>
|
const idx = prev.findIndex(m =>
|
||||||
m.toolCallId === msg.tool_call_id && m.type === 'tool_call' && m.toolName === 'task')
|
m.toolCallId === msg.tool_call_id && m.type === 'tool_call' && m.toolName === 'task')
|
||||||
if (idx >= 0 && !prev[idx].navigateToTaskId) {
|
if (idx >= 0 && !prev[idx].navigateToTaskId) {
|
||||||
console.log('[useChat] task_started EXACT MATCH at index', idx, 'task_id:', msg.task_id)
|
|
||||||
const updated = [...prev]
|
const updated = [...prev]
|
||||||
updated[idx] = { ...updated[idx], navigateToTaskId: msg.task_id }
|
updated[idx] = { ...updated[idx], navigateToTaskId: msg.task_id }
|
||||||
return updated
|
return updated
|
||||||
@ -674,13 +666,11 @@ export function useChat(): UseChatReturn {
|
|||||||
// 回退:backward-search (兼容无 tool_call_id 的旧版本)
|
// 回退:backward-search (兼容无 tool_call_id 的旧版本)
|
||||||
for (let i = prev.length - 1; i >= 0; i--) {
|
for (let i = prev.length - 1; i >= 0; i--) {
|
||||||
if (prev[i].type === 'tool_call' && prev[i].toolName === 'task' && !prev[i].navigateToTaskId) {
|
if (prev[i].type === 'tool_call' && prev[i].toolName === 'task' && !prev[i].navigateToTaskId) {
|
||||||
console.log('[useChat] task_started BACKWARD MATCH at index', i, 'task_id:', msg.task_id)
|
|
||||||
const updated = [...prev]
|
const updated = [...prev]
|
||||||
updated[i] = { ...updated[i], navigateToTaskId: msg.task_id }
|
updated[i] = { ...updated[i], navigateToTaskId: msg.task_id }
|
||||||
return updated
|
return updated
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('[useChat] task_started NO matching task tool_call found in messages')
|
|
||||||
return prev
|
return prev
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
@ -688,8 +678,6 @@ export function useChat(): UseChatReturn {
|
|||||||
|
|
||||||
case 'session_list': {
|
case 'session_list': {
|
||||||
const msg = message as SessionList
|
const msg = message as SessionList
|
||||||
console.log('Session list received:', msg)
|
|
||||||
|
|
||||||
// 清空旧数据(切换通道时避免数据污染)
|
// 清空旧数据(切换通道时避免数据污染)
|
||||||
setTopics([])
|
setTopics([])
|
||||||
setSelectedTopic(null)
|
setSelectedTopic(null)
|
||||||
@ -723,8 +711,6 @@ export function useChat(): UseChatReturn {
|
|||||||
|
|
||||||
case 'topic_list': {
|
case 'topic_list': {
|
||||||
const msg = message as TopicList
|
const msg = message as TopicList
|
||||||
console.log('Topic list received:', msg)
|
|
||||||
|
|
||||||
// 转换 topics 格式
|
// 转换 topics 格式
|
||||||
const newTopics: Topic[] = msg.topics.map((t: TopicSummary) => ({
|
const newTopics: Topic[] = msg.topics.map((t: TopicSummary) => ({
|
||||||
id: t.topic_id,
|
id: t.topic_id,
|
||||||
@ -955,7 +941,6 @@ export function useChat(): UseChatReturn {
|
|||||||
|
|
||||||
case 'channel_list': {
|
case 'channel_list': {
|
||||||
const msg = message as ChannelList
|
const msg = message as ChannelList
|
||||||
console.log('Channel list received:', msg)
|
|
||||||
setChannels(msg.channels)
|
setChannels(msg.channels)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@ -114,7 +114,6 @@ export function useWebSocket({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 首次挂载,或者 url 发生了变化,都要重连
|
// 首次挂载,或者 url 发生了变化,都要重连
|
||||||
if (prevUrlRef.current !== url) {
|
if (prevUrlRef.current !== url) {
|
||||||
console.log(`WebSocket URL changed: ${prevUrlRef.current} → ${url}`)
|
|
||||||
// 先断开旧连接
|
// 先断开旧连接
|
||||||
disconnect()
|
disconnect()
|
||||||
prevUrlRef.current = url
|
prevUrlRef.current = url
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user