Files
ttstd-infinite-canvas/docs/plans/editor-implementation-runbook.md
爱笑的毛毛虫 05d1bce876 feat(editor): AI 编辑助手悬浮面板、插件权限体系与时间线渲染管线(M4–M6.3) (#411)
* feat(editor): 编辑器实施 M0-M1 - 剪辑入口占位与插件 SDK v2 垂直切片

- detail.tsx 新增剪辑成片视图(editor tab,常驻),概览页插入剪辑步骤卡
- plugin-types.ts 新增 v2 类型(EditorSlotKind/EditorPluginPermission/PluginManifestV2),修复权限数组交叉归约 bug
- editor-slot-registry.ts 插槽注册表(优先级排序、幂等注册、订阅机制)
- plugin-registry.ts 注册器 v1/v2 分支校验,v1 路径零改动
- editor-shell.tsx 垂直切片插件:timeline-panel 插槽真实渲染,验证插件链路全通
- 测试:插槽注册表 5 用例 + 命令测试骨架,v1 插件回归 21 例零回归

* feat(editor): 命令协议黄金文件 - editor-commands 纯函数 reducer + 8 个黄金 op

* feat(editor): 快照撤销历史 - editor-history 有界 200 层 undo/redo

* feat(editor): 命令状态机 store - dispatch/手势预览/1.5s 防抖保存/串行保存链

* fix(editor): createEditorStore 返回 zustand hook 类型修复 typecheck

* feat(editor): 手势 echo 最小件 - 时间线面板拖拽移动/裁剪 + store 接线与本地持久化

* docs(editor): M2 完成状态记录

* feat(editor): M3.1 时间线面板选中联动 - store 选中状态 + 片段高亮/点击取消

* feat(editor): M3.2 预览监视器 - preview-renderer 插槽 + 真实媒体帧/播放头/时间码

* feat(editor): M3.3 片段检查器 - inspector 插槽 + 属性编辑入队可撤销

* feat(editor): M3.4 asset-ingest 素材库插件(addClip 命令入轨)

* feat(editor): M3.4 补充 - host 上下文(项目信息+资产)、editor.tsx 接线、asset-ingest 纯函数、死代码清理

* feat(editor): M3.5 subtitle-tools - SRT import/export + rebuild from node snapshot

* feat(editor): M3.6 transcription - deterministic mock ASR to subtitle track

* feat(editor): M3.7 export - render plan + ffmpeg.wasm fallback export

* feat(editor): M3.8 - render all 8 editor slots in three-pane layout + ai-assistant placeholder

* docs(editor): M3 完成状态 - 8 插件 + 全插槽接线,冒烟待确认

* feat(editor): 编辑器媒体与时间线打磨 - 资产来源分组筛选、时间线全宽布局、拖拽末片段修复、OSS 降级本地存储、Concat 风格控制条与检查器

* fix(plugins): 合并上游后修复插件分类筛选对 v2 清单的类型兼容

* feat(editor): 时间线工具态与轨道操作打磨 - 工具按钮选中仅图标变色、监视器 Concat 式控制条、removeTrack 黄金命令与轨道行头移除按钮、maskClosable 迁移

* fix(api): 资源上传超 MaxBytesReader 上限时报中文大小限制而非英文裸 400

* fix(editor): 监视器控制条 - 修复播放按钮 JSX 模板闭合花括号缺失导致的编译错误

* feat(api): 本地媒体导入分片化对齐 Concat - >50MB 视频不再失败 - 分片会话三接口(POST开始/PUT单片/complete合并)、前端超50MB自动切8MB分片并失败整传重试、默认配额放宽(日200MB→2GB、总2GB→20GB)、UploadResourceFile 豁免单文件限

* feat(editor): 轨道行头新增可见性/静音开关按钮 - 复用现有图标资源,按轨道类型显隐;命令层新增 setTrackFlag 黄金命令(flag 白名单 fail-closed)并补单测

* docs(progress): 记录轨道可见/静音开关改动与复测项

* docs(editor): M4 转写与渲染任务设计 - 复用 Task 表与状态机、独立创建 API、worker 领取类型过滤、whisper HTTP provider(OpenAI 兼容 /v1/audio)

* fix(api): OSS 上传失败自动降级本地存储时 body 已被 Transport 关闭 - newOSSRequest 用 NopCloser 包装请求体,服务端提前返回(签名 403)时不再关闭调用方持有的文件,降级分支 Seek 重读生效;补回归测试

* docs(progress): 记录媒体导入根治(分片+OSS 降级修复)复测项

* feat(api): 视频播放副本转码 H.265→H.264 + variant=playback 服务与启动回填 - 上传视频后异步探测 codec,H.265/AV1 走 moov 快速探测并转码 H.264 播放副本;GET /api/user-data/...?variant=playback 就绪时服务副本(独立 ETag :pb),未就绪回退原件并撤销后缀防 304 黑屏;storeResource 触发、启动幂等回填存量;前端播放失败自动轮询副本就绪后刷新

* fix(editor): 轨道至少保留一条 - 不可移除轨道点击 X 时居中浮现原因提示(2.2s 自动消失, aria-live);修正可见性 toggle 传参 (visible===false 语义) ;轨道行内移除按钮改为常驻(不可移除时点击给提示)

* feat(timeline): M4.1 字幕转写后台任务 - whisper.cpp 本地执行 - 新增 timeline_transcription 任务类型,worker 按类型分叉独立执行器(不走计费与模型渠道);输入 resourceId+language,读取用户资源后 ffmpeg 预处理 16k 单声道 wav,POST 本地 whisper.cpp /inference,结果写回 segments+srt+language;未配置 CANVAS_WHISPER_BASE_URL 等失败均落明确终态;feature 门控;进度分阶段;含解析/SRT/快速失败单元测试

* feat(media): 素材卡片时长与媒体面板打磨 - 导入媒体解析时长(前端 media-metadata + 后端 project_asset durationMs 透出)随卡片展示;媒体面板/监视器/时间线相关样式细节收尾

* docs(progress): 阶段 C 监视器播放复检清单 - H.265 播放副本/真实播放驱动/时长链路待浏览器确认

* feat(timeline): M4.1 转写任务创建 API - POST /api/timeline/transcriptions - service CreateTimelineTranscriptionTask:IsDraining 守护 + feature 门控 + 资源归属与可转写类型校验 + 排队计 active 限额(含任务级/项目级存储估算);local/whisper.cpp 标记,任务不经过模型路由与计费;createTaskWithinStorageQuota 复用与 ErrActiveTaskLimit 用户可读映射;测试覆盖排队成功(owner/type/provider 断言)、跨用户资源拒绝、非音视频拒绝

* feat(timeline): M4.3 字幕转写前端接线 - 真实任务替换 mock - 新增 services/api/timeline-tasks.ts(POST /timeline/transcriptions,复用 apiClient+request);EditorTranscription 改为 创建任务→waitForGenerationTask 轮询(25min)→结果 segments 映射 SrtEntry[]→rebuildSubtitleClips 写入字幕轨道;实时展示 stage/progress,失败/空识别/无字幕轨道分别提示;素材选择支持音频与视频(取 storageKey 中的 resource id 提交)

* fix(media): 播放副本状态收敛与轮询终态 - 无ffmpeg/远端存储/编码不可处理落 none 避免重复探测;前端 none 停轮询回原生播放并加 processing 上限

* feat(timeline): M4.2 时间线渲染任务 - POST /api/timeline/renders 创建(buildRenderPlan 校验含可渲染媒体、配额入队、local/ffmpeg、60min 超时);worker 顶部按类型分叉 processTimelineRender;ffmpeg 合成产物写资源存储并经 ResultJSON 记 resourceId;render 计划/结果测试 PASS

* fix(resource): 转码原子抢占与 OSS 请求体加固 - ClaimPlaybackTranscode 空/none→processing 单实例抢占避免重复转码,runPlaybackTranscode defer recover 落 failed;If-Range 与带 :pb 后缀的 serveETag 比较;无请求体的 OSS 请求不再包 NopCloser(nil)(防 Go 1.26 body 探测 nil 解引用)

* style(editor): 修正字幕回写面板缩进

* docs(timeline): M4 设计文档按实现回写 - 单一 worker 池 + processClaimedTask 顶部按类型分叉(决策 3/4),端点统一为复数 /api/timeline/transcriptions 与 /api/timeline/renders(注册于 RegisterTaskRoutes),ResultJSON/feature gate/ffmpeg env 同步;Runbook M4 表格与状态行更新;auth.go API 前缀白名单补 timeline

* fix(service): 修复上游 v1.2.5 公告配图删除保护自锁与测试夹具缺口

上游 #385 资源引用走查器对公告配图/草稿做了硬性引用检查,导致三个问题:
- UpdateAnnouncement 在更新公告前对旧配图走查,必然命中当前公告自身引用而自锁,
  公告替换配图功能实际不可用;DiscardAnnouncementImage 同理被草稿自身引用阻塞。
  修复:ensureResourceHasNoBusinessReferences 支持忽略指定引用(当前公告/当前草稿),
  其余引用仍按原语义拦截。
- admin DeleteAdminResources 将公告草稿引用视为阻塞,但删除事务会级联清理草稿与
  资源绑定,service 层永不进入事务,回滚审计注入测试无法到达预期路径。修复:service
  预检时过滤公告草稿引用,与 repository 级联语义对齐。
- project_settings_test.go 夹具未建 ResourceReferenceSnapshot 走查的 Task/Session/
  Message/TaskLog/Result/Announcement/AnnouncementImageDraft 表,报 no such table。
  修复:夹具 AutoMigrate 补齐。

后端全量 go test ./... 通过。

* feat(editor): 成片导出接入后端渲染任务(M4.2 前端接线)- timeline-tasks 增 createTimelineRenderTask;editor-export 主路径改 POST /timeline/renders 轮询取 resourceId 内嵌预览/下载,ffmpeg.wasm 降级保留

* feat(editor): M5 插件权限执行(fail-closed)- plugin-permission-check 新建(8 插槽最小权限表/判定/断言);SlotStack 渲染前过滤缺权限插槽改诊断条;editor-shell manifest 补 timeline.command/export.run;测试 10 用例含 editor-shell 真实回归

* feat(editor): M6.1 AI 命令 schema + M6.2 时间线摘要 - ai-command-schema(parse/dry-run 校验/12-op 提示词契约/版本化+golden);timeline-summary(确定性中文摘要/折叠上限);各 20/10 用例全绿

* docs(progress): M6 质检清单 - M6.1 schema/M6.2 摘要标记完成(含测试计数),M6.3 待模型源确认;清理 M5.1 重复条目

* feat(editor): AI 命令 schema 支持只读问答 - commands 空数组合法化(无法执行/纯提问时 reasoning 直接作答),新增 AI_EDITING_MAX_COMMANDS=8 上限防失控编辑,提示词同步

* feat(editor): AI 助手重构为右上悬浮对话面板(M6.3)+ 素材入库原子化收尾

- editor-ai-assistant.tsx: 占位面板替换为 OpenAI Canvas 风格右上悬浮对话面板;
  面板内文本模型选择器(effective.textModels,选择即持久化并覆写请求模型),
  消息流 user/assistant/error/preview 四类气泡 + 思考中占位,空态引导示例 chips
  一键发送,底部输入条;顶部工具栏常驻 Sparkles 入口(aria-pressed 切换)。
  修复上一版重复/截断函数声明(appendToEntry 双定义、settleAnswer 三声明等)。
  宿主链路:buildAiEditingSystemPrompt(summarizeTimeline(project)) 喂上下文 →
  parseAiCommandPlan → validateAiCommandBatch dry-run(任一非法整批拒绝)→
  ≤3 条 dispatch 直执行 / >3 条预览确认卡片;宿主拒绝时 failTurn 回填 FAILED_MARK 可重试。
- editor-asset-ingest/editor-preview-monitor/detail/editor.tsx: 配套接线与打磨
  (编辑器顶栏 AI 入口、素材导入面板、预览监视、项目页顶栏整理)。
- backend: LinkProjectAsset 事务内原子创建资产 + 项目链接(OnConflict 幂等),
  资产首次入库不再预落库,失败整体回滚,消除"有资产无链接"孤儿记录。
- docs: pending-test.mdx / runbook M6.3 状态更新(tsc + 测试 + 构建通过,
  浏览器冒烟待真实文本渠道)。
2026-09-04 14:03:33 +08:00

159 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
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.
# 编辑器实施 Runbook(分步执行计划)
> 配套文档:[编辑器预设插件化实施规格](../plans/editor-preset-plugin-implementation.md)(目标架构与接口)与 [ADR-0001 ~ 0007](../adr/)(决策)。本文件只解决一个问题:**按什么顺序、以什么粒度动手,让每一步都可验证、可回退、看得见进度**。
>
> 状态:2026-09-01 制定。M0–M3 已完成并提交;M4.1(转写后端:本地 whisper.cpp HTTP,
> `CANVAS_WHISPER_BASE_URL`)e9d1b8ad/bf26ebb1、M4.2(渲染后端:`POST /api/timeline/renders`)
> 3f16d13b、M4.3(转写前端回写,真实任务替换 mock)fd5e8139 均已提交。
## 0. 执行原则(为什么这样拆)
一次性大爆炸实施的三个失败模式:**看不见进度**(写一堆类型层后毫无可见产物)、**无法定位回归**(一个 diff 里混了协议+UI+后端)、**无法回退**(改动面太大只能整体回滚)。本计划按三条铁律对抗:
1. **原子步**:每一步是一个可独立验证的变更,只动一个目标(类型 / 纯函数 / 单个插件 / 单个接线点),不混多个目标。
2. **验证门**:每步结束必须过最轻的验证门(typecheck / 专项测试 / 构建,按步标注),通过才能进下一步。验证命令以当前 `package.json` 为准(§4 速查)。
3. **垂直切片优先**:先让「最小可演示」跑通(M0 空壳 + M1.4 插槽链路 + M2 命令),让每一步都有浏览器可见或测试可断言的产物,再横向铺开功能面。
配套纪律:每步一个 commit(`feat(editor)` / `test(editor)` 等,见 AGENTS.md);测试文件按项目实际约定放 `web/test/`(不是 `src/__tests__`,实施规格 §6 的路径为示意,落位时以本文件为准);不覆盖、不回滚非本次变更。
## 1. 里程碑总览
| 里程碑 | 核心产出 | 演示 / 验收标准 | 依赖 |
|---|---|---|---|
| **M0 准备与基线** | 编辑器路由占位壳 + 概览流程入口 + 测试骨架 | 浏览器访问 `/projects/:id/editor` 看到占位页;概览页有「剪辑成片」步骤卡;`bun test test/editor-commands.test.ts` 可跑 | 无 |
| **M1 SDK v2** | v2 类型 + editor-slot-registry + 注册器兼容 + 插槽链路垂直切片 | 一个预设插件注册的 timeline-panel 插槽在占位页**真实渲染**出空面板(链路全通) | M0 |
| **M2 命令状态机** | editor-commands / editor-history / editor-store + 黄金文件测试 | 命令序列 → 状态快照黄金文件比对通过;撤销/重做 200 层测试通过;手势(移动/裁剪)echo→提交→撤销浏览器可操作 | M1 |
| **M3 预设插件** | 8 个 editor 插件逐个人轨 + detail.tsx 接线 | 编辑器三段式布局完整:时间线/预览/检查器/素材库/字幕/转写/导出逐区域可见可操作 | M2、M4(API 面) |
| **M4 后端任务** | 转写 + 渲染异步任务(服务端) | `go test ./...` 通过;前端任务客户端(创建/轮询/SSE)接 editor-transcription/export | M3 前需 API 面 |
| **M5 权限执行** | plugin-permission-check + fail-closed 接入 | permission-denied 测试通过;未声明 `timeline.command` 的插件写操作被拒;v1 插件权限语义不变 | M1、M3 |
| **M6 AI 编辑交互** | ai-command-schema + timeline-summary + editor-ai-assistant | 代码侧完成(schema 黄金测试 / 摘要确定性 / 助手面板 M6.3 已实现,tsc + 全量测试 + 构建通过);待用户浏览器冒烟:命令执行→撤销、非法命令整批拒绝、批量 diff 预览 | M2、M3、M5 |
## 2. 原子步明细
> 每步的「验证」是**最轻**的通过标准;「完成标准」是这一步真正做完的标志。
### M0 准备与基线
| 步骤 | 改动文件 | 内容 | 验证 | 完成标准 |
|---|---|---|---|---|
| M0.1 | `web/src/pages/projects/detail.tsx` | `DetailView` 加 `"editor"`;`views` 数组加「剪辑」项(常驻,插在「项目画布」后);`editor` 视图渲染**占位页**(`editor-slot-registry` 为空时显示空态壳) | `bun run typecheck`;浏览器 `/projects/:id/editor` | tab 存在且可切换,占位页可见,其余 6 视图不受影响 |
| M0.2 | `web/src/pages/projects/detail/overview.tsx` | `productionSteps` 在「镜头视频」与「交付与打包」间插入「剪辑成片」步骤卡(`href: /projects/:id/editor`) | `bun run typecheck`;浏览器概览页 | 步骤卡可见可跳转;完成态 = 镜头视频齐备且已有时间线 |
| M0.3 | `web/test/editor-commands.test.ts`(新建骨架) | 空测试壳 + `bun test` 跑通;确认 `web/test/` 下测试基建 | `bun test test/editor-commands.test.ts` | 测试可运行(1 个占位断言通过) |
### M1 插件 SDK v2
| 步骤 | 改动文件 | 内容 | 验证 | 完成标准 |
|---|---|---|---|---|
| M1.1 | `web/src/lib/plugins/plugin-types.ts` | 加 `PLUGIN_API_VERSION "yingce.plugin/v2"` 类型、编辑器贡献类型(timeline-panel / preview-renderer / inspector / asset-ingest / subtitle-tool / transcription-provider / export-renderer / ai-assistant)、`timeline.*` / `export.run` / `ai.text` 权限字面量 | `bun run typecheck` | 纯类型增量,v1 类型零改动,既有代码零编译错误 |
| M1.2 | `web/src/lib/plugins/editor-slot-registry.ts`(新建) | 插槽注册表:`registerEditorSlot / unregisterEditorSlot / getEditorSlot`,按插槽类型 + 优先级排序,幂等注册 | 新增 `web/test/editor-slot-registry.test.ts`:注册/去重/排序/未知插槽 | 专项测试通过,无 UI 依赖(纯函数) |
| M1.3 | `web/src/lib/plugins/plugin-registry.ts` | 注册断言按 apiVersion 分支:v1 保持现状(kebab-case + ≥1 贡献);v2 增加插槽贡献合法性校验;重复权限检查对 v2 生效 | `bun test test/canvas-node-registry.test.ts`(既有)全过 + 新增 v2 注册测试 | v1 插件行为完全不变;v2 注册/拒绝路径测试通过 |
| M1.4 | `web/src/lib/plugins/builtin/editor/editor-shell.ts`(临时)+ `web/src/lib/plugins/builtin/index.ts` | 垂直切片:`editor-shell` 预设插件注册 1 个 timeline-panel 插槽,占位页渲染该插槽 | `bun run typecheck`;浏览器 `/projects/:id/editor` | 插槽注册→渲染链路全通(空面板可见),**这是第一条垂直切片** |
### M2 命令状态机(纯函数层,黄金文件先行)
| 步骤 | 改动文件 | 内容 | 验证 | 完成标准 |
|---|---|---|---|---|
| M2.1 | `web/src/lib/timeline/editor-commands.ts` + `web/test/fixtures/commands.golden.json`(新建) | 核心 op:`addClip / moveClip / trimClip / splitClip / removeClip / setClipProperty / addSubtitle / removeSubtitle`;reducer 纯函数;黄金文件 = 命令序列→期望状态快照 | 新增 `web/test/editor-commands.test.ts`:逐命令断言 + 黄金文件比对 | 全部 op 通过;非法 payload 抛错路径覆盖 |
| M2.2 | `web/src/lib/timeline/editor-history.ts`(新建) | 快照撤销栈:200 层上限、undo/redo、undo 后新命令清空 redo | 新增 `web/test/editor-history.test.ts` | 边界测试通过(0 层/200 层/undo 后 redo 清空) |
| M2.3 | `web/src/lib/timeline/editor-store.ts`(新建) | zustand store:命令入队→reducer→新状态;echo 预演与 release 提交;1.5s 防抖保存(localforage + 后端项目接口);保存失败向上抛 | 新增 `web/test/editor-store.test.ts`(mock 保存层) | 队列/防抖/失败传播测试通过 |
| M2.4 | 手势 echo 层(`web/src/components/editor/` 起步最小件) | 时间线区域支持移动、裁剪两个手势:拖动中本地 echo 预演,release 提交命令 | `bun run typecheck` + M2.1 测试 | 浏览器拖拽片段→撤销回退可操作(与画布拖拽不冲突,含 `data-canvas-no-zoom` 边界) |
### M3 预设插件(每步一个插件,每步后对应区域可见)
> 顺序按依赖:面板(数据展示)→ 预览 → 检查器(选中联动)→ 素材入轨(数据源)→ 字幕 → 转写/导出(依赖 M4 API 面,可先接 mock)。
| 步骤 | 改动文件 | 内容 | 验证 | 完成标准 |
|---|---|---|---|---|
| M3.1 | `web/src/lib/plugins/builtin/editor/editor-timeline-panel.ts` | `createTimelinePanel` → 注册 timeline-panel(bottom);渲染时间线轨道区(读 editor-store,接 M2.4 手势) | typecheck + `bun test test/editor-*.test.ts` | 轨道/片段渲染、选中联动、撤销重做 UI |
| M3.2 | `web/src/lib/plugins/builtin/editor/editor-preview-monitor.ts` | `createPreviewMonitor` → 注册 preview-renderer(center);浏览器原生近似渲染 + 播放头/时间码/帧步进 | typecheck;浏览器预览可播放 | 预览与时间线播放头同步(走 `buildTimelineRenderPlan` 预览近似路径) |
| M3.3 | `web/src/lib/plugins/builtin/editor/editor-inspector.ts` | `createInspector` → 注册 inspector(right);选中片段属性(起止/音量/变速/滤镜参数模型) | typecheck;浏览器选中联动 | 属性编辑经 `setClipProperty` 命令入队,可撤销 |
| M3.4 | `web/src/lib/plugins/builtin/editor/editor-asset-ingest.ts` | `createAssetIngest` → 注册 asset-ingest(left);角色资产/项目资产/本地上传 → 拖入时间线(`asset-ingest` 权限 + directMedia) | typecheck;浏览器拖拽入轨 | 素材入轨生成 clip(`addClip` 命令),复用现有资产层 |
| M3.5 | `web/src/lib/plugins/builtin/editor/editor-subtitle-tools.ts` | `createSubtitleTool` → 注册 subtitle-tool(right 折叠);SRT 导入导出(复用 `srt-parser`/`srt-resegment`)、AI 高亮、批量样式、**「从节点重建字幕片段」命令(§3.1 快照契约)** | typecheck + 新增字幕命令测试 | SRT 往返一致;重建命令替换过期快照(单向显式同步) |
| M3.6 | `web/src/lib/plugins/builtin/editor/editor-transcription.ts` | `createTranscription` → 注册 transcription-provider;调 M4 转写任务(API 面未就绪时先 mock) | typecheck;浏览器触发转写→字幕入轨 | 转写结果落字幕轨道(ADR-0004 协议) |
| M3.7 | `web/src/lib/plugins/builtin/editor/editor-export.ts` | `createExporter` → 注册 export-renderer;导出面板 → `buildTimelineRenderPlan` + 后端渲染任务 / ffmpeg.wasm 降级 | typecheck;浏览器导出 MP4 | 导出走单一滤镜图计划;`nodeId` 悬空引用按 §3.1 降级/报错二分 |
| M3.8 | `web/src/pages/projects/detail.tsx` + `web/src/lib/plugins/builtin/editor/index.ts` + `web/src/lib/plugins/builtin/index.ts` | editor 视图从 `editor-slot-registry` 渲染全部插槽;8 个插件静态注册(含 editor-ai-assistant 的 M6 先注册占位贡献,M6 补实现) | `bun run build`(全量)+ 浏览器三段式布局 | 布局完整、停用插件显示空态、v1 插件不受影响 |
### M4 后端任务(服务端)
| 步骤 | 改动文件 | 内容 | 验证 | 完成标准 |
|---|---|---|---|---|
| M4.1 | `backend/`:`task_timeline.go` + `transcription_whisper.go`;路由 POST `/api/timeline/transcriptions`(注册于 `RegisterTaskRoutes`) | 转写异步任务:`CreateTimelineTranscriptionTask`(feature 门控/资源归属/可转写类型校验)配额入队(local/whisper.cpp,不走模型路由与计费);worker 顶部按类型分叉 `processTimelineTranscription`,本地 whisper.cpp HTTP 转写,结果 `ResultJSON{segments,srt,language}` 落盘 | 转写相关单测 PASS(`go test ./internal/service/ ./internal/handler/`) | 未配置 `CANVAS_WHISPER_BASE_URL` 时任务明确失败并提示 |
| M4.2 | `backend/`:`task_render.go` + `timeline_render_plan.go`;路由 POST `/api/timeline/renders`(注册于 `RegisterTaskRoutes`) | 渲染任务:接收 v2 快照 → `buildRenderPlan`(含可渲染媒体校验)→ 服务端 ffmpeg 合成 → 产物入资源存储,`ResultJSON` 记 resourceId | render 单测 PASS | 60 分钟超时;`CANVAS_FFMPEG_PATH` 或 PATH |
| M4.3 | `web/src/services/api/timeline-tasks.ts` + `editor-transcription.tsx` | 转写任务客户端:创建 → `waitForGenerationTask` 轮询(25min)→ 结果 segments 映射 SrtEntry 写入字幕轨道(去 mock) | typecheck 通过 | 浏览器端到端:发任务→进度→字幕入轨待用户确认 |
### M5 权限执行(横切,fail-closed)
| 步骤 | 改动文件 | 内容 | 验证 | 完成标准 |
|---|---|---|---|---|
| M5.1 | `web/src/lib/plugins/plugin-permission-check.ts`(新建) | 纯函数:`checkPermission(plugin, required, context)`——声明缺失 → 拒绝(fail-closed);`timeline.write` 拆分为 `timeline.read`/`timeline.command` 粒度 | 新增 `web/test/plugin-permission-check.test.ts`(10 用例,含 editor-shell 真实回归) | 缺失/越权/边界测试通过 ✅ |
| M5.2 | 接入执行路径:`editor-slot-registry` 渲染时 + 命令队列入口 + 模型调用 | 插槽渲染校验、命令入队校验、AI 命令校验(`timeline.command` 未声明 → 拒绝) | `bun test test/plugin-permission-check.test.ts test/editor-*.test.ts`;**v1 插件全量既有测试回归** | 未声明权限的写操作被拒;v1 插件行为零回归(权限语义不收紧存量) |
| M5.3 | **执行路径接入(边界说明)**:`web/src/pages/projects/detail/editor.tsx` SlotStack fail-closed 过滤(缺权限插槽 → 一行诊断条,不渲染);`editor-shell` manifest 权限补全为 `timeline.read/timeline.command/export.run` | 命令队列入口与模型调用是**宿主全局命令**(M2 editor-commands 注册表,v1 语义);v2 插件不代发宿主命令,仅贡献插槽。M5 权限域(timeline.*/export.run)只约束 v2 插件插槽贡献与后续 M6 AI 命令代发通道,宿主 UI 手势不受权限校验——与 ADR-0007「v1 权限语义一字不动」一致 | editor-shell 8 插槽真实回归可渲染;全量 bun test 无新增失败 | 缺权限插槽被拒渲染且有诊断;editor-shell 权限齐备可渲染全部 8 插槽 ✅ |
### M6 AI 编辑交互(ADR-0007)
| 步骤 | 改动文件 | 内容 | 验证 | 完成标准 |
|---|---|---|---|---|
| M6.1 | `web/src/lib/timeline/ai-command-schema.ts` + `web/test/fixtures/ai-commands.golden.json`(新建) | per-op 命令 JSON schema(与 M2.1 handler 同源维护)、`aiCommandSchemaVersion` 版本化;fail-closed 校验函数 | 新增 `web/test/ai-command-schema.test.ts` | 合法/非法 payload 全覆盖;单条非法整批拒绝;错误回填格式稳定 ✅(14 用例全绿;golden op 集与 EDITOR_COMMAND_OPS 黄金同步断言通过) |
| M6.2 | `web/src/lib/timeline/timeline-summary.ts`(新建) | 时间线结构化精简摘要(轨道/片段/时长/关键属性,不含媒体二进制) | 新增 `web/test/timeline-summary.test.ts` | 摘要确定性强(同输入同输出);大对象排除 ✅(16 用例全绿;折叠上限/中文字幕统计/空态覆盖) |
| M6.3 | `web/src/lib/plugins/builtin/editor/editor-ai-assistant.tsx` | AI 助手:右上悬浮对话面板(OpenAI Canvas 风格,由顶部工具栏 Sparkles 入口开合;面板内文本模型选择器 + 消息流 + 空态引导 + 输入条)、命令执行链路(≤3 条直执行 / >3 条 diff 预览待确认)、宿主 registry 拒绝 failTurn 回填失败标记 | typecheck + M6.1/M6.2 测试 + `bun run build` ✅ | 自然语言→命令→撤销全链路代码侧就绪(≤3 条直执行按条入撤销栈,撤销按条回退);非法命令回填错误;与画布会话上下文隔离(历史存 `historyRef`,仅时间线上下文入提示词)✅(浏览器冒烟待用户确认) |
## 3. 依赖关系
```
M0(壳+骨架)──▶ M1(SDK v2 + 插槽链路)
│
▼
M2(命令状态机)
│
┌───────────┴───────────┐
▼ ▼
M4(后端任务)◀───(API 面先行,M3.6/3.7 可先 mock)
│ │
▼ ▼
M3(8 插件逐个)──▶ M5(权限执行,横切接入)
│
▼
M6(AI 编辑交互)
```
- M3.6 / M3.7 只依赖 M4 的 **API 面**(可先用 mock 接口),不必等 M4 全部完成。
- M5 横切:在 M3 完成后接入执行路径,避免在插件开发期被权限拦截干扰;但 M1.3 注册期就应声明权限字段。
- M6 依赖 M2(命令)、M3(面板)、M5(权限),最后做——它复用全部前置协议,是收尾验证。
## 4. 验证命令速查(以当前 package.json / go.mod 为准)
| 场景 | 命令 |
|---|---|
| 前端类型检查(每步) | `cd web && bun run typecheck` |
| 前端专项测试 | `cd web && bun test test/editor-commands.test.ts`(按文件替换) |
| 前端全量构建(M3.8 / 里程碑验收) | `cd web && bun run build`(含 build:bridge + tsc + vite build,较重,非每步跑) |
| 后端测试 | `cd backend && go test ./...` |
| 浏览器冒烟 | 对照实施规格 §7.4 清单(编辑器三路由、明暗主题、空态、快捷键不冲突) |
## 5. 每步通用完成检查
- [ ] diff 只含本步目标文件,无顺手改动
- [ ] 类型/调用方/文档同步(本步涉及的方案章节如有出入,当场修正)
- [ ] 错误与权限路径完整(不 `catch { return defaultValue }` 吞错)
- [ ] 验证如实记录(通过的命令 + 结果;不能浏览器验证时写明替代依据)
- [ ] 按 AGENTS.md 提交规范 commit(`feat(editor): ...` / `test(editor): ...`)
## 6. 高风险步与回退
| 步骤 | 风险 | 缓解 / 回退 |
|---|---|---|
| M1.3 注册器 v1/v2 分支 | 破坏既有 v1 插件(eagle/prompt-optimizer/workflows/portrait-clearance) | 分支隔离 + 既有测试全量回归;异常时 revert 该 commit(v1 路径零改动原则) |
| M3.8 detail.tsx 接线 | editor 视图影响其余 6 视图 | 视图按 id 分发,editor 分支独立渲染;回退 = revert M3.8,其余插件贡献无损 |
| M5.2 权限收紧 | v1 插件未声明新权限域被误拒 | **fail-closed 只针对新权限域**(timeline.* / export.run / ai.text),v1 既有权限语义一字不动;先加测试再接入 |
| M4.1 转写 provider | ~~部署路径未定~~ 已定:本地 whisper.cpp HTTP(`CANVAS_WHISPER_BASE_URL`),不接云 ASR | 语音数据不出本机;未配置仅影响转写任务(任务明确失败并提示) |
| M2.4 手势 echo 与画布拖拽冲突 | 时间线内拖拽与画布手势系统互扰 | 复用画布事件忽略选择器约定(modal/popover/dropdown)与 `data-canvas-no-zoom` 边界,冒烟清单含快捷键不冲突项 |
## 7. 里程碑验收顺序(推荐推进节奏)
1. 完成 M0+M1 → **第一次演示**:`/projects/:id/editor` 占位页有真实插槽渲染(证明插件链路成立)
2. 完成 M2 → **第二次演示**:纯命令驱动的可撤销时间线(无 UI 也能证明核心正确)
3. 完成 M3(含 M4 API 面)→ **第三次演示**:完整三段式编辑器可编辑可导出
4. 完成 M5+M6 → **第四次演示**:权限收紧 + 对话式剪辑(ADR-0007 完整落地)
每两次演示之间建议不超过一个里程碑;若一个里程碑内停滞超过 3 次同类失败,停下记录现象、已排除项与新假设(AGENTS.md §8),再切换路径。