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

158 lines
14 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 灵犀 A6 任务中心与队列骨架 · 实施报告 V0.1
| 项 | 内容 |
| --- | --- |
| 批次 | A6 任务中心与队列骨架(开发计划 §6.7 / §7.6,架构 §3.6 / §3.7 / §6.4) |
| 完成日期 | 2026-11-05 |
| 状态 | ✅ 已完成(A6-BE-01~05、A6-INF-01、A6-FE-01~02、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`,补齐队列积压与失败率看板。