未知项野外指南 · 实现阶段
← 全部示例

实现笔记

别让构建途中的意外淹没在滚动的终端输出里——让 agent 在每次偏离计划的当下就把它记录下来,这些意外就会变成你下一次尝试的输入。

提示词
“Keep an implementation-notes file as you build the export feature. If you hit an edge case that forces you to deviate from the plan, pick the conservative option, log it under ‘Deviations’, and keep going.”

中文大意:在构建导出功能的过程中,持续维护一份实现笔记文件。如果遇到迫使你偏离计划的边界情况,就选保守的那个方案,记在「Deviations(偏离)」条目下,然后继续推进。

Claude 的产出
docs/notes/export-feature-implementation.md 运行开始 14:02 · 最后写入 16:51 · 分支 feat/review-export
11
条目
4
偏离
2
需你拍板
14:02
按计划完成14:02

第 1 步 — 导出任务模型与队列接线,按计划进行

apps/api/src/exports/exportJob.ts 中新增 ExportJob,挂到现有的 BullMQ media-tasks 队列上,使用专用的 export 任务名。Migration 0121_export_jobs 与计划里的 schema 完全一致(status 枚举、requested_byreview_idformat)。

14:18
按计划完成14:18

第 2 步 — POST /api/v1/reviews/:id/export 端点

请求校验按计划复用了 apps/api/src/validation/ 里的 zod schema。format 接受 mp4-burnin | csv | bundle。返回 202 和一个任务 id;轮询端点按计划推迟到第 6 步。

14:29
新发现14:29

Review.duration_ms 是反规范化字段,偶尔会过期

有三条 fixture 评审的 duration_ms 与源资产的探测数据对不上(看起来是重新上传后一直没刷新它)。导出改从 MediaAsset.probe.duration 读取时长——与 playbackManifest.ts 已有的做法相同。不改计划,只是把这个坑记下来,提醒之后碰这块代码的人。

14:41
偏离 1 / 414:41

历史遗留批注不一定有帧时间戳

计划怎么说
每条批注都有 frame_ts;烧录渲染器可以把它们全部排序并放上时间线。
代码揭示了什么
annotations 表约 12% 的行早于 migration 0087,其 frame_ts = NULL——在逐帧批注出现之前,它们本是针对整个评审的一般性评论。
保守选择
视频烧录中排除无时间戳的批注;把它们放进 CSV 附属文件,frame_ts 留空并加 legacy_comment 标记。导出包里没有任何内容被悄悄丢弃。
待回头再议
可以用 created_at 相对评审会话的偏移插值出一个时间戳——需要判断这种做法是否足够诚实、适不适合出现在视频里。
15:05
按计划完成15:05

第 3 步 — 用 ffmpeg drawtext/drawbox 烧录批注,按计划进行

Filtergraph 构建器位于 apps/worker/src/exports/burnin.ts。计划中按批注设置叠加时间窗(enable='between(t,a,b)')的做法可行;用 40 条批注的压力 fixture 验证过——在该 worker 实例规格上渲染耗时为实时的 1.4 倍,在计划 2 倍的预算之内。

15:22
偏离 2 / 415:22

队列一来一回,Date 对象变成了字符串

计划怎么说
ExportJobPayload.requestedAt: Date,透传给 worker,并与评审的 updated_at 比较,用来检测过期导出。
代码揭示了什么
BullMQ 会把 payload 以 JSON 序列化后写入 Redis,所以 worker 收到的是 ISO 字符串而非 Date。这个过期比较在单元测试里靠隐式类型转换侥幸通过,却在集成测试的一次重投递任务上失败了。
保守选择
把 payload 按线上传输格式定义类型(requestedAt: string,ISO-8601),在 exportConsumer.ts 的 worker 边界处只解析一次。不写自定义序列化器。
待回头再议
其他 media-tasks 的 payload 很可能潜伏着同样的 bug——给任务 payload 做一个共享的 zod codec,就能根治这一整类问题。
15:48
偏离 3 / 415:48

流式 zip 早已存在——放弃了计划中的 archiver 依赖

计划怎么说
引入 archiver,把导出包(视频 + CSV + 缩略图)打成 zip 流写入 S3。
代码揭示了什么
packages/media/src/zipStream.ts 已实现带背压处理的流式 zip——它支撑着批量资产下载功能,自三月起就在生产环境运行。
保守选择
原样复用 zipStream.ts。少一个依赖,导出包还继承了与批量下载相同的、经过实战检验的背压行为。
待回头再议
zipStream 位于 packages/media,如今却被两个功能使用——值得在清理时把它提升到 packages/shared
16:07
新发现16:07

websocket 进度频道已有现成约定

转码流水线会发出 media:progress:{assetId} 事件。导出沿用了同一约定——发出 export:progress:{jobId}——这样前端就能复用 apps/web/src/hooks/ 里的 useProgressChannel(),不必再做计划里草拟的轮询 UI。轮询端点也保留了,作为兜底。

16:20
偏离 4 / 416:20

访客评审员能查看评审,却不能下载资产

计划怎么说
导出权限 = 评审级 ACL:能打开评审的人就能导出。
代码揭示了什么
工作区级的 guest_reviewer 角色授予评审访问权,但 can_download_assets = false——客户用它让外部干系人参与评论,同时不让媒体外流。若只检查评审级 ACL,访客就能借导出把源视频带出去。
保守选择
guest_reviewer 的所有导出格式一律返回 403,与 assetAccess.ts 里已有的资产下载检查保持一致。这是最严格、也最站得住脚的解读。
待回头再议
或许访客仍应能拿到 CSV(只含批注、不含媒体)。这是产品决策,不该由我拍板——已记录在下方。
16:33
需人拍板16:33

定夺访客评审员的导出策略

当前行为(来自偏离 4):访客对任何导出格式都会收到 403。如果你希望访客能导出不含媒体的批注 CSV,改动大约 10 行(在 exportPolicy.ts 里)外加一个测试——但这会改变「不能下载资产」对客户的含义,所以应当由你来定。既不阻塞合并,也不阻塞 QA。

16:51
需人拍板16:51

确认导出包的保留期限

计划没有为生成的导出包指定 TTL。我把它们放在 S3 的 exports/ 前缀下,配了 7 天生命周期规则,与分享链接的过期时间一致——足够保守,也没有任何面向客户的承诺超过这个期限。但如果合同或数据保留政策另有规定,只需改 infra/s3.tf 里生命周期规则那一行。