// 长任务(jobs)轮询纯逻辑:把松散 `GET /jobs/{id}` 响应安全收窄成 JobView, // 并提供一个 setTimeout 轮询状态机的纯 reducer(C3.5 / ARCH §7.4)。 // schema 把 GET /jobs/{id} 标成 {[k]:unknown}(T0.3 裸端点),这里安全narrow。 // 后端 job 状态机(C3.5):queued → running → done|failed。 export type JobStatus = "queued" | "running" | "done" | "failed"; export interface JobView { id: string; kind: string; status: JobStatus; progress: number; result: Record | null; error: string | null; } function asString(v: unknown, fallback = ""): string { return typeof v === "string" ? v : fallback; } function asStatus(v: unknown): JobStatus { return v === "running" || v === "done" || v === "failed" || v === "queued" ? v : "queued"; } function asProgress(v: unknown): number { if (typeof v !== "number" || !Number.isFinite(v)) return 0; return Math.min(100, Math.max(0, Math.round(v))); } function asResult(v: unknown): Record | null { return typeof v === "object" && v !== null && !Array.isArray(v) ? (v as Record) : null; } // 把松散 GET /jobs/{id} 响应收窄成 JobView(缺字段给安全默认)。 export function narrowJob(raw: unknown): JobView { const dict = typeof raw === "object" && raw !== null ? (raw as Record) : {}; return { id: asString(dict["id"]), kind: asString(dict["kind"]), status: asStatus(dict["status"]), progress: asProgress(dict["progress"]), result: asResult(dict["result"]), error: typeof dict["error"] === "string" ? (dict["error"] as string) : null, }; } // 学文风 job 完成态的 result:{version, dims_count}(C3 扩 work 摘要)。 export interface StyleLearnResult { version: number | null; dimsCount: number | null; } export function styleLearnResult(job: JobView): StyleLearnResult { const r = job.result ?? {}; const version = typeof r["version"] === "number" ? r["version"] : null; const dimsCount = typeof r["dims_count"] === "number" ? r["dims_count"] : null; return { version, dimsCount }; } // 轮询状态机:polling(queued/running)→ done | error(done/failed)。 export type PollStatus = "polling" | "done" | "error"; export interface PollState { status: PollStatus; progress: number; job: JobView | null; error: string | null; } export const initialPollState: PollState = { status: "polling", progress: 0, job: null, error: null, }; export type PollAction = | { type: "tick"; job: JobView } | { type: "fail"; error: string }; // 纯 reducer:吃一次轮询结果,映射成轮询状态。 // done → 终态 done;failed → 终态 error(用 job.error 文案);其余 → 继续 polling。 export function reducePoll(state: PollState, action: PollAction): PollState { if (action.type === "fail") { return { ...state, status: "error", error: action.error }; } const { job } = action; if (job.status === "done") { return { status: "done", progress: 100, job, error: null }; } if (job.status === "failed") { return { status: "error", progress: job.progress, job, error: job.error ?? "任务失败", }; } return { status: "polling", progress: job.progress, job, error: null }; } // 是否终态(停止轮询)。 export function isTerminal(status: JobStatus): boolean { return status === "done" || status === "failed"; }