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