/** * A6 任务中心接口(唯一出处)。 * * 依据:packages/shared/src/schemas/tasks.ts、技术架构 §6.4(异步任务协议)。 * 约定:query 用 snake_case(C-01);提交必须带 `Idempotency-Key`,响应字段固定 * `{ taskId, estimatedItems, estimatedCost, estimatedSeconds }`;列表 `{ items, next_cursor }`。 * 所有响应 / 入参都用 shared Zod schema parse,契约漂移在请求层直接暴露。 */ import { type MediaTaskCancelResponse, mediaTaskCancelResponseSchema, type MediaTaskDetail, type MediaTaskDetailQuery, mediaTaskDetailQuerySchema, mediaTaskDetailSchema, type MediaTaskListQuery, mediaTaskListQuerySchema, type MediaTaskListResponse, mediaTaskListResponseSchema, type MediaTaskRetryResponse, mediaTaskRetryResponseSchema, type MediaTaskSubmitInput, type MediaTaskSubmitParsed, type MediaTaskSubmitResponse, mediaTaskSubmitResponseSchema, mediaTaskSubmitSchema, } from '@lingxi/shared'; import { request, streamRequest } from './client'; import { ENDPOINTS } from './endpoints'; function searchParams(entries: Record): string { const params = new URLSearchParams(); for (const [key, value] of Object.entries(entries)) { if (value !== undefined && value !== '') params.set(key, String(value)); } const search = params.toString(); return search === '' ? '' : `?${search}`; } /** 任务提交幂等键:8–128 位可见 ASCII(后端强校验);随机 UUID 足够且不落库敏感信息 */ export function createTaskIdempotencyKey(): string { if (typeof crypto.randomUUID === 'function') { return `task-${crypto.randomUUID()}`; } const random = Math.random().toString(36).slice(2); return `task-${Date.now().toString(36)}-${random}`; } /** 跨类型任务列表:类型 / 状态 / 客户 / 时间 / 我的 / 关键字 + 游标分页 */ export async function fetchMediaTasks( query: Partial = {}, ): Promise { const parsed = mediaTaskListQuerySchema.parse(query); const payload: unknown = await request( `${ENDPOINTS.mediaTasks}${searchParams({ type: parsed.type, status: parsed.status, customer_id: parsed.customer_id, created_from: parsed.created_from, created_to: parsed.created_to, mine: parsed.mine, keyword: parsed.keyword, cursor: parsed.cursor, limit: parsed.limit, })}`, ); return mediaTaskListResponseSchema.parse(payload); } /** 任务详情:摘要 + payload 快照 + 任务项首页(默认 100 条)+ itemsNextCursor */ export async function fetchMediaTaskDetail( taskId: string, query: Partial = {}, ): Promise { const parsed = mediaTaskDetailQuerySchema.parse(query); const payload: unknown = await request( `${ENDPOINTS.mediaTask(taskId)}${searchParams({ items_cursor: parsed.items_cursor })}`, ); return mediaTaskDetailSchema.parse(payload); } /** * 提交任务(M0 仅演练任务)。幂等键由调用方生成并在重试 / 双击时复用: * 同 key 同 body 24h 内返回同一任务,不同 body 返回 40902。 */ export async function submitMediaTask( input: MediaTaskSubmitInput, idempotencyKey: string, ): Promise { const body: MediaTaskSubmitParsed = mediaTaskSubmitSchema.parse(input); const payload: unknown = await request(ENDPOINTS.mediaTasks, { method: 'POST', body, headers: { 'Idempotency-Key': idempotencyKey }, }); return mediaTaskSubmitResponseSchema.parse(payload); } /** 取消任务:只影响未开始项,已执行 / 已成功项保留结果 */ export async function cancelMediaTask(taskId: string): Promise { const payload: unknown = await request(ENDPOINTS.mediaTaskCancel(taskId), { method: 'POST' }); return mediaTaskCancelResponseSchema.parse(payload); } /** 仅重试失败项:成功项不重复执行、不重复计费 */ export async function retryMediaTask(taskId: string): Promise { const payload: unknown = await request(ENDPOINTS.mediaTaskRetry(taskId), { method: 'POST' }); return mediaTaskRetryResponseSchema.parse(payload); } /** * 打开任务 SSE 事件流(fetch 流,支持 Authorization 头)。 * 断线续推用 `Last-Event-ID` 头(后端优先)或 `last_event_id` query;由调用方负责重连与降级。 */ export function openMediaTaskEvents( taskId: string, options: { lastEventId?: string; signal?: AbortSignal } = {}, ): Promise { const query = options.lastEventId === undefined ? '' : searchParams({ last_event_id: options.lastEventId }); return streamRequest(`${ENDPOINTS.mediaTaskEvents(taskId)}${query}`, { headers: { Accept: 'text/event-stream' }, ...(options.signal === undefined ? {} : { signal: options.signal }), }); }