codexmobile-relay / client /src /hooks /useTurnPolling.js
Codex
deploy: CodexMobile Relay
90f0300
Raw
History Blame Contribute Delete
7.03 kB
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
};
}