Files
lingxi/docs/开发计划/灵犀_A6任务中心与队列骨架_实施报告_V0.1.md
T
Stev_Wang 45467558ab
ci / 质量门禁 (push) Successful in 5m25s
docs(plan): A6 实施报告与计划回写
背景:A6 全部 WBS 与门禁完成,按文档规范同批更新实现记录与计划状态。
改动:
- 新增 docs/开发计划/灵犀_A6任务中心与队列骨架_实施报告_V0.1.md:范围与完成度、交付物、门禁结果、关键口径(202 / 幂等 / 取消重试 / SSE / 状态机 / 队列物理名 / Outbox 退避 / 前端预估 / 角标)、与计划差异、已知遗留与下一步。
- 开发计划:状态行与 A6 批次置为已完成、§6.7 增加执行状态与完成情况、§7.6 增加状态行、M0 清单勾选任务中心。
- README:索引新增 A6 报告,当前进度推进到 A7。
影响面:仅文档与索引;无代码行为变化。
回滚:撤销提交恢复文档。
验证:pnpm format:check 通过;报告中的门禁数据来自本批实际执行结果。
2026-11-05 11:41:26 +08:00

14 KiB
Raw Blame History

灵犀 A6 任务中心与队列骨架 · 实施报告 V0.1

项 内容
批次 A6 任务中心与队列骨架(开发计划 §6.7 / §7.6,架构 §3.6 / §3.7 / §6.4)
完成日期 2026-11-05
状态 ✅ 已完成(A6-BE-0105、A6-INF-01、A6-FE-0102、A6-QA-01 全部交付)
用户选定方案 前端采用 方案 A · 经典管理台(design/a6-ui-v1/a)
代码提交 4a29cf8(shared 契约与数据模型)、633765f(API / SSE / 队列 / worker)、A6 前端提交见仓库 feat(web)
本地依赖 PostgreSQL 18 / Redis 7.4;pnpm check:dev 通过

1 范围与完成度

编号 交付 状态
A6-BE-01 任务协议与九态状态机、media_tasks / media_items / task_events、提交预估、Idempotency-Key 24h ✅
A6-BE-02 BullMQ 5 九队列(5 组)、worker 按 QUEUE 启停、重试 / 退避 / 死信 / 并发上限、Redis 锁 ✅
A6-BE-03 SSE /media-tasks/{id}/events:心跳、Last-Event-ID 续推、进度落库、轮询兜底 ✅
A6-BE-04 跨类型任务列表 / 详情(客户 / 状态 / 类型 / 时间 / 我提交的 / 关键字)、取消、仅重试失败项、失败行号 ✅
A6-BE-05 outbox_events 事务写入 + dispatcher 至少一次投递 + jobId 幂等 + 死信 + 本地结果事件审计 ✅
A6-INF-01 /api/v1/health/queues、/api/v1/metrics(Prometheus 0.0.4)、4 条告警规则文件、worker 心跳 ✅(未接真实 Grafana,见 §5)
A6-FE-01 /material/tasks、/material/tasks/:id:列表 / 详情 / 提交演练任务 / 取消 / 仅重试失败项 / 三态 / 权限三态 ✅
A6-FE-02 可复用任务组件(类型 / 状态 / 进度条 / 进度环 / SSE 状态)、顶栏全局角标、待处理失败信号、完成 / 失败通知 ✅
A6-QA-01 幂等 / 取消 / 重试 / 断线续推 / 进程重启恢复 / 权限三态 / 浏览器 e2e ✅

本批门禁对照:提交 → SSE → 完成 / 失败闭环 ✅;幂等与取消生效 ✅;五个队列组独立启停 ✅;进度落库、刷新 / 重连 / 进程重启不丢 ✅。


2 交付物

2.1 共享契约(packages/shared)

  • src/tasks/task-protocol.ts(+ spec):任务类型、架构 §3.6 九态与允许迁移、5 组 9 队列定义(并发 / 重试 / 退避)、Outbox 事件、SSE 事件、进度与收敛纯函数。
  • src/schemas/tasks.ts(+ spec):提交 / 响应 / 摘要 / 详情 / 列表 query+response / 取消 / 重试 / SSE 事件载荷;body camelCase、query snake_case、列表 { items, next_cursor }。
  • src/config/system-configs.ts:新增 task 分组 3 项(task.demo.max_items=20 / task.estimate.demo_cost_per_item=0.02 / task.estimate.demo_seconds_per_item=3);后两项置为 public 供前端提交前展示预估(PRD R-06),数值非敏感且演练任务不产生真实计费。

2.2 数据库(prisma/)

  • 迁移 20261104125003_a6_media_tasks(expand-only,未改历史迁移与旧表):
    • media_tasks:任务头(类型 / 状态 / 客户快照 / 计数 / 预估 / 实际 / 取消与失败信息 / 时间);
    • media_items:任务项(seq / 状态 / 失败原因码与文案 / 尝试次数 / 时间;(task_id, seq) 唯一);
    • task_events:append-only 事件流(seq BIGSERIAL 作为 SSE id;(task_id, seq) 索引)。
  • 关系:任务项 / 事件 onDelete: Cascade(无删除任务对外入口,级联仅用于测试夹具清理;审计留痕由 append-only audit_logs 承担)。

2.3 后端 API(apps/api)

  • modules/media/:controller / service / repository / mapper / estimator / cursor / idempotency / SSE / 结果审计消费者,共 9 个单测 + 1 个集成测试。
  • infra/queue/:9 队列注册与生命周期、Outbox dispatcher(Redis 锁 + FOR UPDATE SKIP LOCKED + jobId 幂等 + 退避 / 死信)、/health/queues、/metrics。
  • 通用改动:AuditInterceptor 支持幂等重放 skipAudit(默认行为不变);AppModule 接线 MediaModule / QueueModule。

2.4 Worker(apps/worker)

  • config.ts(QUEUE=all|分组|队列名、并发 / workerId)、queues.ts、heartbeat.ts(30s TTL,10s 续写)、redis-options.ts。
  • processors/demo.processor.ts:事务 + 任务行锁、任务项条件认领、取消语义、非末次失败回 queued 交给 BullMQ 重试、末次失败落 failed、原子计数、item / progress / done 事件、收敛后写 media.task.finished|failed outbox;纯函数单测。
  • main.ts:按 QUEUE 建 Worker、心跳、SIGINT/SIGTERM 优雅退出并清理心跳。

2.5 前端(apps/web,方案 A)

  • 路由:/material/tasks、/material/tasks/:id(material:read;写操作按钮按 material:render 渲染),侧栏「生产任务」更名「任务中心」,详情面包屑补齐。
  • 页面:pages/material/tasks/(TasksPage / TaskDetailPage / TaskFilters / TaskTable / TaskSubmitModal / TaskConfirmModal / task-filter-utils + spec)。
  • 可复用组件:components/tasks/(TaskTypeTag / TaskStatusTag / TaskItemStatusTag / TaskProgressBar / TaskMiniProgress / TaskProgressRing / TaskSseIndicator / TaskBadge / useTaskStream / task-display / task-sse + spec)。
  • API / hooks:api/media.ts(+ spec)、hooks/queries/use-media-tasks.ts、api/{endpoints,query-keys,client,query-client}.ts 增量。
  • 样式:styles/tasks.css(基础组件 + 列表)、styles/task-detail.css(详情);全部使用 --lx-* token。
  • 交互:筛选 URL 双向同步(关键字 260ms 防抖)、游标「加载更多」、行点击进详情、提交演练任务(实时预估 + 复用幂等键 + 提交中禁用)、取消 / 重试二次确认、顶栏角标 + 待处理失败直达、完成 / 失败通知。

2.6 测试与监控

  • 单元测试:shared 197、api 257、web 238、worker 27。
  • 集成测试:api 92(含 media 12、outbox / 队列端点)。
  • 浏览器 e2e:e2e/a6-tasks.spec.ts 6 条(真实 API + worker + PG/Redis + 浏览器,40s 左右)。
  • 监控:infra/monitoring/queue-alerts.yml(积压 > 10min、失败率 > 5%、心跳缺失、outbox 死信)+ README.md(抓取地址 / promtool 校验 / 接入步骤)。

3 验证结果

门禁 命令 结果
格式 pnpm format:check ✅
Lint pnpm lint ✅ 0 error / 0 warning
类型 pnpm typecheck ✅ 10/10 tasks
单测(shared) pnpm --filter @lingxi/shared test ✅ 18 files / 197 tests
单测(api) pnpm --filter @lingxi/api test ✅ 37 files / 257 tests(lines 71%+)
单测(web) pnpm --filter @lingxi/web test ✅ 37 files / 238 tests(lines 91.7%)
单测(worker) pnpm --filter @lingxi/worker test ✅ 7 files / 27 tests
集成(api) pnpm --filter @lingxi/api test:int(停 dev 服务后) ✅ 11 files / 92 tests
构建 pnpm build ✅ 8/8 tasks
OpenAPI pnpm openapi:diff ✅ 无破坏性变更(新增 6 端点)
浏览器 e2e A6_E2E=1 pnpm exec playwright test e2e/a6-tasks.spec.ts ✅ 6/6(约 39s,连跑多次稳定)

e2e 覆盖: ① 提交混合模式 → 列表出现 → 顶栏角标面板 → 详情 SSE 推进到「部分失败」→ 仅重试失败项; ② 提交 20 项后立即取消 → 已取消、未开始项跳过; ③ 同 Idempotency-Key 同 body 返回同一任务、不同 body 40902(API 断言); ④ SSE Last-Event-ID 续推首帧 seq 更大、不重发历史; ⑤ 仅 material:read:不渲染提交 / 取消 / 重试,保留查看与只读提示; ⑥ 无 material:read:访问 /material/tasks → /403。

真实画面证据(真实 API + worker,1440×900 / 1024×900):design/a6-ui-v1/shots/impl/real/(列表、1024、提交弹层、角标面板、详情进行中 / 部分失败 / 待预览、重试确认、只读列表 / 详情)。独立视觉评审 7/10,按评审意见完成两轮修正:失败数进入角标与 1024 首屏、部分失败首屏头条改为「失败 N / M 项已处理」、去除工程化文案与禁用入口、状态色 / 图标统一、行高与表格撑满、重复入口与计数去重。


4 关键口径与实现说明

  1. 提交 = 202:按工程规范 04 §1/§6,异步任务提交返回 202;幂等重放同为 202 且返回同一 taskId。取消 / 重试是同步状态迁移,返回 201。
  2. 幂等:Idempotency-Key 8–128 可见 ASCII;指纹 sha256(actorId + 规范化 body);24h;同 key 同 body 返回首次响应且不重复写审计 / outbox / 任务行(skipAudit),不同 body 40902;并发由 (scope,key) 唯一约束兜底。
  3. 取消:仅排队 / 生成中;条件 UPDATE 认领;排队项 → 已跳过,执行中的项完成;无运行项时立即收敛为已取消,有运行项时交 worker 收敛。已成功项不受影响。
  4. 仅重试失败项:仅「部分失败」;失败项重置为排队、attempts 保留、失败原因清空;任务回到排队,成功项不重复执行。
  5. SSE:Authorization 头鉴权(前端 fetch 流,不用 EventSource);id = task_events.seq;800ms 落库轮询事件、15s 心跳、单轮 200 条;Last-Event-ID 优先于 last_event_id;终态补发 done 后关闭;前端重连 2 次失败后降级 5s 轮询(状态提示可见)。
  6. 状态机:完整九态与迁移表;M0 演练任务点亮排队 / 生成中 / 部分失败 / 待预览 / 失败 / 已取消;成功终态为「待预览」(B3 接入产物后点亮勾选 / 导出 / 入库)。
  7. 任务标题:后端生成 {类型} · {客户} · N 项 快照;前端列表 / 角标去掉类型前缀(类型已有独立标签),详情标题同理。
  8. 队列物理名:BullMQ 5 禁止队列名 / jobId 含 :,逻辑队列名(media:verify 等,用于指标 / 心跳 / QUEUE env)与物理名(media-verify)一对一映射,协议与监控口径不变。
  9. Outbox 退避:outbox_events 无 next_attempt_at,失败时把 occurred_at 重置为失败时刻作为退避起点;canceled 事件直接置已投递(worker 以 DB 状态感知),finished|failed 永不认领(media 本地消费者写审计)。
  10. 前端预估:读 /system/configs/effective 的 task.*(已置 public);缺失时回退 shared 定义默认值做展示兜底,后端仍最终校验与计算。
  11. 全局角标:数字 = 排队 + 生成中;有待处理失败时显示红点与「待处理失败 N 项,去处理」(直达失败筛选);1024 页头保留「待处理失败」一条关键统计。
  12. 完整任务号:后端无独立任务编号字段,UI 展示 T- + ULID 末 6 位,详情可复制完整 ID。

5 与计划的差异与待确认

编号 项 现状 / 决策
A6-D-01 A5 审计导出迁移 本轮未迁移(用户确认):A5 AuditExportJob 保持 setImmediate,B3 导出统一接入任务协议时迁移;A5 报告中的下一步顺延到 B3。
A6-D-02 队列物理名 BullMQ 5 禁止冒号 → 物理名连字符、逻辑名保留冒号;已写入代码注释、infra README 与本报告。
A6-D-03 Outbox 退避 复用 occurred_at 作为退避起点,无 schema 变更;代价是首次投递最多 ~1.5s 固有延迟。
A6-D-04 状态迁移补充 增加 partial_failed → queued(重试)与 running → failed(全部失败直接收敛)两条工程补充,单测覆盖,不改变 §3.6 九态枚举。
A6-D-05 重试与历史失败原因 任务项重试后清空 failureCode / failureMessage(详情展示最新状态);历史由 append-only task_events / audit_logs 保留。
A6-D-06 9 队列范围 采用 script:generate / image:matting / image:render / video:keyframe / video:shot / video:compose / delivery:zip / export:package / media:verify;架构中 media:post / embed:index 不在本批 9 队列内(M1/M3 再启用)。
A6-D-07 监控基建 本机无 Prometheus / Grafana:交付指标端点、健康检查、告警规则文件与接入说明,未接真实 Grafana。
A6-D-08 任务中心路由 采用工程规范 11 的 /material/tasks(计划 §7.6 /tasks 视为简写);权限复用 material:read / material:render(DP-01,不新增 task:*)。
A6-D-09 单任务项重试 本批只提供「仅重试失败项」批量入口,不提供单项重试(计划未要求;B3 快筛 / M1 重试可按需追加)。
A6-D-10 任务提交演练入口 M0 真实任务未建设,按用户确认提供「提交演练任务」(material:render);B3 / M1 接入真实任务后下线演练入口。

6 已知遗留

  1. 真实任务类型:图片 / 视频 / 脚本 / 导出 / 校验任务与产物、勾选、导出、入库由 B3 / M1 / M2 接入;当前九态中「已勾选 / 已导出 / 已入库」仅契约保留。
  2. 审计导出迁移:A5 审计导出仍走 setImmediate,B3 统一迁移到 A6 任务协议(含对象存储与并发限流)。
  3. 可观测:OTel SDK(新增依赖待评审)未接入;Grafana 告警规则文件未在本机加载验证;任务成功率 / 成本看板在 M2 / M4。
  4. SSE 扩展性:当前每连接 800ms DB 轮询 + 15s 心跳,100 人规模足够;若任务事件量增大应改为 Redis pub/sub 或 Postgres LISTEN/NOTIFY。
  5. 任务数据清理:task_events / media_tasks 无归档与保留期策略(后续与 A5 保留策略一并定)。
  6. 单项重试 / 批量取消:本批未提供(见 A6-D-09)。
  7. 演练任务成功终态:M0 演练任务成功收敛到「待预览」,不含产物与快筛;接口与 UI 已注明快筛后续开放。

7 下一步

  • 按计划进入 A7 AI 配置与调用底座(A6 的 Outbox / 任务协议 / 审计可直接复用)。
  • B3 导出接入时:迁移 A5 审计导出、接通对象存储签名 URL、补充导出任务类型与进度。
  • M1 接入图片 / 视频真实任务:产物表、快筛勾选、单镜替换、断点续跑在既有任务协议上扩展。
  • 运维:Prometheus / Grafana 接入 infra/monitoring/queue-alerts.yml,补齐队列积压与失败率看板。