Spaces:
Running
Running
| import { apiFetch } from '../api.js'; | |
| import { briefActivityLabel } from '../app-activity-labels.js'; | |
| import { | |
| upsertActivityMessage, | |
| upsertAssistantMessage, | |
| upsertStatusMessage | |
| } from '../app-message-state.js'; | |
| import { upsertSessionInProject } from '../app-core-utils.js'; | |
| import { DEFAULT_STATUS, canUseAppShellFromStatus, connectionStateFromStatus } from '../relay-status.js'; | |
| const MESSAGE_HANDLERS = { | |
| connected: handleConnectionStatus, | |
| 'relay-status': handleConnectionStatus, | |
| 'chat-started': handleChatStarted, | |
| 'thread-started': handleThreadStarted, | |
| 'message-deleted': handleMessageDeleted, | |
| 'user-message': handleUserMessage, | |
| 'assistant-update': handleAssistantUpdate, | |
| 'status-update': handleStatusUpdate, | |
| 'activity-update': handleActivityUpdate, | |
| 'chat-complete': handleTerminalChatEvent, | |
| 'chat-error': handleTerminalChatEvent, | |
| 'chat-aborted': handleTerminalChatEvent, | |
| 'sync-complete': handleSyncComplete | |
| }; | |
| export function handleSocketMessage({ app, runRegistry, turnRefresh, event }) { | |
| let payload; | |
| try { | |
| payload = JSON.parse(event.data); | |
| } catch { | |
| console.error('Invalid websocket payload'); | |
| return; | |
| } | |
| const handler = MESSAGE_HANDLERS[payload.type]; | |
| if (handler) { | |
| handler({ app, runRegistry, turnRefresh, payload }); | |
| } | |
| } | |
| function handleConnectionStatus({ app, runRegistry, payload }) { | |
| const { type, ...relayStatus } = payload; | |
| const nextStatus = payload.type === 'connected' ? payload.status || DEFAULT_STATUS : relayStatus; | |
| app.setStatus(nextStatus); | |
| app.setAuthenticated(canUseAppShellFromStatus(nextStatus)); | |
| app.setConnectionState(connectionStateFromStatus(nextStatus)); | |
| runRegistry.syncActiveRunsFromStatus(nextStatus); | |
| } | |
| function handleChatStarted({ app, runRegistry, payload }) { | |
| runRegistry.markRun(payload); | |
| if (!runRegistry.payloadMatchesCurrentConversation(payload)) { | |
| return; | |
| } | |
| if (!app.selectedSessionRef.current && payload.sessionId) { | |
| app.setSelectedSession({ id: payload.sessionId, projectId: payload.projectId, title: '新对话' }); | |
| } | |
| } | |
| function handleMessageDeleted({ app, runRegistry, payload }) { | |
| if (runRegistry.payloadMatchesCurrentConversation(payload)) { | |
| app.setMessages((current) => current.filter((message) => String(message.id) !== String(payload.messageId))); | |
| } | |
| } | |
| function handleActivityUpdate({ app, runRegistry, payload }) { | |
| if (payload.status === 'running' || payload.status === 'queued') { | |
| runRegistry.markRun(payload); | |
| } | |
| if (runRegistry.payloadMatchesCurrentConversation(payload)) { | |
| app.setMessages((current) => upsertActivityMessage(current, payload)); | |
| } | |
| } | |
| function handleSyncComplete({ app, payload }) { | |
| if (payload.projects) { | |
| app.setProjects(payload.projects); | |
| const project = app.selectedProjectRef.current; | |
| if (project?.id) { | |
| const projectId = project.id; | |
| const fetchSessions = app.apiFetch || apiFetch; | |
| fetchSessions(`/api/projects/${encodeURIComponent(project.id)}/sessions`) | |
| .then((data) => { | |
| if (app.selectedProjectRef.current?.id !== projectId) { | |
| return; | |
| } | |
| app.setSessionsByProject((current) => ({ ...current, [projectId]: data.sessions || [] })); | |
| }) | |
| .catch((error) => { | |
| console.warn(`[websocket] sync session refresh failed project=${projectId}:`, error.message || error); | |
| }); | |
| } | |
| } | |
| } | |
| function handleThreadStarted({ app, runRegistry, payload }) { | |
| if (!payload.sessionId) { | |
| return; | |
| } | |
| const projectId = payload.projectId || app.selectedProjectRef.current?.id || app.selectedSessionRef.current?.projectId; | |
| const currentSession = app.selectedSessionRef.current; | |
| const nextSession = { | |
| ...(currentSession || {}), | |
| id: payload.sessionId, | |
| projectId, | |
| title: currentSession?.title || '新对话', | |
| updatedAt: new Date().toISOString(), | |
| draft: false | |
| }; | |
| runRegistry.markRun(payload); | |
| app.setSelectedSession((current) => { | |
| if (!current) { | |
| return nextSession; | |
| } | |
| const shouldReplace = | |
| current.id === payload.previousSessionId || | |
| current.id === payload.sessionId || | |
| current.turnId === payload.turnId || | |
| (current.draft && current.projectId === projectId); | |
| return shouldReplace ? { ...current, ...nextSession } : current; | |
| }); | |
| app.setSessionsByProject((current) => | |
| upsertSessionInProject(current, projectId, nextSession, payload.previousSessionId) | |
| ); | |
| app.setMessages((current) => | |
| current.map((message) => | |
| message.turnId === payload.turnId || message.sessionId === payload.previousSessionId | |
| ? { ...message, sessionId: payload.sessionId } | |
| : message | |
| ) | |
| ); | |
| } | |
| function handleUserMessage({ app, runRegistry, payload }) { | |
| if (!runRegistry.payloadMatchesCurrentConversation(payload)) { | |
| return; | |
| } | |
| app.setMessages((current) => { | |
| const incoming = { | |
| ...payload.message, | |
| sessionId: payload.message?.sessionId || payload.sessionId || null, | |
| turnId: payload.message?.turnId || payload.turnId || null | |
| }; | |
| const alreadyShown = current.some((message) => { | |
| if (message.role !== 'user') { | |
| return false; | |
| } | |
| if (incoming.id && message.id === incoming.id) { | |
| return true; | |
| } | |
| return Boolean(incoming.turnId && message.turnId === incoming.turnId); | |
| }); | |
| return alreadyShown ? current : [...current, incoming]; | |
| }); | |
| } | |
| function handleAssistantUpdate({ app, runRegistry, payload }) { | |
| if (!payload.content?.trim()) { | |
| return; | |
| } | |
| runRegistry.markRun(payload); | |
| if (!runRegistry.payloadMatchesCurrentConversation(payload)) { | |
| return; | |
| } | |
| if (payload.phase === 'commentary') { | |
| app.setMessages((current) => | |
| upsertStatusMessage(current, { | |
| ...payload, | |
| kind: payload.kind || 'agent_message', | |
| label: briefActivityLabel(payload.content), | |
| status: payload.status || 'running' | |
| }) | |
| ); | |
| return; | |
| } | |
| if (payload.kind === 'agent_message') { | |
| app.setMessages((current) => upsertAssistantMessage(current, payload)); | |
| return; | |
| } | |
| if (!payload.done) { | |
| app.setMessages((current) => | |
| upsertStatusMessage(current, { | |
| ...payload, | |
| kind: payload.kind || 'message', | |
| label: '正在整理回复', | |
| status: payload.status || 'running' | |
| }) | |
| ); | |
| return; | |
| } | |
| app.setMessages((current) => upsertAssistantMessage(current, payload)); | |
| } | |
| function handleStatusUpdate({ app, runRegistry, turnRefresh, payload }) { | |
| if (payload.status === 'running' || payload.status === 'queued') { | |
| runRegistry.markRun(payload); | |
| } | |
| if (!runRegistry.payloadMatchesCurrentConversation(payload)) { | |
| return; | |
| } | |
| if (payload.kind === 'turn' && payload.status === 'completed') { | |
| turnRefresh.markTurnCompleted({ | |
| ...payload, | |
| allowLatestAssistantFallback: !payload.messageId | |
| }); | |
| return; | |
| } | |
| app.setMessages((current) => upsertStatusMessage(current, payload)); | |
| } | |
| function handleTerminalChatEvent({ app, runRegistry, turnRefresh, payload }) { | |
| if (!runRegistry.payloadMatchesCurrentConversation(payload)) { | |
| runRegistry.clearRun(payload); | |
| return; | |
| } | |
| if (payload.type === 'chat-complete') { | |
| const terminalPayload = { | |
| ...payload, | |
| status: 'completed', | |
| allowLatestAssistantFallback: !payload.messageId | |
| }; | |
| turnRefresh.markTurnCompleted(terminalPayload); | |
| turnRefresh.scheduleTurnRefresh(terminalPayload); | |
| return; | |
| } | |
| runRegistry.clearRun(payload); | |
| if (payload.type === 'chat-error' && payload.error) { | |
| app.setMessages((current) => | |
| upsertStatusMessage(current, { | |
| ...payload, | |
| kind: 'turn', | |
| status: 'failed', | |
| label: '任务失败', | |
| detail: payload.error | |
| }) | |
| ); | |
| } else if (payload.type === 'chat-aborted') { | |
| app.setMessages((current) => | |
| upsertStatusMessage(current, { | |
| ...payload, | |
| kind: 'turn', | |
| status: 'completed', | |
| label: '已中止' | |
| }) | |
| ); | |
| } | |
| } | |