/** * 任务中心共享契约(A6-BE-01 / A6-BE-03 / A6-BE-04) * * 依据:开发计划 §6.7 / §7.6;技术架构 §6.4(异步任务协议)/ §3.6(状态机)。 * 约定: * - body / 响应字段用 camelCase(C-01);列表 query 用 snake_case; * - 列表 `{ items, next_cursor }`,单资源不套 `data`(C-02); * - 时间字段一律 ISO 字符串(UTC),金额 `estimatedCost` 用 number(元,2 位小数); * - 提交响应字段固定为 `{ taskId, estimatedItems, estimatedCost, estimatedSeconds }`。 */ import { z } from 'zod'; import { TASK_EVENT_TYPES, TASK_ITEM_HARD_LIMIT, TASK_ITEM_STATUSES, TASK_STATUSES, TASK_TYPES, } from '../tasks/task-protocol.js'; import { isoDateTimeSchema, ulidSchema } from './common.js'; export const taskTypeSchema = z.enum(TASK_TYPES); export const taskStatusSchema = z.enum(TASK_STATUSES); export const taskItemStatusSchema = z.enum(TASK_ITEM_STATUSES); export const taskEventTypeSchema = z.enum(TASK_EVENT_TYPES); /** 演练任务模式:全部成功 / 混合失败 / 全部失败(worker 按此产出,便于验收状态机) */ export const mediaTaskDemoModeSchema = z.enum(['success', 'mixed', 'fail']); export type MediaTaskDemoMode = z.infer; /** * 提交任务入参(M0 仅演练任务)。 * `itemCount` 只做硬上限(TASK_ITEM_HARD_LIMIT),业务上限读系统配置 `task.demo.max_items`。 */ export const mediaTaskSubmitSchema = z .object({ type: z.enum(['demo'], { errorMap: () => ({ message: 'M0 仅支持演练任务' }) }), customerId: ulidSchema, itemCount: z.number().int().min(1).max(TASK_ITEM_HARD_LIMIT), mode: mediaTaskDemoModeSchema, note: z.string().trim().max(200).optional(), }) .strict(); export type MediaTaskSubmitInput = z.input; export type MediaTaskSubmitParsed = z.output; /** 提交响应:预估条数 / 成本 / 时长缺一不可(A6-BE-01 明确字段名) */ export const mediaTaskSubmitResponseSchema = z.object({ taskId: ulidSchema, estimatedItems: z.number().int().positive(), estimatedCost: z.number().nonnegative(), estimatedSeconds: z.number().int().nonnegative(), }); export type MediaTaskSubmitResponse = z.infer; /** 任务摘要(列表 / 详情共用;任务头不返回 payload 与大 JSON) */ export const mediaTaskSummarySchema = z.object({ id: ulidSchema, type: taskTypeSchema, status: taskStatusSchema, title: z.string().min(1).max(200), customerId: ulidSchema, customerName: z.string().min(1).max(128), itemTotal: z.number().int().nonnegative(), itemSucceeded: z.number().int().nonnegative(), itemFailed: z.number().int().nonnegative(), itemSkipped: z.number().int().nonnegative(), progress: z.number().int().min(0).max(100), estimatedItems: z.number().int().nonnegative(), estimatedCost: z.number().nonnegative(), estimatedSeconds: z.number().int().nonnegative(), createdBy: ulidSchema, createdByName: z.string().min(1).max(64), createdAt: isoDateTimeSchema, startedAt: isoDateTimeSchema.optional(), finishedAt: isoDateTimeSchema.optional(), }); export type MediaTaskSummary = z.infer; /** 任务项:失败行含 failureCode / failureMessage(失败原因必填,来自字典) */ export const mediaTaskItemSchema = z.object({ id: ulidSchema, seq: z.number().int().positive(), status: taskItemStatusSchema, failureCode: z.string().max(64).optional(), failureMessage: z.string().max(500).optional(), attempts: z.number().int().nonnegative(), startedAt: isoDateTimeSchema.optional(), finishedAt: isoDateTimeSchema.optional(), }); export type MediaTaskItem = z.infer; /** * 任务详情:摘要 + payload 快照 + 首屏 items(默认 100 条)+ 续拉游标。 * `itemsNextCursor` 为 null 表示没有更多任务项。 */ export const mediaTaskDetailSchema = mediaTaskSummarySchema.extend({ payload: z.record(z.unknown()), cancelRequestedAt: isoDateTimeSchema.optional(), lastErrorCode: z.string().max(32).optional(), lastErrorMessage: z.string().max(500).optional(), items: z.array(mediaTaskItemSchema), itemsNextCursor: z.string().nullable(), }); export type MediaTaskDetail = z.infer; /** 详情 query:items 续拉游标(首屏不传) */ export const mediaTaskDetailQuerySchema = z.object({ items_cursor: z.string().min(1).max(512).optional(), }); export type MediaTaskDetailQuery = z.infer; /** SSE query:`last_event_id` 与 `Last-Event-ID` 头等价(header 优先) */ export const mediaTaskSseQuerySchema = z.object({ last_event_id: z.string().min(1).max(32).optional(), }); export type MediaTaskSseQuery = z.infer; /** 列表筛选 + 游标分页(query 用 snake_case;稳定排序 createdAt DESC, id DESC) */ export const mediaTaskListQuerySchema = z.object({ type: taskTypeSchema.optional(), status: taskStatusSchema.optional(), customer_id: ulidSchema.optional(), created_from: isoDateTimeSchema.optional(), created_to: isoDateTimeSchema.optional(), mine: z.enum(['true', 'false']).optional(), keyword: z.string().trim().min(1).max(100).optional(), cursor: z.string().min(1).max(512).optional(), limit: z.coerce.number().int().min(1).max(100).default(20), }); export type MediaTaskListQuery = z.infer; export const mediaTaskListResponseSchema = z.object({ items: z.array(mediaTaskSummarySchema), next_cursor: z.string().nullable(), }); export type MediaTaskListResponse = z.infer; export const mediaTaskCancelResponseSchema = z.object({ taskId: ulidSchema, status: taskStatusSchema, }); export type MediaTaskCancelResponse = z.infer; export const mediaTaskRetryResponseSchema = z.object({ taskId: ulidSchema, status: taskStatusSchema, retriedItems: z.number().int().nonnegative(), }); export type MediaTaskRetryResponse = z.infer; // --------------------------------------------------------------------------- // SSE 事件载荷(apps/api 写入 task_events.payload_json,前端按 event 类型解析) // --------------------------------------------------------------------------- /** `item`:单个任务项状态变更 */ export const taskItemEventPayloadSchema = z.object({ taskId: ulidSchema, itemId: ulidSchema, seq: z.number().int().positive(), status: taskItemStatusSchema, failureCode: z.string().max(64).optional(), failureMessage: z.string().max(500).optional(), }); export type TaskItemEventPayload = z.infer; /** `progress`:任务头进度快照(进度落库,断线重连按 seq 续推) */ export const taskProgressEventPayloadSchema = z.object({ taskId: ulidSchema, status: taskStatusSchema, progress: z.number().int().min(0).max(100), itemTotal: z.number().int().nonnegative(), itemSucceeded: z.number().int().nonnegative(), itemFailed: z.number().int().nonnegative(), itemSkipped: z.number().int().nonnegative(), }); export type TaskProgressEventPayload = z.infer; /** `done`:任务收敛(待预览 / 部分失败 / 失败 / 已取消),SSE 随后关闭 */ export const taskDoneEventPayloadSchema = z.object({ taskId: ulidSchema, status: taskStatusSchema, progress: z.number().int().min(0).max(100), }); export type TaskDoneEventPayload = z.infer; /** `error`:任务级错误(错误码与统一错误码同源) */ export const taskErrorEventPayloadSchema = z.object({ code: z.number().int(), message: z.string().min(1).max(500), }); export type TaskErrorEventPayload = z.infer; /** SSE 事件元素(序列化单测与前端契约共用) */ export const taskSseEventSchema = z.object({ id: z.string().min(1), event: taskEventTypeSchema, data: z.unknown(), }); export type TaskSseEvent = z.infer;