Spaces:
Running
Running
| import { useEffect, useRef } from 'react'; | |
| import { apiFetch } from '../api.js'; | |
| import { hasAssistantResultForTurn, upsertSessionInProject } from '../app-core-utils.js'; | |
| import { upsertAssistantMessage, upsertStatusMessage } from '../app-message-state.js'; | |
| const TURN_POLL_INTERVAL_MS = 1400; | |
| const TURN_POLL_TIMEOUT_MS = 30 * 60 * 1000; | |
| export function useTurnPolling(app, runRegistry, turnRefresh) { | |
| const stoppedRef = useRef(false); | |
| useEffect(() => { | |
| stoppedRef.current = false; | |
| return () => { | |
| stoppedRef.current = true; | |
| }; | |
| }, []); | |
| function turnMatchesCurrentSelection(turnId, optimisticSessionId, realSessionId, previousSessionId) { | |
| const current = app.selectedSessionRef.current; | |
| if (!current) { | |
| return true; | |
| } | |
| return ( | |
| current.id === optimisticSessionId || | |
| current.id === realSessionId || | |
| current.id === previousSessionId || | |
| current.turnId === turnId || | |
| current.draft | |
| ); | |
| } | |
| function applyTurnSession(turn, optimisticSessionId, projectId, previousSessionId) { | |
| const sessionIdText = String(turn.sessionId || ''); | |
| const realSessionId = | |
| sessionIdText && !sessionIdText.startsWith('draft-') && !sessionIdText.startsWith('codex-') | |
| ? sessionIdText | |
| : null; | |
| if (!realSessionId) { | |
| return null; | |
| } | |
| const currentSession = app.selectedSessionRef.current; | |
| const nextSession = { | |
| ...(currentSession || {}), | |
| id: realSessionId, | |
| projectId, | |
| title: currentSession?.title || '新对话', | |
| updatedAt: turn.completedAt || turn.updatedAt || new Date().toISOString(), | |
| draft: false | |
| }; | |
| app.setSelectedSession((current) => { | |
| if (!current) { | |
| return nextSession; | |
| } | |
| if (!turnMatchesCurrentSelection(turn.turnId, optimisticSessionId, realSessionId, previousSessionId)) { | |
| return current; | |
| } | |
| return { ...current, ...nextSession }; | |
| }); | |
| app.setSessionsByProject((current) => | |
| upsertSessionInProject(current, projectId, nextSession, previousSessionId || optimisticSessionId) | |
| ); | |
| app.setMessages((current) => | |
| current.map((message) => | |
| message.turnId === turn.turnId || | |
| message.sessionId === optimisticSessionId || | |
| message.sessionId === previousSessionId | |
| ? { ...message, sessionId: realSessionId } | |
| : message | |
| ) | |
| ); | |
| return realSessionId; | |
| } | |
| const loadTurnMessages = async ({ | |
| realSessionId, | |
| turnId, | |
| messageId, | |
| optimisticSessionId, | |
| previousSessionId, | |
| hadAssistantText = false | |
| }) => { | |
| if (!realSessionId) { | |
| return false; | |
| } | |
| const current = app.selectedSessionRef.current; | |
| if ( | |
| current && | |
| current.id !== realSessionId && | |
| current.id !== optimisticSessionId && | |
| current.id !== previousSessionId && | |
| current.turnId !== turnId | |
| ) { | |
| return false; | |
| } | |
| const data = await apiFetch(`/api/sessions/${encodeURIComponent(realSessionId)}/messages?limit=120`); | |
| if ( | |
| data.messages?.length && | |
| hasAssistantResultForTurn(data.messages, { | |
| turnId, | |
| messageId, | |
| hadAssistantText, | |
| status: 'completed', | |
| allowLatestAssistantFallback: !messageId | |
| }) | |
| ) { | |
| app.setMessages(data.messages); | |
| return true; | |
| } | |
| return false; | |
| }; | |
| const pollTurnUntilComplete = async ({ turnId, optimisticSessionId, projectId, previousSessionId }) => { | |
| if (stoppedRef.current || !turnId || app.activePollsRef.current.has(turnId)) { | |
| return; | |
| } | |
| app.activePollsRef.current.add(turnId); | |
| const startedAt = Date.now(); | |
| try { | |
| while (Date.now() - startedAt < TURN_POLL_TIMEOUT_MS) { | |
| await new Promise((resolve) => window.setTimeout(resolve, TURN_POLL_INTERVAL_MS)); | |
| if (stoppedRef.current) { | |
| break; | |
| } | |
| let turn = null; | |
| try { | |
| const result = await apiFetch(`/api/chat/turns/${encodeURIComponent(turnId)}`); | |
| turn = result.turn; | |
| } catch { | |
| continue; | |
| } | |
| if (!turn) { | |
| continue; | |
| } | |
| if (stoppedRef.current) { | |
| break; | |
| } | |
| const realSessionId = applyTurnSession(turn, optimisticSessionId, projectId, previousSessionId); | |
| if (turn.status === 'failed') { | |
| runRegistry.clearRun({ turnId, sessionId: realSessionId || optimisticSessionId, previousSessionId }); | |
| app.setMessages((current) => | |
| upsertStatusMessage(current, { | |
| sessionId: realSessionId || optimisticSessionId, | |
| turnId, | |
| kind: 'turn', | |
| status: 'failed', | |
| label: '任务失败', | |
| detail: turn.error || turn.detail || '任务失败' | |
| }) | |
| ); | |
| break; | |
| } | |
| if (turn.status === 'aborted') { | |
| runRegistry.clearRun({ turnId, sessionId: realSessionId || optimisticSessionId, previousSessionId }); | |
| app.setMessages((current) => | |
| upsertStatusMessage(current, { | |
| sessionId: realSessionId || optimisticSessionId, | |
| turnId, | |
| kind: 'turn', | |
| status: 'completed', | |
| label: '已中止' | |
| }) | |
| ); | |
| break; | |
| } | |
| if (turn.status === 'completed') { | |
| const terminalPayload = { sessionId: realSessionId || optimisticSessionId, turnId, previousSessionId }; | |
| turnRefresh.markTurnCompleted({ ...terminalPayload, detail: turn.detail || '' }); | |
| const loaded = await loadTurnMessages({ | |
| realSessionId, | |
| turnId, | |
| messageId: turn.messageId || null, | |
| optimisticSessionId, | |
| previousSessionId, | |
| hadAssistantText: turn.hadAssistantText | |
| }); | |
| if (loaded) { | |
| runRegistry.clearRun(terminalPayload); | |
| } else if (turn.assistantPreview) { | |
| app.setMessages((current) => | |
| upsertAssistantMessage(current, { | |
| ...terminalPayload, | |
| preview: true, | |
| content: turn.assistantPreview | |
| }) | |
| ); | |
| turnRefresh.scheduleTurnRefresh({ | |
| ...terminalPayload, | |
| messageId: turn.messageId || null, | |
| hadAssistantText: true, | |
| allowLatestAssistantFallback: !turn.messageId, | |
| usage: turn.usage || null | |
| }); | |
| runRegistry.clearRun(terminalPayload); | |
| } else { | |
| turnRefresh.scheduleTurnRefresh({ | |
| ...terminalPayload, | |
| messageId: turn.messageId || null, | |
| allowLatestAssistantFallback: !turn.messageId, | |
| hadAssistantText: turn.hadAssistantText || Boolean(turn.assistantPreview), | |
| usage: turn.usage || null | |
| }); | |
| } | |
| break; | |
| } | |
| } | |
| } finally { | |
| app.activePollsRef.current.delete(turnId); | |
| } | |
| }; | |
| return { | |
| pollTurnUntilComplete | |
| }; | |
| } | |