/** * Client-side rectification entry routing contracts. * * Every disposition decision comes from the server Case open API. The browser * never guesses "resume vs create" from session lists or message counts. */ export type RectificationEntrySummary = Readonly<{ hasResumableCase: boolean; hasTerminalCaseWithTime: boolean; latestResumable: Readonly<{ caseId: string; status: string; lastActivityAt: string; }> | null; latestTerminal: Readonly<{ caseId: string; status: string; hasUsableTime: boolean; }> | null; }>; export type RectificationCardAction = "start" | "restart"; export const rectificationEntryLabels: Readonly> = { start: "开始新的生时校正", restart: "再次校正", }; export function resolveRectificationEntryAction( summary: RectificationEntrySummary, ): RectificationCardAction { if (summary.hasTerminalCaseWithTime) return "restart"; return "start"; } export type OpenRectificationDisposition = "created" | "resumed" | "readonly"; export type OpenRectificationCaseResponse = Readonly<{ disposition: OpenRectificationDisposition; caseId: string; sessionId: string; status: string; shouldStartOpening: boolean; skillVersion: string; }>; const RESUMABLE_STATUSES = new Set([ "draft", "collecting_evidence", "candidate_ready", "candidate_accepted", "needs_rebaseline", "paused", ]); const TERMINAL_STATUSES = new Set([ "confirmed", "closed", "abandoned", "superseded", ]); export function isResumableRectificationStatus(status: string): boolean { return RESUMABLE_STATUSES.has(status); } export function isTerminalRectificationStatus(status: string): boolean { return TERMINAL_STATUSES.has(status); } export function entrySummaryFromResponse(value: unknown): RectificationEntrySummary { if (!value || typeof value !== "object") { return { hasResumableCase: false, hasTerminalCaseWithTime: false, latestResumable: null, latestTerminal: null }; } const row = value as Record; const latestResumable = row.latest_resumable && typeof row.latest_resumable === "object" ? row.latest_resumable as Record : null; const latestTerminal = row.latest_terminal && typeof row.latest_terminal === "object" ? row.latest_terminal as Record : null; return { hasResumableCase: row.has_resumable_case === true, hasTerminalCaseWithTime: row.has_terminal_case_with_time === true, latestResumable: latestResumable && typeof latestResumable.case_id === "string" ? { caseId: latestResumable.case_id, status: String(latestResumable.status ?? ""), lastActivityAt: String(latestResumable.last_activity_at ?? ""), } : null, latestTerminal: latestTerminal && typeof latestTerminal.case_id === "string" ? { caseId: latestTerminal.case_id, status: String(latestTerminal.status ?? ""), hasUsableTime: latestTerminal.has_usable_time === true, } : null, }; } export function openResponseFromPayload(value: unknown): OpenRectificationCaseResponse | null { if (!value || typeof value !== "object") return null; const row = value as Record; const disposition = row.disposition; const caseId = typeof row.caseId === "string" ? row.caseId : ""; const sessionId = typeof row.sessionId === "string" ? row.sessionId : ""; const status = typeof row.status === "string" ? row.status : ""; if ( (disposition !== "created" && disposition !== "resumed" && disposition !== "readonly") || !caseId || !sessionId || !status ) { return null; } return { disposition, caseId, sessionId, status, shouldStartOpening: row.shouldStartOpening === true, skillVersion: typeof row.skillVersion === "string" ? row.skillVersion : "", }; } export type RectificationEntryOpenIntent = "homepage" | "session" | "new"; /** Build the server open request body; the browser never adds business state. */ export function openRectificationRequestBody( intent: RectificationEntryOpenIntent, sessionId: string | null, ): { intent: RectificationEntryOpenIntent; requestId: string; sessionId?: string } { const requestId = globalThis.crypto?.randomUUID?.() ?? fallbackUuid(); if (intent === "session" && sessionId) { return { intent, requestId, sessionId }; } return { intent, requestId }; } function fallbackUuid(): string { return "00000000-0000-4000-8000-000000000000"; }