826 lines
24 KiB
TypeScript
826 lines
24 KiB
TypeScript
import { useState, useCallback, useEffect, useRef, useMemo } from 'react'
|
||
import type {
|
||
Command,
|
||
ChatMessage,
|
||
Topic,
|
||
WsOutbound,
|
||
AssistantResponse,
|
||
ToolCall,
|
||
ToolResult,
|
||
ToolPending,
|
||
SessionEstablished,
|
||
SessionList,
|
||
SessionSummary,
|
||
TopicList,
|
||
TopicSummary,
|
||
TaskMessagesLoaded,
|
||
TaskStarted,
|
||
Attachment,
|
||
MemorySummary,
|
||
MemoryList,
|
||
SkillSummary,
|
||
SkillList,
|
||
SchedulerJobList,
|
||
SchedulerJobSummary,
|
||
SchedulerJobSessionLookup,
|
||
Channel,
|
||
ChannelList,
|
||
} from '../types/protocol'
|
||
|
||
// 简化后的层级状态
|
||
interface UseChatReturn {
|
||
// 连接状态
|
||
connectionId: string | null
|
||
isConnected: boolean
|
||
|
||
// 简化的层级状态
|
||
sessions: SessionSummary[]
|
||
selectedSessionId: string | null
|
||
session: SessionSummary | null
|
||
sessionId: string | null
|
||
chatId: string
|
||
topics: Topic[]
|
||
selectedTopic: string | null
|
||
|
||
// 消息
|
||
messages: ChatMessage[]
|
||
isLoading: boolean
|
||
|
||
// 通道状态
|
||
channels: Channel[]
|
||
selectedChannel: string
|
||
isWritable: boolean
|
||
|
||
// 是否只读
|
||
isReadOnly: boolean
|
||
|
||
// 子智能体视图
|
||
subAgentView: SubAgentView | null
|
||
|
||
// 方法
|
||
handleMessage: (content: string, attachments?: Attachment[]) => void
|
||
handleCommand: (command: Command) => void
|
||
clearMessages: () => void
|
||
handleServerMessage: (message: WsOutbound) => void
|
||
|
||
// Topic 方法
|
||
selectTopic: (topicId: string) => void
|
||
createTopic: (title?: string) => Command
|
||
switchTopic: (topicId: string) => Command
|
||
deleteTopic: (topicId: string) => Command
|
||
|
||
// 初始化方法
|
||
requestSessionList: () => Command
|
||
requestTopicList: () => Command | null
|
||
topicRefreshTrigger: number
|
||
requestChannelList: () => Command
|
||
selectChannel: (channelId: string) => void
|
||
selectSession: (sessionId: string) => void
|
||
|
||
// 子智能体导航方法
|
||
enterSubAgentView: (taskId: string, description: string) => Command
|
||
exitSubAgentView: () => void
|
||
|
||
// 记忆状态
|
||
memories: MemorySummary[]
|
||
requestMemoryList: () => Command
|
||
createMemory: (namespace: string, key: string, content: string) => Command
|
||
updateMemory: (id: string, content: string) => Command
|
||
deleteMemory: (id: string) => Command
|
||
|
||
// 技能状态
|
||
skills: SkillSummary[]
|
||
requestSkillList: () => Command
|
||
|
||
// 定时任务状态
|
||
schedulerJobs: SchedulerJobSummary[]
|
||
sidebarTab: 'topics' | 'scheduler'
|
||
setSidebarTab: (tab: 'topics' | 'scheduler') => void
|
||
requestSchedulerJobList: () => Command
|
||
|
||
// 定时任务执行对话查看
|
||
schedulerView: SchedulerJobView | null
|
||
enterSchedulerJobView: (lookup: SchedulerJobSessionLookup, jobId: string, description: string) => Command
|
||
exitSchedulerJobView: () => void
|
||
|
||
// 停止当前 Agent 执行
|
||
handleStop: () => Command
|
||
}
|
||
|
||
interface SubAgentView {
|
||
taskId: string
|
||
description: string
|
||
subagentType: string
|
||
status: string
|
||
summary?: string
|
||
messages: ChatMessage[]
|
||
}
|
||
|
||
interface SchedulerJobView {
|
||
jobId: string
|
||
description: string
|
||
channel: string
|
||
chatId: string
|
||
messages: ChatMessage[]
|
||
}
|
||
|
||
const DEFAULT_CHAT_ID = 'default'
|
||
|
||
export function useChat(): UseChatReturn {
|
||
const [messages, setMessages] = useState<ChatMessage[]>([])
|
||
const [isLoading, setIsLoading] = useState(false)
|
||
|
||
// 简化的状态管理
|
||
const [connectionId, setConnectionId] = useState<string | null>(null)
|
||
const [topics, setTopics] = useState<Topic[]>([])
|
||
const [selectedTopic, setSelectedTopic] = useState<string | null>(null)
|
||
const [topicRefreshTrigger, setTopicRefreshTrigger] = useState(0)
|
||
const [sessions, setSessions] = useState<SessionSummary[]>([])
|
||
const [selectedSessionId, setSelectedSessionId] = useState<string | null>(null)
|
||
const [subAgentView, setSubAgentView] = useState<SubAgentView | null>(null)
|
||
const [memories, setMemories] = useState<MemorySummary[]>([])
|
||
const [skills, setSkills] = useState<SkillSummary[]>([])
|
||
const [schedulerJobs, setSchedulerJobs] = useState<SchedulerJobSummary[]>([])
|
||
const [sidebarTab, setSidebarTab] = useState<'topics' | 'scheduler'>('topics')
|
||
const [schedulerView, setSchedulerView] = useState<SchedulerJobView | null>(null)
|
||
const [channels, setChannels] = useState<Channel[]>([])
|
||
const [selectedChannel, setSelectedChannel] = useState<string>('websocket')
|
||
|
||
// Message ID generator
|
||
const messageIdCounter = useRef(0)
|
||
const generateMessageId = () => {
|
||
messageIdCounter.current += 1
|
||
return `msg_${Date.now()}_${messageIdCounter.current}`
|
||
}
|
||
|
||
// Ref to track subAgentView and schedulerView for use in callbacks
|
||
const subAgentViewRef = useRef<SubAgentView | null>(null)
|
||
const schedulerViewRef = useRef<SchedulerJobView | null>(null)
|
||
const topicsRef = useRef<Topic[]>([])
|
||
const selectedTopicRef = useRef<string | null>(null)
|
||
const pendingNewTopicRef = useRef(false)
|
||
|
||
const isConnected = useMemo(() => connectionId !== null, [connectionId])
|
||
const selectedSession = useMemo(
|
||
() => sessions.find(s => s.session_id === selectedSessionId) ?? null,
|
||
[sessions, selectedSessionId]
|
||
)
|
||
const sessionId = useMemo(() => selectedSession?.session_id ?? null, [selectedSession])
|
||
const chatId = useMemo(() => sessionId ?? DEFAULT_CHAT_ID, [sessionId])
|
||
const isWritable = useMemo(
|
||
() => channels.find(c => c.id === selectedChannel)?.isWritable ?? false,
|
||
[channels, selectedChannel]
|
||
)
|
||
|
||
// Extract subagent_task_id from a message if present
|
||
const getSubagentTaskId = (message: WsOutbound): string | undefined => {
|
||
if (message.type === 'tool_call' || message.type === 'tool_result'
|
||
|| message.type === 'tool_pending' || message.type === 'assistant_response') {
|
||
return (message as ToolCall | ToolResult | ToolPending | AssistantResponse).subagent_task_id
|
||
}
|
||
return undefined
|
||
}
|
||
|
||
// Convert a server message to ChatMessage (extracted from handleServerMessage logic)
|
||
const serverMessageToChatMessage = (message: WsOutbound): ChatMessage | null => {
|
||
switch (message.type) {
|
||
case 'assistant_response': {
|
||
const msg = message as AssistantResponse
|
||
const role = msg.role === 'user' || msg.role === 'tool' ? msg.role : 'assistant'
|
||
return {
|
||
id: msg.id,
|
||
role: role as ChatMessage['role'],
|
||
content: msg.content,
|
||
timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
|
||
type: 'message',
|
||
attachments: msg.attachments,
|
||
subagentTaskId: msg.subagent_task_id,
|
||
reasoningContent: msg.reasoning_content,
|
||
}
|
||
}
|
||
case 'tool_call': {
|
||
const msg = message as ToolCall
|
||
return {
|
||
id: msg.id,
|
||
role: 'tool',
|
||
content: msg.content,
|
||
timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
|
||
type: 'tool_call',
|
||
toolName: msg.tool_name,
|
||
toolCallId: msg.tool_call_id,
|
||
arguments: msg.arguments,
|
||
subagentTaskId: msg.subagent_task_id,
|
||
}
|
||
}
|
||
case 'tool_result': {
|
||
const msg = message as ToolResult
|
||
return {
|
||
id: msg.id,
|
||
role: 'tool',
|
||
content: msg.content,
|
||
timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
|
||
type: 'tool_result',
|
||
toolName: msg.tool_name,
|
||
toolCallId: msg.tool_call_id,
|
||
subagentTaskId: msg.subagent_task_id,
|
||
durationMs: msg.duration_ms,
|
||
}
|
||
}
|
||
case 'tool_pending': {
|
||
const msg = message as ToolPending
|
||
return {
|
||
id: msg.id,
|
||
role: 'tool',
|
||
content: `${msg.content}\n\n${msg.resume_hint}`,
|
||
timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
|
||
type: 'tool_pending',
|
||
toolName: msg.tool_name,
|
||
toolCallId: msg.tool_call_id,
|
||
subagentTaskId: msg.subagent_task_id,
|
||
}
|
||
}
|
||
case 'error': {
|
||
return {
|
||
id: generateMessageId(),
|
||
role: 'assistant',
|
||
content: `Error: ${message.message}`,
|
||
timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
|
||
type: 'message',
|
||
}
|
||
}
|
||
default:
|
||
return null
|
||
}
|
||
}
|
||
|
||
// Append a server message to the sub-agent view
|
||
const appendToSubAgentViewMessage = (message: WsOutbound) => {
|
||
const chatMsg = serverMessageToChatMessage(message)
|
||
if (chatMsg) {
|
||
setSubAgentView((prev) =>
|
||
prev
|
||
? { ...prev, messages: [...prev.messages, chatMsg] }
|
||
: prev
|
||
)
|
||
}
|
||
}
|
||
|
||
const handleServerMessage = useCallback((message: WsOutbound) => {
|
||
console.log('Received message:', message)
|
||
|
||
// Route to scheduler job view if active
|
||
const currentSchedulerView = schedulerViewRef.current
|
||
if (currentSchedulerView) {
|
||
// Route chat messages to the scheduler view
|
||
const chatMsg = serverMessageToChatMessage(message)
|
||
if (chatMsg) {
|
||
setSchedulerView((prev) =>
|
||
prev
|
||
? { ...prev, messages: [...prev.messages, chatMsg] }
|
||
: prev
|
||
)
|
||
return
|
||
}
|
||
// Non-chat messages (session_list, topic_list, etc.) fall through to main handler
|
||
}
|
||
|
||
// Route to sub-agent view if active
|
||
const currentSubAgentView = subAgentViewRef.current
|
||
if (currentSubAgentView) {
|
||
if (message.type === 'task_messages_loaded') {
|
||
const msg = message as TaskMessagesLoaded
|
||
setSubAgentView((prev) =>
|
||
prev
|
||
? {
|
||
...prev,
|
||
subagentType: msg.subagent_type,
|
||
status: msg.status,
|
||
summary: msg.summary,
|
||
}
|
||
: prev
|
||
)
|
||
return
|
||
}
|
||
|
||
// Only accept messages explicitly tagged with matching subagent_task_id.
|
||
// History messages are now tagged by the backend (send_task_messages),
|
||
// and live sub-agent messages are tagged by SubAgentEmitter.
|
||
const msgSubagentTaskId = getSubagentTaskId(message)
|
||
if (msgSubagentTaskId && msgSubagentTaskId === currentSubAgentView.taskId) {
|
||
appendToSubAgentViewMessage(message)
|
||
return
|
||
}
|
||
}
|
||
|
||
// In main view, skip sub-agent messages (they belong to sub-agent view).
|
||
// But use the task_id to associate with the running task tool card.
|
||
const msgSubagentTaskId = getSubagentTaskId(message)
|
||
if (msgSubagentTaskId) {
|
||
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: msgSubagentTaskId }
|
||
return updated
|
||
}
|
||
}
|
||
return prev
|
||
})
|
||
return
|
||
}
|
||
|
||
switch (message.type) {
|
||
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
|
||
// 立即更新对应的 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)
|
||
|
||
// 清空旧数据(切换通道时避免数据污染)
|
||
setTopics([])
|
||
setSelectedTopic(null)
|
||
setMessages([])
|
||
|
||
// 存储全部 session
|
||
setSessions(msg.sessions)
|
||
|
||
// 自动选中:优先保持当前选中,否则选第一个
|
||
setSelectedSessionId(prev => {
|
||
if (prev && msg.sessions.some(s => s.session_id === prev)) {
|
||
return prev
|
||
}
|
||
return msg.sessions.length > 0 ? msg.sessions[0].session_id : null
|
||
})
|
||
|
||
setIsLoading(false)
|
||
break
|
||
}
|
||
|
||
case 'session_created': {
|
||
// 创建新 Topic 后更新列表
|
||
setIsLoading(false)
|
||
break
|
||
}
|
||
|
||
case 'session_loaded': {
|
||
setIsLoading(false)
|
||
break
|
||
}
|
||
|
||
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,
|
||
session_id: t.session_id,
|
||
title: t.title,
|
||
description: t.description || undefined,
|
||
message_count: Number(t.message_count),
|
||
created_at: t.created_at,
|
||
updated_at: t.last_active_at,
|
||
}))
|
||
setTopics(newTopics)
|
||
|
||
// 新建话题后自动聚焦到新话题(列表按 last_active_at DESC 排序,第一个即最新)
|
||
if (pendingNewTopicRef.current) {
|
||
pendingNewTopicRef.current = false
|
||
if (newTopics.length > 0) {
|
||
setSelectedTopic(newTopics[0].id)
|
||
setMessages([])
|
||
}
|
||
}
|
||
|
||
setIsLoading(false)
|
||
break
|
||
}
|
||
|
||
case 'assistant_response': {
|
||
const msg = message as AssistantResponse
|
||
// 按 topic_id 隔离:如果消息属于其他话题则丢弃
|
||
if (msg.topic_id && msg.topic_id !== selectedTopicRef.current) return
|
||
const role = msg.role === 'user' || msg.role === 'tool' ? msg.role : 'assistant'
|
||
setMessages((prev) => [
|
||
...prev,
|
||
{
|
||
id: msg.id,
|
||
role,
|
||
content: msg.content,
|
||
timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
|
||
type: 'message',
|
||
attachments: msg.attachments,
|
||
reasoningContent: msg.reasoning_content,
|
||
},
|
||
])
|
||
setIsLoading(false)
|
||
|
||
// 当前话题无描述时,可能刚触发了异步生成,标记需要刷新
|
||
const currentTopic = topicsRef.current.find(t => t.id === selectedTopicRef.current)
|
||
if (currentTopic && !currentTopic.description) {
|
||
setTopicRefreshTrigger(n => n + 1)
|
||
}
|
||
break
|
||
}
|
||
|
||
case 'tool_call': {
|
||
const msg = message as ToolCall
|
||
// 按 topic_id 隔离:如果消息属于其他话题则丢弃
|
||
if (msg.topic_id && msg.topic_id !== selectedTopicRef.current) return
|
||
setMessages((prev) => [
|
||
...prev,
|
||
{
|
||
id: msg.id,
|
||
role: 'tool',
|
||
content: msg.content,
|
||
timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
|
||
type: 'tool_call',
|
||
toolName: msg.tool_name,
|
||
toolCallId: msg.tool_call_id,
|
||
arguments: msg.arguments,
|
||
subagentTaskId: msg.subagent_task_id,
|
||
},
|
||
])
|
||
break
|
||
}
|
||
|
||
case 'tool_result': {
|
||
const msg = message as ToolResult
|
||
// 按 topic_id 隔离:如果消息属于其他话题则丢弃
|
||
if (msg.topic_id && msg.topic_id !== selectedTopicRef.current) return
|
||
setMessages((prev) => [
|
||
...prev,
|
||
{
|
||
id: msg.id,
|
||
role: 'tool',
|
||
content: msg.content,
|
||
timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
|
||
type: 'tool_result',
|
||
toolName: msg.tool_name,
|
||
toolCallId: msg.tool_call_id,
|
||
subagentTaskId: msg.subagent_task_id,
|
||
durationMs: msg.duration_ms,
|
||
},
|
||
])
|
||
break
|
||
}
|
||
|
||
case 'tool_pending': {
|
||
const msg = message as ToolPending
|
||
// 按 topic_id 隔离:如果消息属于其他话题则丢弃
|
||
if (msg.topic_id && msg.topic_id !== selectedTopicRef.current) return
|
||
setMessages((prev) => [
|
||
...prev,
|
||
{
|
||
id: msg.id,
|
||
role: 'tool',
|
||
content: `${msg.content}\n\n${msg.resume_hint}`,
|
||
timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
|
||
type: 'tool_pending',
|
||
toolName: msg.tool_name,
|
||
toolCallId: msg.tool_call_id,
|
||
},
|
||
])
|
||
break
|
||
}
|
||
|
||
case 'execution_cancelled': {
|
||
setMessages((prev) => [
|
||
...prev,
|
||
{
|
||
id: generateMessageId(),
|
||
role: 'assistant',
|
||
content: (message as { type: 'execution_cancelled'; message: string }).message,
|
||
timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
|
||
type: 'message',
|
||
},
|
||
])
|
||
setIsLoading(false)
|
||
break
|
||
}
|
||
|
||
case 'error': {
|
||
setMessages((prev) => [
|
||
...prev,
|
||
{
|
||
id: generateMessageId(),
|
||
role: 'assistant',
|
||
content: `Error: ${message.message}`,
|
||
timestamp: (message as any).timestamp ?? Math.floor(Date.now() / 1000),
|
||
type: 'message',
|
||
},
|
||
])
|
||
setIsLoading(false)
|
||
break
|
||
}
|
||
|
||
case 'scheduler_job_list': {
|
||
const msg = message as SchedulerJobList
|
||
setSchedulerJobs(msg.jobs)
|
||
break
|
||
}
|
||
case 'memory_list': {
|
||
const msg = message as MemoryList
|
||
setMemories(msg.memories)
|
||
break
|
||
}
|
||
case 'skill_list': {
|
||
const msg = message as SkillList
|
||
setSkills(msg.skills)
|
||
break
|
||
}
|
||
|
||
case 'channel_list': {
|
||
const msg = message as ChannelList
|
||
console.log('Channel list received:', msg)
|
||
setChannels(msg.channels)
|
||
break
|
||
}
|
||
case 'pong':
|
||
// 忽略这些消息
|
||
break
|
||
}
|
||
}, [])
|
||
|
||
const handleMessage = useCallback((content: string, attachments?: Attachment[]) => {
|
||
setMessages((prev) => [
|
||
...prev,
|
||
{
|
||
id: generateMessageId(),
|
||
role: 'user',
|
||
content,
|
||
timestamp: Math.floor(Date.now() / 1000),
|
||
type: 'message',
|
||
attachments: attachments || [],
|
||
},
|
||
])
|
||
setIsLoading(true)
|
||
}, [])
|
||
|
||
const handleCommand = useCallback((command: Command) => {
|
||
switch (command.type) {
|
||
case 'create_session':
|
||
case 'switch_topic':
|
||
case 'load_topic':
|
||
case 'list_sessions':
|
||
case 'list_sessions_by_channel':
|
||
case 'delete_topic':
|
||
case 'list_topics':
|
||
setIsLoading(true)
|
||
break
|
||
}
|
||
}, [])
|
||
|
||
const clearMessages = useCallback(() => {
|
||
setMessages([])
|
||
}, [])
|
||
|
||
// Topic 操作方法
|
||
const selectTopic = useCallback((topicId: string) => {
|
||
setSelectedTopic(topicId)
|
||
setMessages([])
|
||
}, [])
|
||
|
||
const createTopic = useCallback((title?: string): Command => {
|
||
pendingNewTopicRef.current = true
|
||
return {
|
||
type: 'create_session',
|
||
title: title || `话题 ${new Date().toLocaleString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}`,
|
||
}
|
||
}, [])
|
||
|
||
const switchTopic = useCallback((topicId: string): Command => {
|
||
return {
|
||
type: 'switch_topic',
|
||
topic_id: topicId,
|
||
}
|
||
}, [])
|
||
|
||
const deleteTopic = useCallback((topicId: string): Command => {
|
||
return {
|
||
type: 'delete_topic',
|
||
topic_id: topicId,
|
||
}
|
||
}, [])
|
||
|
||
// 初始化方法
|
||
const requestSessionList = useCallback((): Command => {
|
||
return {
|
||
type: 'list_sessions_by_channel',
|
||
channel_name: selectedChannel,
|
||
include_archived: false,
|
||
}
|
||
}, [selectedChannel])
|
||
|
||
const requestChannelList = useCallback((): Command => {
|
||
return { type: 'list_channels' }
|
||
}, [])
|
||
|
||
const selectChannel = useCallback((channelId: string) => {
|
||
if (channelId === selectedChannel) return
|
||
setSelectedChannel(channelId)
|
||
setSessions([])
|
||
setSelectedSessionId(null)
|
||
setTopics([])
|
||
setSelectedTopic(null)
|
||
setMessages([])
|
||
setIsLoading(true)
|
||
}, [selectedChannel])
|
||
|
||
const selectSession = useCallback((sessionId: string) => {
|
||
if (sessionId === selectedSessionId) return
|
||
setSelectedSessionId(sessionId)
|
||
setTopics([])
|
||
setSelectedTopic(null)
|
||
setMessages([])
|
||
setIsLoading(true)
|
||
}, [selectedSessionId])
|
||
|
||
const requestTopicList = useCallback((): Command | null => {
|
||
if (!sessionId) return null
|
||
return {
|
||
type: 'list_topics',
|
||
session_id: sessionId,
|
||
}
|
||
}, [sessionId])
|
||
|
||
// Keep refs in sync with state
|
||
useEffect(() => {
|
||
subAgentViewRef.current = subAgentView
|
||
}, [subAgentView])
|
||
|
||
useEffect(() => {
|
||
schedulerViewRef.current = schedulerView
|
||
}, [schedulerView])
|
||
|
||
useEffect(() => {
|
||
topicsRef.current = topics
|
||
}, [topics])
|
||
|
||
useEffect(() => {
|
||
selectedTopicRef.current = selectedTopic
|
||
}, [selectedTopic])
|
||
|
||
const enterSubAgentView = useCallback((taskId: string, description: string): Command => {
|
||
const newView: SubAgentView = {
|
||
taskId,
|
||
description,
|
||
subagentType: '',
|
||
status: 'loading',
|
||
messages: [],
|
||
}
|
||
// Sync ref immediately so WebSocket response routing works correctly
|
||
subAgentViewRef.current = newView
|
||
setSubAgentView(newView)
|
||
return {
|
||
type: 'load_task_messages',
|
||
task_id: taskId,
|
||
}
|
||
}, [])
|
||
|
||
const exitSubAgentView = useCallback(() => {
|
||
subAgentViewRef.current = null
|
||
setSubAgentView(null)
|
||
}, [])
|
||
|
||
// 记忆方法
|
||
const requestMemoryList = useCallback((): Command => {
|
||
return { type: 'list_memories' }
|
||
}, [])
|
||
|
||
const createMemory = useCallback((namespace: string, key: string, content: string): Command => {
|
||
return { type: 'create_memory', namespace, key, content }
|
||
}, [])
|
||
|
||
const updateMemory = useCallback((id: string, content: string): Command => {
|
||
return { type: 'update_memory', id, content }
|
||
}, [])
|
||
|
||
const deleteMemory = useCallback((id: string): Command => {
|
||
return { type: 'delete_memory', id }
|
||
}, [])
|
||
|
||
const requestSkillList = useCallback((): Command => {
|
||
return { type: 'list_skills' }
|
||
}, [])
|
||
|
||
// 定时任务方法
|
||
const requestSchedulerJobList = useCallback((): Command => {
|
||
return { type: 'list_scheduler_jobs' }
|
||
}, [])
|
||
|
||
const enterSchedulerJobView = useCallback(
|
||
(lookup: SchedulerJobSessionLookup, jobId: string, description: string): Command => {
|
||
const newView: SchedulerJobView = {
|
||
jobId,
|
||
description,
|
||
channel: lookup.channel,
|
||
chatId: lookup.chat_id,
|
||
messages: [],
|
||
}
|
||
schedulerViewRef.current = newView
|
||
setSchedulerView(newView)
|
||
return {
|
||
type: 'load_chat_messages',
|
||
channel: lookup.channel,
|
||
chat_id: lookup.chat_id,
|
||
}
|
||
},
|
||
[]
|
||
)
|
||
|
||
const exitSchedulerJobView = useCallback(() => {
|
||
schedulerViewRef.current = null
|
||
setSchedulerView(null)
|
||
}, [])
|
||
|
||
const handleStop = useCallback((): Command => {
|
||
return { type: 'stop_execution' }
|
||
}, [])
|
||
|
||
// Memoize messages: sub-agent view > scheduler view > main
|
||
const resolvedMessages = useMemo(() => {
|
||
if (subAgentView) {
|
||
return subAgentView.messages
|
||
}
|
||
if (schedulerView) {
|
||
return schedulerView.messages
|
||
}
|
||
return messages
|
||
}, [subAgentView, schedulerView, messages])
|
||
|
||
// 只读状态由当前通道决定
|
||
const isReadOnly = !isWritable
|
||
|
||
return {
|
||
connectionId,
|
||
isConnected,
|
||
sessions,
|
||
selectedSessionId,
|
||
session: selectedSession,
|
||
sessionId,
|
||
chatId,
|
||
topics,
|
||
selectedTopic,
|
||
messages: resolvedMessages,
|
||
isLoading,
|
||
isReadOnly,
|
||
isWritable,
|
||
channels,
|
||
selectedChannel,
|
||
subAgentView,
|
||
handleMessage,
|
||
handleCommand,
|
||
clearMessages,
|
||
handleServerMessage,
|
||
selectTopic,
|
||
createTopic,
|
||
switchTopic,
|
||
deleteTopic,
|
||
requestSessionList,
|
||
requestTopicList,
|
||
topicRefreshTrigger,
|
||
requestChannelList,
|
||
selectChannel,
|
||
selectSession,
|
||
enterSubAgentView,
|
||
exitSubAgentView,
|
||
memories,
|
||
requestMemoryList,
|
||
createMemory,
|
||
updateMemory,
|
||
deleteMemory,
|
||
skills,
|
||
requestSkillList,
|
||
schedulerJobs,
|
||
sidebarTab,
|
||
setSidebarTab,
|
||
requestSchedulerJobList,
|
||
schedulerView,
|
||
enterSchedulerJobView,
|
||
exitSchedulerJobView,
|
||
handleStop,
|
||
}
|
||
}
|