// 长任务(例如 Agent 生成报表)的前端状态机:用 reducer 管理,所有状态转换集中在一处,便于测试。 // 自检:node taskReducer-demo.ts export type TaskStatus = "queued" | "running" | "waiting_input" | "succeeded" | "failed" | "cancelled"; export interface TaskState { id: string | null; status: TaskStatus | "idle"; steps: { name: string; done: boolean }[]; result: { title: string; url: string } | null; error: string | null; version: number; // 服务端事件的序号:丢弃过期 / 乱序的事件 } export type TaskEvent = | { type: "submitted"; id: string } | { type: "progress"; seq: number; step: string; done: boolean } | { type: "need_input"; seq: number; question: string } | { type: "succeeded"; seq: number; result: { title: string; url: string } } | { type: "failed"; seq: number; error: string; retryable: boolean } | { type: "cancel" } | { type: "retry" }; export const initialState: TaskState = { id: null, status: "idle", steps: [], result: null, error: null, version: 0 }; const FINAL: ReadonlySet = new Set(["succeeded", "failed", "cancelled"]); export function taskReducer(s: TaskState, e: TaskEvent): TaskState { switch (e.type) { case "submitted": return { ...initialState, id: e.id, status: "queued" }; case "cancel": return FINAL.has(s.status) ? s : { ...s, status: "cancelled" }; case "retry": return s.status === "failed" ? { ...s, status: "queued", error: null } : s; default: // 服务端事件:终态之后、或者序号不大于当前版本的事件一律丢弃(重连后可能收到重复事件) if (FINAL.has(s.status) || e.seq <= s.version) return s; } switch (e.type) { case "progress": { const steps = s.steps.some((x) => x.name === e.step) ? s.steps.map((x) => (x.name === e.step ? { ...x, done: e.done } : x)) : [...s.steps, { name: e.step, done: e.done }]; return { ...s, status: "running", steps, version: e.seq }; } case "need_input": return { ...s, status: "waiting_input", version: e.seq }; case "succeeded": return { ...s, status: "succeeded", result: e.result, version: e.seq }; case "failed": return { ...s, status: "failed", error: e.retryable ? `${e.error}(可重试)` : e.error, version: e.seq }; } }