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:
oudecheng 2026-07-07 17:45:07 +08:00
parent f264a7b307
commit 7eb2933ca5
7 changed files with 13 additions and 260 deletions

View File

@ -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,

View File

@ -14,6 +14,8 @@ pub enum TaskSessionState {
Failed, Failed,
/// 已超时 /// 已超时
Timeout, Timeout,
/// 状态未知(如重启后从 DB 重建时无法可靠推断原状态)
Unknown,
} }
impl Default for TaskSessionState { impl Default for TaskSessionState {

View File

@ -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' :

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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
} }

View File

@ -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