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;
|
||||
|
||||
// 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 {
|
||||
id: task_id.to_string(),
|
||||
session_id,
|
||||
@ -158,9 +167,7 @@ fn reconstruct_task_from_db(
|
||||
parent_channel_name: record.channel_name.clone(),
|
||||
description,
|
||||
subagent_type,
|
||||
// TODO: DB 重建时无法可靠推断 task 状态,暂硬编码为 Completed。
|
||||
// 实际 failed/timeout 的子智能体会被错误显示为 completed,需独立跟进。
|
||||
state: TaskSessionState::Completed,
|
||||
state: TaskSessionState::Unknown,
|
||||
created_at: record.created_at,
|
||||
updated_at: now,
|
||||
summary: None,
|
||||
|
||||
@ -14,6 +14,8 @@ pub enum TaskSessionState {
|
||||
Failed,
|
||||
/// 已超时
|
||||
Timeout,
|
||||
/// 状态未知(如重启后从 DB 重建时无法可靠推断原状态)
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Default for TaskSessionState {
|
||||
|
||||
@ -753,6 +753,7 @@ function App() {
|
||||
level.status === 'timeout' ? '超时' :
|
||||
level.status === 'running' ? '执行中' :
|
||||
level.status === 'loading' ? '加载中...' :
|
||||
level.status === 'unknown' ? '未知' :
|
||||
level.status
|
||||
const statusColor =
|
||||
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) => {
|
||||
console.log('Received message:', message)
|
||||
|
||||
// Route to scheduler job view if active
|
||||
const currentSchedulerView = schedulerViewRef.current
|
||||
if (currentSchedulerView) {
|
||||
@ -639,33 +637,27 @@ export function useChat(): UseChatReturn {
|
||||
case 'session_established': {
|
||||
const msg = message as SessionEstablished
|
||||
setConnectionId(msg.session_id)
|
||||
console.log('Connection established:', msg.session_id)
|
||||
break
|
||||
}
|
||||
|
||||
case 'task_started': {
|
||||
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,避免跨话题串扰
|
||||
if (msg.topic_id && msg.topic_id !== selectedTopicRef.current) {
|
||||
console.log('[useChat] task_started filtered by topic_id')
|
||||
break
|
||||
}
|
||||
// 孙智能体的 TaskStarted 不应 backfill 到主视图
|
||||
if (msg.parent_task_id) {
|
||||
console.log('[useChat] task_started filtered by parent_task_id')
|
||||
break
|
||||
}
|
||||
|
||||
// 设置 navigateToTaskId,让用户可以点击查看实时进度
|
||||
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 精确匹配
|
||||
if (msg.tool_call_id) {
|
||||
const idx = prev.findIndex(m =>
|
||||
m.toolCallId === msg.tool_call_id && m.type === 'tool_call' && m.toolName === 'task')
|
||||
if (idx >= 0 && !prev[idx].navigateToTaskId) {
|
||||
console.log('[useChat] task_started EXACT MATCH at index', idx, 'task_id:', msg.task_id)
|
||||
const updated = [...prev]
|
||||
updated[idx] = { ...updated[idx], navigateToTaskId: msg.task_id }
|
||||
return updated
|
||||
@ -674,13 +666,11 @@ export function useChat(): UseChatReturn {
|
||||
// 回退:backward-search (兼容无 tool_call_id 的旧版本)
|
||||
for (let i = prev.length - 1; i >= 0; i--) {
|
||||
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]
|
||||
updated[i] = { ...updated[i], navigateToTaskId: msg.task_id }
|
||||
return updated
|
||||
}
|
||||
}
|
||||
console.log('[useChat] task_started NO matching task tool_call found in messages')
|
||||
return prev
|
||||
})
|
||||
break
|
||||
@ -688,8 +678,6 @@ export function useChat(): UseChatReturn {
|
||||
|
||||
case 'session_list': {
|
||||
const msg = message as SessionList
|
||||
console.log('Session list received:', msg)
|
||||
|
||||
// 清空旧数据(切换通道时避免数据污染)
|
||||
setTopics([])
|
||||
setSelectedTopic(null)
|
||||
@ -723,8 +711,6 @@ export function useChat(): UseChatReturn {
|
||||
|
||||
case 'topic_list': {
|
||||
const msg = message as TopicList
|
||||
console.log('Topic list received:', msg)
|
||||
|
||||
// 转换 topics 格式
|
||||
const newTopics: Topic[] = msg.topics.map((t: TopicSummary) => ({
|
||||
id: t.topic_id,
|
||||
@ -955,7 +941,6 @@ export function useChat(): UseChatReturn {
|
||||
|
||||
case 'channel_list': {
|
||||
const msg = message as ChannelList
|
||||
console.log('Channel list received:', msg)
|
||||
setChannels(msg.channels)
|
||||
break
|
||||
}
|
||||
|
||||
@ -114,7 +114,6 @@ export function useWebSocket({
|
||||
useEffect(() => {
|
||||
// 首次挂载,或者 url 发生了变化,都要重连
|
||||
if (prevUrlRef.current !== url) {
|
||||
console.log(`WebSocket URL changed: ${prevUrlRef.current} → ${url}`)
|
||||
// 先断开旧连接
|
||||
disconnect()
|
||||
prevUrlRef.current = url
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user