报告创建请求、校正入口和同名类型改从 contracts 推导;同义的对象判断与 sha256 改走共享函数。调用处用 import 别名保留原名,使 diff 低于 1500 行。行为、哈希和状态字符串不变。 Co-Authored-By: Grok (agent loop)
116 lines
4.8 KiB
TypeScript
116 lines
4.8 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.
|
|
*/
|
|
|
|
import {
|
|
RESUMABLE_CASE_STATUSES,
|
|
TERMINAL_CASE_STATUSES,
|
|
type OpenRectificationCaseResponseWire,
|
|
type OpenRectificationDisposition,
|
|
type RectificationEntrySummaryWire,
|
|
} from "@/lib/contracts/rectification-entry";
|
|
|
|
export type { OpenRectificationDisposition };
|
|
export type RectificationEntrySummary = RectificationEntrySummaryWire;
|
|
export type OpenRectificationCaseResponse = OpenRectificationCaseResponseWire;
|
|
|
|
const resumableStatuses = new Set<string>(RESUMABLE_CASE_STATUSES);
|
|
const terminalStatuses = new Set<string>(TERMINAL_CASE_STATUSES);
|
|
|
|
export function isResumableRectificationStatus(status: string): boolean {
|
|
return resumableStatuses.has(status);
|
|
}
|
|
|
|
export function isTerminalRectificationStatus(status: string): boolean {
|
|
return terminalStatuses.has(status);
|
|
}
|
|
|
|
/* `GET /api/rectification/cases/entry-summary` answers with the camelCase
|
|
object `getRectificationEntrySummary` returns; only the RPC underneath speaks
|
|
snake_case. This parser used to read the snake_case keys alone, so every real
|
|
response parsed as "nothing on record" and the homepage never saw a resumable
|
|
or finished case (BUG-1041). The route's camelCase shape wins; snake_case is
|
|
still read so an RPC-shaped payload does not silently collapse either. */
|
|
function field(row: Record<string, unknown>, camel: string, snake: string): unknown {
|
|
return camel in row ? row[camel] : row[snake];
|
|
}
|
|
|
|
function objectField(row: Record<string, unknown>, camel: string, snake: string): Record<string, unknown> | null {
|
|
const value = field(row, camel, snake);
|
|
return value && typeof value === "object" ? value as Record<string, unknown> : null;
|
|
}
|
|
|
|
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 = objectField(row, "latestResumable", "latest_resumable");
|
|
const latestTerminal = objectField(row, "latestTerminal", "latest_terminal");
|
|
const resumableCaseId = latestResumable ? field(latestResumable, "caseId", "case_id") : null;
|
|
const terminalCaseId = latestTerminal ? field(latestTerminal, "caseId", "case_id") : null;
|
|
return {
|
|
hasResumableCase: field(row, "hasResumableCase", "has_resumable_case") === true,
|
|
hasTerminalCaseWithTime: field(row, "hasTerminalCaseWithTime", "has_terminal_case_with_time") === true,
|
|
latestResumable: latestResumable && typeof resumableCaseId === "string"
|
|
? {
|
|
caseId: resumableCaseId,
|
|
status: String(latestResumable.status ?? ""),
|
|
lastActivityAt: String(field(latestResumable, "lastActivityAt", "last_activity_at") ?? ""),
|
|
}
|
|
: null,
|
|
latestTerminal: latestTerminal && typeof terminalCaseId === "string"
|
|
? {
|
|
caseId: terminalCaseId,
|
|
status: String(latestTerminal.status ?? ""),
|
|
hasUsableTime: field(latestTerminal, "hasUsableTime", "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,
|
|
sourceSessionId: string | null = null,
|
|
): { intent: RectificationEntryOpenIntent; requestId: string; sessionId?: string; sourceSessionId?: string } {
|
|
const requestId = globalThis.crypto?.randomUUID?.() ?? fallbackUuid();
|
|
if (intent === "session" && sessionId) {
|
|
return { intent, requestId, sessionId };
|
|
}
|
|
return { intent, requestId, ...(intent === "homepage" && sourceSessionId ? { sourceSessionId } : {}) };
|
|
}
|
|
|
|
function fallbackUuid(): string {
|
|
return "00000000-0000-4000-8000-000000000000";
|
|
}
|