143 lines
4.4 KiB
TypeScript
143 lines
4.4 KiB
TypeScript
/**
|
|
* 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<Record<RectificationCardAction, string>> = {
|
|
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<string, unknown>;
|
|
const latestResumable = row.latest_resumable && typeof row.latest_resumable === "object"
|
|
? row.latest_resumable as Record<string, unknown>
|
|
: null;
|
|
const latestTerminal = row.latest_terminal && typeof row.latest_terminal === "object"
|
|
? row.latest_terminal as Record<string, unknown>
|
|
: 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<string, unknown>;
|
|
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";
|
|
}
|