From 3cc6f75ccc799170c358ed97b6462e28b19a3407 Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Tue, 25 Aug 2026 03:03:28 +0800 Subject: [PATCH] docs: archive completed plans, add the external plugin system design - docs/zh/plans: finished plans move to completed/ (the RIIR series, the event-bridge and dependency plans, the v04 manual test plan). - New design docs: the external (functional) plugin system (process-isolated, JSON-RPC/shm) and its protocol. - ai-agent-design refreshed; README pointers follow the moves. --- README.new.md | 4 +- crates/oak-codec/README.md | 2 +- docs/zh/README.new.md | 4 +- ...investigation-edge-display-and-playback.md | 96 --- docs/zh/ofx-pluginrenderer-functions-zh.md | 44 -- docs/zh/plans/README.md | 21 +- docs/zh/plans/ai-agent-design.md | 305 +++++++--- docs/zh/plans/completed/README.md | 4 +- .../completed/c-abi-migration-handoff-v4.md | 2 +- .../completed/c-abi-migration-handoff-v6.md | 2 +- .../eliminate-event-bridge-issues.md | 2 +- .../{ => completed}/eliminate-event-bridge.md | 0 .../{ => completed}/gtest-migration-guide.md | 2 +- .../issues-9-20-dependency-plan.md | 0 docs/zh/plans/completed/r6-cleanup-plan.md | 4 +- docs/zh/plans/completed/r7-pure-abi-plan.md | 2 +- .../r8-app-pure-abi-cleanup.md | 4 +- .../{ => completed}/r8-p3-node-param-abi.md | 2 +- docs/zh/plans/{ => completed}/riir.md | 4 +- .../plans/{ => completed}/riir/00-overview.md | 0 .../riir/01-adapter-pattern.md | 0 .../riir/02-modules-and-order.md | 0 .../plans/{ => completed}/riir/03-testing.md | 0 .../{ => completed}/riir/04-interfaces.md | 0 .../{ => completed}/riir/M1-oakcommon.md | 0 .../{ => completed}/riir/M10-oakstorage.md | 0 .../{ => completed}/riir/M11-ofx-host.md | 0 docs/zh/plans/{ => completed}/riir/M12-app.md | 0 .../{ => completed}/riir/M13-storage-live.md | 0 .../{ => completed}/riir/M14-direct-rlib.md | 0 .../riir/M15-render-process-isolation.md | 0 .../plans/{ => completed}/riir/M2-oakundo.md | 0 .../plans/{ => completed}/riir/M3-oaknode.md | 0 .../{ => completed}/riir/M4-oaktimeline.md | 0 .../plans/{ => completed}/riir/M5-oakcodec.md | 0 .../plans/{ => completed}/riir/M6-oakaudio.md | 0 .../{ => completed}/riir/M7-oakrender.md | 2 +- .../plans/{ => completed}/riir/M8-oaktask.md | 0 .../{ => completed}/riir/M9-oakplugin.md | 0 docs/zh/plans/{ => completed}/riir/notes.md | 0 .../plans/{ => completed}/riir/single-lib.md | 0 .../plans/{ => completed}/ui-redesign-plan.md | 8 +- ...olor-audio-performance-manual-test-plan.md | 0 docs/zh/plans/external-plugin-protocol.md | 548 ++++++++++++++++++ docs/zh/plans/external-plugin-system.md | 334 +++++++++++ docs/zh/project-storage.md | 4 +- 46 files changed, 1120 insertions(+), 280 deletions(-) delete mode 100644 docs/zh/investigation-edge-display-and-playback.md delete mode 100644 docs/zh/ofx-pluginrenderer-functions-zh.md rename docs/zh/plans/{ => completed}/eliminate-event-bridge-issues.md (99%) rename docs/zh/plans/{ => completed}/eliminate-event-bridge.md (100%) rename docs/zh/plans/{ => completed}/gtest-migration-guide.md (99%) rename docs/zh/plans/{ => completed}/issues-9-20-dependency-plan.md (100%) rename docs/zh/plans/{ => completed}/r8-app-pure-abi-cleanup.md (97%) rename docs/zh/plans/{ => completed}/r8-p3-node-param-abi.md (99%) rename docs/zh/plans/{ => completed}/riir.md (99%) rename docs/zh/plans/{ => completed}/riir/00-overview.md (100%) rename docs/zh/plans/{ => completed}/riir/01-adapter-pattern.md (100%) rename docs/zh/plans/{ => completed}/riir/02-modules-and-order.md (100%) rename docs/zh/plans/{ => completed}/riir/03-testing.md (100%) rename docs/zh/plans/{ => completed}/riir/04-interfaces.md (100%) rename docs/zh/plans/{ => completed}/riir/M1-oakcommon.md (100%) rename docs/zh/plans/{ => completed}/riir/M10-oakstorage.md (100%) rename docs/zh/plans/{ => completed}/riir/M11-ofx-host.md (100%) rename docs/zh/plans/{ => completed}/riir/M12-app.md (100%) rename docs/zh/plans/{ => completed}/riir/M13-storage-live.md (100%) rename docs/zh/plans/{ => completed}/riir/M14-direct-rlib.md (100%) rename docs/zh/plans/{ => completed}/riir/M15-render-process-isolation.md (100%) rename docs/zh/plans/{ => completed}/riir/M2-oakundo.md (100%) rename docs/zh/plans/{ => completed}/riir/M3-oaknode.md (100%) rename docs/zh/plans/{ => completed}/riir/M4-oaktimeline.md (100%) rename docs/zh/plans/{ => completed}/riir/M5-oakcodec.md (100%) rename docs/zh/plans/{ => completed}/riir/M6-oakaudio.md (100%) rename docs/zh/plans/{ => completed}/riir/M7-oakrender.md (99%) rename docs/zh/plans/{ => completed}/riir/M8-oaktask.md (100%) rename docs/zh/plans/{ => completed}/riir/M9-oakplugin.md (100%) rename docs/zh/plans/{ => completed}/riir/notes.md (100%) rename docs/zh/plans/{ => completed}/riir/single-lib.md (100%) rename docs/zh/plans/{ => completed}/ui-redesign-plan.md (97%) rename docs/zh/{ => plans/completed}/v04-color-audio-performance-manual-test-plan.md (100%) create mode 100644 docs/zh/plans/external-plugin-protocol.md create mode 100644 docs/zh/plans/external-plugin-system.md diff --git a/README.new.md b/README.new.md index ab320e67a..0c6c66452 100644 --- a/README.new.md +++ b/README.new.md @@ -45,7 +45,7 @@ Oak is split into small, independently testable components with a pure C ABI at | `oak-render-worker` | process | headless render process that executes frames off the GUI thread (NDJSON IPC) | | `oak-cli` | tool | command-line frontend for the engine: media info, probing, rendering, and transcoding without the GUI | -The C ABI boundary is what makes the engine embeddable and is the foundation for a planned module-by-module rewrite of the engine in Rust (see [`docs/zh/plans/riir.md`](docs/zh/plans/riir.md)). +The C ABI boundary is what makes the engine embeddable and is the foundation for a planned module-by-module rewrite of the engine in Rust (see [`docs/zh/plans/completed/riir.md`](docs/zh/plans/completed/riir.md)). ![Architecture diagram](docs/images/architecture.png) @@ -89,7 +89,7 @@ Contributions are welcome. Please read [`CONTRIBUTING.md`](CONTRIBUTING.md) firs - the **Google Test** requirement for all tests, - the C ABI boundary contract for engine-facing code. -Useful project docs: [`docs/zh/`](docs/zh/) (中文文档), [`docs/zh/facade-migration-roadmap.md`](docs/zh/facade-migration-roadmap.md), [`docs/zh/plans/riir.md`](docs/zh/plans/riir.md). +Useful project docs: [`docs/zh/`](docs/zh/) (中文文档), [`docs/zh/facade-migration-roadmap.md`](docs/zh/facade-migration-roadmap.md), [`docs/zh/plans/completed/riir.md`](docs/zh/plans/completed/riir.md). ## License diff --git a/crates/oak-codec/README.md b/crates/oak-codec/README.md index 15f7d8926..d5561b62e 100644 --- a/crates/oak-codec/README.md +++ b/crates/oak-codec/README.md @@ -7,7 +7,7 @@ > through the [`ffmpeg-next`] crate (decode, probe, audio conform, > encode); the OIIO engine remains a stub in this build. > -> Single-lib unification (M14, `docs/zh/plans/riir/single-lib.md`): the +> Single-lib unification (M14, `../../docs/zh/plans/completed/riir/single-lib.md`): the > C-ABI export layer (`src/ffi.rs`) and the module-crossing bridge > (`src/bridge/`) are gone. Other module crates and the oakengine facade > call this crate's modules directly; the facade (`crates/oakengine`) diff --git a/docs/zh/README.new.md b/docs/zh/README.new.md index ca7f60b73..4e8db47e1 100644 --- a/docs/zh/README.new.md +++ b/docs/zh/README.new.md @@ -45,7 +45,7 @@ Oak 拆分为若干可独立测试的组件,组件之间以**纯 C ABI** 为 | `oak-render-worker` | 进程 | 无头渲染进程,在 GUI 线程之外渲染帧(NDJSON IPC) | | `oak-cli` | 工具 | 引擎的命令行前端:媒体信息、探测、渲染、转码,无需 GUI | -这条 C ABI 边界让引擎可以被嵌入,也是后续将引擎按模块逐步用 Rust 重写的基础(见 [`riir.md`](plans/riir.md))。 +这条 C ABI 边界让引擎可以被嵌入,也是后续将引擎按模块逐步用 Rust 重写的基础(见 [`riir.md`](plans/completed/riir.md))。 ![架构图](../images/architecture.png) @@ -89,7 +89,7 @@ ctest --test-dir build --output-on-failure - 所有测试必须使用 **Google Test** 编写, - 面向引擎代码的 C ABI 边界契约。 -更多项目文档:[中文文档目录](./)、[`facade-migration-roadmap.md`](plans/completed/facade-migration-roadmap.md)、[`riir.md`](plans/riir.md)、[`gtest-migration-guide.md`](plans/gtest-migration-guide.md)。 +更多项目文档:[中文文档目录](./)、[`facade-migration-roadmap.md`](plans/completed/facade-migration-roadmap.md)、[`riir.md`](plans/completed/riir.md)、[`gtest-migration-guide.md`](plans/completed/gtest-migration-guide.md)。 ## 许可证 diff --git a/docs/zh/investigation-edge-display-and-playback.md b/docs/zh/investigation-edge-display-and-playback.md deleted file mode 100644 index 2be9d96ef..000000000 --- a/docs/zh/investigation-edge-display-and-playback.md +++ /dev/null @@ -1,96 +0,0 @@ -# 节点图边显示异常 & 播放问题排查进展(2026-08-02) - -本文记录当前排查状态,供手工继续排查。随调查更新。 - -## 当前未解决的两个现象 - -### A. 节点图边显示随机缺失 -- 每次显示都不一样:有时全部显示,有时缺几条,缺的边每次不同,无规律。 -- 手工重连能连上;切换选中素材(时间线点选)再切回来,又随机缺。 -- 已确认**不是只有 footage 相关边**受影响,缺的边类型随机。 - -### B. 播放冻结(部分修复后仍有残留报告) -- 历史症状:播放头不动、无声音、画面不动。 -- 已修复两个确定的根因(见"已修复"清单 8、9),复测中。 - -## 已验证的事实(不要再重复验证) - -1. **引擎图是稳定的**。用 C ABI 直接加载 `~/Movies/bbb.ove`,12 条边在 - activate project + 多轮 frame request 后全部存活(/tmp/edge2_repro 验证)。 - 边不显示 ≠ 边被引擎删除。 -2. **项目文件内容正常**。bbb.ove 的 12 条连接(见下"图结构")序列化无误, - clip 节点只声明了 9 个输入(无 pos_in/tex_in/volume_in——那些警告见第 6 条)。 -3. **NodeViewContext 的边创建没有走跳过分支**。`OAK_DEBUG_EDGES=1` 运行时, - "no item for" 跳过日志一条都没有 → 每条边都调用了 `add_edge_internal`, - 边对象是创建了的。问题在创建之后:被事件删掉、或绘制/几何异常。 -4. **删边路径只有两条**:`child_input_disconnected`(NODE_INPUT_DISCONNECTED - 事件驱动,`nodeviewcontext.cpp:251`)和 `remove_child`(节点移除时递归删边)。 -5. **XML/context 解析已排除**。C ABI 实测:引擎解析出的 context 成员与 - bbb.ove 完全一致(sequence=[sequence,track,track]; - clip1=[transform,clip1,footage];clip2=[footage,volume,clip2]), - 12 条边全部存在。图在引擎里 100% 正确,问题只在 app 的 - NodeViewContext 的 item_map_ 里缺节点 item。 -6. **根因已修复(边显示随机缺失)**:`oakengine_node_output_connection_at_ex` - 和 `oakengine_node_output_connection_at` 把 `output_connections()` 的 - `conn.first`(其实是 source 自身)当成目标节点返回,正确目标是 - `conn.second.node()`。后果:NodeViewContext 的 out-edge 枚举拿到的 - "对端"永远是节点自己,`item_map_` 查到自己 → from==to 静默丢弃 → - out-edge 一条都画不出,只能靠 in-edge 补;哪些边缺取决于节点进入 - context 的顺序(成员顺序+时序)→ 表现为随机缺边。已加回归测试 - (oakengine_node_test.cpp test_edges:at_ex 的目标必须是 LUT 而非 solid)。 - 同路径受益者:timelinewidget multicam、nodeparamview 的删边逻辑。 -7. 日志里的 `Failed to retrieve array size of parameter "pos_in"... in Olive.clip` - 和 `"muted_in" ... in Olive.sequence` 是**显示层的错查**,调用栈: - `NodeParamView::update_element_y`(`app/widget/nodeparamview/nodeparamview.cpp:1174`) - 把每个 item 的输入都拿去对 `contexts_.first()` 做 group resolve——硬编码 - 第一个 context,解析错了节点。此 bug 未修,与边显示的关系未确定。 -7. bbb.ove 图结构(12 条边): - - folder.child_in → footage(33755451136), sequence(33848831232) - - sequence.tex_in ← track(33796807680);sequence.samples_in ← track(33796810368) - - sequence.track_in_0/1 ← 两个 track - - track_v.block_in ← clip(33755511232);track_a.block_in ← clip(33755497792) - - clip1.buffer_in ← transform(33764063616);transform.tex_in ← footage - - clip2.buffer_in ← volume(33795740032);volume.samples_in ← footage - -## 已修复的问题(本批,工作区内未全部提交) - -| # | 问题 | 位置 | 状态 | -|---|------|------|------| -| 1 | get_distance_between_nodes 无递归出口栈溢出 | app/widget/nodeparamview/nodeparamview.cpp | 已提交 a4dfc62f0 | -| 2 | viewerdisplay texture_ 悬垂指针(GL 崩溃) | viewerdisplay.{h,cpp} assign_texture | 已提交 a4dfc62f0 | -| 3 | resignal_requests 遍历中改容器 | engine/render/playbackcache.h | 已提交 a4dfc62f0 | -| 4 | preview request 释放后 ticket 回调 UAF | engine/src/capi/preview.cpp | 已提交 a4dfc62f0 | -| 5 | 播放队列帧时间戳全为 0 → 画面不动 | preview.h/.cpp + viewer.cpp | 已提交 a4dfc62f0 | -| 6 | 浮动"查看器"绑定到序列节点 | mainwindow.cpp open_node_in_viewer | 已提交 a4dfc62f0 | -| 7 | Track 析构 UAF + undo remove_track 丢片段 | engine/node/output/track/track.{h,cpp} | 已提交 30853cbcf | -| 8 | 音频 sample_count API 缺失(无声音根因之一) | preview.h/.cpp 新增 get_audio_sample_count | 未提交 | -| 9 | teardown 系列:~Node 中断边事件打到半死对象 | node.cpp silent disconnect;project.cpp is_being_cleared_;clip.cpp marker disconnect 空指针;projectcopier/previewautocacher 项目死后野指针 | 未提交 | -| 10 | 输入 id memcpy 未 NUL 终止(参数名腐坏) | nodeparamviewitem.cpp:40、nodeparamviewwidgetbridge.cpp:145 | 未提交 | -| 11 | 裸事件订阅析构不退订(拖播放头崩溃) | nodeparamviewkeyframecontrol、nodeparamviewconnectedlabel、export dialog 各加析构退订 | 未提交 | - -测试:`cd cmake-build-debug && ctest -j4` 目前 122/122 通过 -(含新增 preview request 端到端回归:单帧+音频请求、teardown 不崩)。 - -## 仍在工作区里的调试代码(提交前需清理) - -- `OAK_DEBUG_EDGES=1`:nodeundo.cpp(Add/RemoveCommand redo/undo)、 - node.cpp disconnect_edge、nodeviewcontext.cpp(edge added/removed/跳过)。 -- `OAK_DEBUG_INVALID_INPUT=1`:node.cpp report_invalid_input 打调用栈。 -- /tmp 下的复现程序(编译产物在 cmake-build-debug/app/ 下): - - `preview_repro`:C ABI 播放请求全流程(单帧+音频+teardown) - - `edge_repro`:工厂建节点连边 + autocache churn - - `edge2_repro`:加载 bbb.ove 验证 12 条边存活 - -## 下一步排查方向(按优先级) - -1. 用最新构建(含 edge added/removed 日志)跑 `OAK_DEBUG_EDGES=1`, - 对照 12 条边在视图里的 add/remove 次数,找幽灵 remove。 -2. 若证实是 load 线程事件与 GUI 建图交错:检查 EngineEventBridge 的 - 事件入队时序 vs NodeView::set_contexts 的建图时机(重复边/乱序删边)。 -3. 修 `update_element_y` 的 `contexts_.first()` 硬编码(应按 item 所属 context resolve)。 -4. clip 缺少 transform 输入(pos_in 等)导致的参数面板/关键帧视图查询失败 - (bbb.ove 里没有这些输入声明,但 app 代码在查)——需确认 Olive 原版 - ClipBlock 是否有这些输入,是被 R8 弄丢的还是本来就不该查。 -5. 音频残留:`Tried to allocate sample buffer with invalid audio parameters` - 仍在日志出现(audio processor 输出参数 channels=0 → fix_channel_layout - 修正为 2,但上游某处仍用 0 声道创建 buffer)。 diff --git a/docs/zh/ofx-pluginrenderer-functions-zh.md b/docs/zh/ofx-pluginrenderer-functions-zh.md deleted file mode 100644 index 2a1dcc418..000000000 --- a/docs/zh/ofx-pluginrenderer-functions-zh.md +++ /dev/null @@ -1,44 +0,0 @@ -# OFX PluginRenderer 函数说明(中文) - -日期:2026-01-11 -执行者:Codex - -## 说明 -本文档概述 `app/render/plugin/pluginrenderer.cpp` 与 `app/render/plugin/pluginrenderer.h` 中函数的职责,用于排查 OFX 渲染链路问题。 - -## 头文件(pluginrenderer.h) -- `olive::plugin::detail::BytesToPixels`:将字节行跨度转换为像素行跨度,供纹理读写使用。 -- `olive::plugin::PluginRenderer`:OFX 插件渲染器,负责插件调用与 GL/CPU 纹理桥接。 -- `PluginRenderer::AttachOutputTexture`:绑定输出纹理到 OFX 的 GL 输出路径。 -- `PluginRenderer::DetachOutputTexture`:解除 OFX 的 GL 输出绑定。 -- `PluginRenderer::RenderPlugin`:执行完整的 OFX 渲染流程(输入/输出准备、动作调用、结果处理)。 - -## 源文件(pluginrenderer.cpp) -- `GetOfxAVPixelFormat`:根据 OFX Image 的属性推导 FFmpeg 像素格式,并返回每像素字节数。 -- `ApplyClipPreferencesToParams`:读取 clip 偏好(深度/组件)并更新 VideoParams。 -- `PixelFormatFromOfxDepth`:OFX bit depth 字符串 → 内部 PixelFormat。 -- `OfxDepthFromPixelFormat`:内部 PixelFormat → OFX bit depth 字符串。 -- `ChannelCountFromOfxComponent`:OFX components 字符串 → 通道数。 -- `OfxComponentsFromChannels`:通道数 → OFX components 字符串。 -- `EffectSupportsPixelDepth`:检查插件是否支持指定像素深度。 -- `ClipSupportsComponents`:检查 clip 是否支持指定组件格式。 -- `ConversionCost`:估算源参数到目标参数的转换代价,用于排序。 -- `ParamsConvertible`:判断目标参数能否映射为可用的 AVPixelFormat。 -- `ConvertTextureForClip`:结合插件能力选择输入格式并执行转换。 -- `create_avframe_from_ofx_image`:从 OFX Image 复制数据到 AVFrame(按图像属性推导格式)。 -- `create_avframe_from_ofx_image_with_params`:按指定 VideoParams 复制 OFX Image 到 AVFrame。 -- `GetDestinationAVPixelFormat`:将 VideoParams 映射为最终输出 AVPixelFormat。 -- `GetRenderFieldForParams`:根据交错设置返回 OFX render field 字符串。 -- `ReadbackTextureToFrame`:从 GPU 纹理回读到 AVFrame(必要时做格式转换)。 -- `olive::plugin::detail::BytesToPixels`:字节行跨度 → 像素行跨度。 -- `ConvertFrameIfNeeded`:必要时将 AVFrame 转换为目标 VideoParams 对应格式。 -- `LinesizeToPixels`:字节行跨度 → 像素行跨度。 -- `ConvertTextureForParams`:将纹理转换为指定 VideoParams(CPU 路径,必要时回读)。 -- `PluginIdForInstance`:安全获取插件标识符,便于日志输出。 -- `LogOfxFailure`:统一 OFX 调用失败日志输出。 -- `LogClipState`:输出 clip 声明属性与 VideoParams,用于定位格式不一致。 -- `LogImageProps`:输出 OFX Image 属性(深度/组件/行跨度/边界)。 -- `MarkRenderFailure`:渲染失败时标记目标画面(紫色)。 -- `PluginRenderer::RenderPlugin`:执行 OFX 插件渲染全流程。 -- `PluginRenderer::AttachOutputTexture`:绑定输出纹理到 OFX GL 输出路径。 -- `PluginRenderer::DetachOutputTexture`:解除 OFX GL 输出绑定。 diff --git a/docs/zh/plans/README.md b/docs/zh/plans/README.md index 9b7981b7b..ba441ce70 100644 --- a/docs/zh/plans/README.md +++ b/docs/zh/plans/README.md @@ -7,25 +7,10 @@ | 文档 | 内容 | 启动前提 | |---|---|---| -| [`riir.md`](riir.md) | **RIIR 绞杀者模式执行计划**:C ABI 迁移完成后,把 liboakengine 安全拆成若干小模块,再逐个用 Rust 重写;含 API 冻结保证、模块图、六步流程与验证门禁 | C ABI 迁移战役验收完成 | -| [`riir/`](riir/) | **模块拆分执行手册**(riir.md 第一阶段落地):00 总览 → 01 双层适配器规范 → 02 依赖矩阵与拆分顺序 → 03 测试规范 → M1-M9 逐模块手册(C API 冻结);只拆分不重写,模块间 C ABI | 已解锁(R7 完成),随时启动 | -| [`ai-agent-design.md`](ai-agent-design.md) | **AI Agent 设计**:多模态 LLM 经 MCP 调用策展工具面自动剪辑,渲染帧回喂形成"编辑→看图→再编辑"视觉闭环;含工具面、回放回路、安全与测试 | RIIR 拆分完成(面对一堆小库) | -| [`gtest-migration-guide.md`](gtest-migration-guide.md) | **测试统一到 Google Test**:把 OAK_ADD_TEST 宏框架、纯 C assert、已有 gtest 三套收敛为单一 Google Test,ctest 仅作运行器 | 已解锁(R5-R7 完成),可与 UI 改版并行 | -| [`ui-redesign-plan.md`](ui-redesign-plan.md) | **主界面 UI 改版**:依据 `design/` 三张设计图落地 10 个工作包(工具条、双监看、效果栈检查器、节点编辑器移位、电平条、状态栏等),全部文字精确定义 | 已解锁(R5-R7 完成),可与 GTest 迁移并行 | +| [`ai-agent-design.md`](ai-agent-design.md) | **AI Agent 插件设计**(已按 OPP/1 重写):多模态 LLM 作为外部插件经策展工具面自动剪辑,`render.*` 取帧回喂形成"编辑→看图→再编辑"视觉闭环;事务化编辑、双层确认、声明式 AI 面板、Mock LLM/回放夹具测试、A1–A5 里程碑 | external-plugin-system P1–P3 完成(面板需 P4) | +| [`external-plugin-system.md`](external-plugin-system.md) | **外部功能插件系统**:插件=独立进程(非库加载),JSON-RPC over stdio 控制面 + shm 数据面(泛化 M15 render-worker 传输);策展宿主 API(事务化可撤销编辑、取帧回喂 AI)、声明式/像素面双 UI 路径、能力位与确认模式;含与 ai-agent-design.md 的关系与 P1–P6 里程碑 | 已解锁(RIIR + M15 完成),随时启动 | +| [`external-plugin-protocol.md`](external-plugin-protocol.md) | **插件协议规范 OPP/1**:NDJSON 分帧 + JSON-RPC 2.0 双向信封、握手/心跳/关闭、全量方法/事件/错误码、编辑事务协议、shm 数据面(无头部无锁)、声明式与像素面 UI 协议、限流配额、版本演进规则、AI 粗剪报文示例 | 随 external-plugin-system 启动 | -## 已完成(completed/) - -C ABI 迁移战役(B1-R7,nm 557→0 + visibility 收口 3486→19)的 -全套文档归档在 [`completed/`](completed/): - -- 交接:[`c-abi-migration-handoff.md`](completed/c-abi-migration-handoff.md)(v3)、`-v4`、`-v5`、`-v6` -- 战役记录:[`facade-migration-roadmap.md`](completed/facade-migration-roadmap.md) -- R5:[`r5-app-migration-guide.md`](completed/r5-app-migration-guide.md)、 - [`r5-phase2-detailed-guide.md`](completed/r5-phase2-detailed-guide.md)、 - [`r5-phase3-final-guide.md`](completed/r5-phase3-final-guide.md)、 - [`r5-final-sprint.md`](completed/r5-final-sprint.md) -- R6/R7:[`r6-cleanup-plan.md`](completed/r6-cleanup-plan.md)、 - [`r7-pure-abi-plan.md`](completed/r7-pure-abi-plan.md) ## 其他参考 diff --git a/docs/zh/plans/ai-agent-design.md b/docs/zh/plans/ai-agent-design.md index eaa4cafdb..983aa7fbc 100644 --- a/docs/zh/plans/ai-agent-design.md +++ b/docs/zh/plans/ai-agent-design.md @@ -1,12 +1,18 @@ -# AI Agent 设计文档(RIIR 拆分后长期规划) +# AI Agent 插件设计(基于 OPP/1 外部插件系统) -> 本文是 Oak 引入 AI 能力的长期设计,**执行前提是 RIIR 绞杀者拆分完成** -> (见 [`riir.md`](riir.md))。彼时 `liboakengine.so` 已不存在,取而代之的是 -> 一组以纯 C ABI 为缝的小动态库。本文面向没有当前对话记忆的执行者,自包含。 +> 本文是 Oak 引入 AI 能力的长期设计,**已按外部插件系统重写**(旧版假设 +> RIIR 拆分后面对一堆 C ABI 小库、引擎内置 `oak-mcp-server`,已作废)。 +> AI 能力现在是一个**外部功能插件**:独立进程、经 OPP/1 协议操作 Oak。 > -> **一句话**:把多模态 LLM 当成引擎 C ABI 的**第三个一等消费者** -> (继 oak-cli、oak-render-worker 之后),用 MCP 暴露策展过的工具面, -> 用渲染管线把帧喂回给多模态模型,形成"编辑 → 看图 → 再编辑"的视觉闭环。 +> 依赖文档(冲突时以它们为准): +> - [`external-plugin-system.md`](external-plugin-system.md)——插件系统总体设计 +> (进程模型、能力位、确认模式、里程碑 P1–P6) +> - [`external-plugin-protocol.md`](external-plugin-protocol.md)——**OPP/1 协议全文** +> (方法/事件/事务/shm/UI 的冻结定义,本文引用的 §n 均指该文档) +> +> **一句话**:多模态 LLM 跑在一个独立插件进程里,经 OPP/1 的策展宿主 API +> 操作 Oak(事务化编辑、确认后执行),经 `render.get_frame/get_thumbnails` +> 的 shm 取帧通路把画面回喂模型,形成"编辑 → 看图 → 再编辑"的视觉闭环。 --- @@ -14,128 +20,235 @@ ### 1.1 前提(未满足不动工) -- RIIR 拆分战役完成:引擎已拆为 §2.1 的小库;各库导出仅 C 符号; - `oakengine_*` facade 壳稳定且全量测试绿。 -- Google Test 已是唯一测试框架;`gtest_discover_tests` 已接入。 -- 本文不改动 RIIR 既定的模块划分,只在模块化树上**新增叶子**。 +- 插件系统 **P1–P3 完成**:传输与生命周期、宿主 API 核心(事务 + + project/media/timeline/node 方法族)、取帧与导出的 shm 数据面可用。 +- AI 面板需要 **P4**(声明式 UI);无头模式(脚本/CI)只依赖 P1–P3。 +- Python SDK `oakxp`(P6 的一部分)是本插件的载体,二者同期开发、互为验证。 -### 1.2 设计铁律(继承自迁移/拆分战役) +### 1.2 设计铁律(继承旧版,按插件系统重述) -1. AI Agent **只经 C ABI** 访问引擎,一行 engine C++ 都不碰;不污染符号边界。 -2. Agent 的一切编辑动作**必须可撤销**(undoable 原语),UI 默认"确认后执行"。 -3. 不为 AI 发明新的引擎内部机制;工具面是现有 facade/小库的组合。 -4. 引擎各模块**不得新增 Qt 依赖、不得新增 QObject 信号/moc 类**。 +1. AI Agent **只经 OPP/1** 访问 Oak——插件进程内一行 Oak 代码都没有, + 不链接任何 Oak 产物(铁律:协议是唯一边界)。 +2. 一切编辑动作**必须包在 `edit.begin/commit` 事务里**(协议 §6), + 历史面板一次 Ctrl-Z 整段撤销;UI 默认"确认后执行"。 +3. 不为 AI 发明新的引擎内部机制;Agent 工具面是 OPP/1 方法的组合, + OPP/1 方法又是现有 `graphops/renderops/oak_task` 的组合。 +4. API key 绝不写入 **Oak 侧**的任何文件(`.ove` 工程、Oak 自身配置); + 允许存**插件自己的**配置文件(§5.1),环境变量仅作 CI/无头场景的覆盖项。 ## 2. 总体架构 -### 2.1 在模块化树上的位置 +### 2.1 插件进程内部分层 + +插件名 **`oak-plugin-ai`**(Python 3,实现语言与发行形态的论证见 §2.3; +参考实现即插件系统的 `examples/plugin-roughcut` 的完整版): ``` -app / oak-cli / oak-render-worker / oak-agent / oak-mcp-server - │ - liboakengine-facade(壳:capi + 事件 + init) - │ - ┌────────┬────────┼─────────┬──────────┐ - oaktask oakrender oakplugin oakaudio oakserialize - │ │ │ │ │ - └────────┴────┬───┴──────────┴──────────┘ - │ - oakmodel(节点图 + 项目模型 + 时间线模型) - │ - ┌────────┼─────────┐ - oakcodec liboakcore oakbackend(GPU 插件) - │ - ffmpeg_bridge +┌─ Oak 主进程 ────────────────────────────────┐ +│ oak-plugin-host(P1–P4 提供) │ +│ ├ OPP/1 控制面(JSON-RPC over stdio) │ +│ └ shm down/up 区域 │ +└───────┬─────────────────────────────────────┘ + │ stdio + shm +┌───────┴─── oak-plugin-ai(独立进程)────────┐ +│ ⑤ LLMProvider:Claude/GPT │ llama.cpp 本地 │ +│ ④ Agent 编排:对话 loop、工具调用、视觉闭环 │ +│ ③ 工具适配层:OPP 方法 → LLM tool schema │ +│ ② 会话状态:事务令牌、帧缓存、操作日志 │ +│ ① oakxp SDK:分帧/收发/shm attach/帧→PNG │ +└─────────────────────────────────────────────┘ ``` -**新增三个叶子组件**(与 oak-cli、oak-render-worker 平级,都是纯消费者): - -- **`oak-mcp-server`**:把策展过的工具面经 MCP 暴露给任何 LLM 客户端。 -- **`oak-agent`**:无头 Agent 运行时(对话编排 + 视觉闭环),供脚本/CI/本地使用。 -- **editor AI 面板**:app 内的聊天/操作日志/确认界面,与 `oak-agent` 复用同一工具面。 - -AI 功能**不进入** oakmodel、oakrender 等引擎模块,引擎核心对 AI 无感知。 +- 插件可以**纯无头**跑(CI、批处理脚本:spawn 后不注册面板,只走工具面); + 注册面板时才是用户可见的"AI 剪辑助手"。 +- **MCP 的位置**:如要让外部 LLM 客户端(Claude Desktop、agent 框架)直连, + 由**插件自己**在进程内起一个 MCP server,把 §3 的工具面按 MCP 再暴露一次。 + Oak 内核始终对 AI、对 MCP 无感知——这是与旧版"引擎内置 oak-mcp-server" + 的根本区别。 ### 2.2 视觉闭环(本设计的核心) ``` -多模态 LLM ──► oak-agent ──► facade/小库执行编辑 ──► 渲染取帧 ──► PNG ──► 回喂 LLM - ▲ │ - └──────────────── 看图判断(效果/切点/内容定位) ◄───────────────┘ +多模态 LLM ──► Agent 编排 ──► OPP 事务化编辑 ──► render.get_frame ──► PNG ──► 回喂 LLM + ▲ │ + └────────────────── 看图判断(效果/切点/内容定位) ◄─────────────────────────┘ ``` -- **验证式**:每次编辑后取一帧,LLM 判断"效果对不对"。 -- **内容感知式**:沿时间线批量取缩略图拼 contact sheet,LLM 扫图定位 - ("人何时进画面""哪里该切"),Agent 据此下刀——自动粗剪/打点的雏形。 -- **连续回放**:经 playback 族起范围播放,按间隔采样帧。 +- **验证式**:`edit.commit` 后立即 `render.get_frame` 取切口/效果帧,LLM 判断 + "效果对不对",不对则 `edit.undo` 或追加修正事务。 +- **内容感知式**:`render.get_thumbnails(range, count≤64)` 等间隔采样拼 + contact sheet,LLM 扫图定位("人何时进画面""哪里该切"),据此下刀—— + 自动粗剪/打点的雏形。 +- **连续回放**:`playback.play` + `playback.playhead_moved` 事件(≤30 Hz)+ + 按间隔 `get_frame` 采样。注意受 §12 限流约束(默认 8 帧/s),采样间隔 + 不得小于限流周期。 +- 完整报文流水示例见协议文档 §14(握手→缩略图→事务下刀→验证帧→事件), + 可直接作为本插件的集成测试夹具。 -## 3. 工具面(策展,非全量 facade) +### 2.3 实现语言与跨宿主复用(决策已定) -**不暴露全部 ~200+ facade 函数**,而是策展约 25 个高层工具,每个是 facade/小库 -的组合。`oak-mcp-server` 内部就是一个薄模块,链接 facade 壳与相关小库。 +**定为 Python,解释器嵌入式发行**:插件包内含私有 CPython(PyInstaller 或 +python-build-standalone),`.oakplugin` 单包交付,用户无需自行安装 Python。 +插件以 **GPLv3** 开源发布;作为独立进程经 stdio/JSON-RPC 与 Oak 通信, +许可证选择不影响 Oak 本体。 -| 工具 | 落到哪个库 | 说明 | +选 Python 而非 Rust 的理由: + +1. **性能无关**:插件只做协议编解码、LLM 编排、PNG 编码(Pillow 为 C 实现); + 延迟大头是 LLM 往返(秒级)与 Oak 侧渲染(与插件语言无关),插件进程 + 没有任何重计算。 +2. **跨宿主复用**:同一套 AI 能力规划覆盖 Oak / DaVinci Resolve / Premiere, + 但三家宿主的扩展 API 语言各异——Resolve 是 Python/Lua;**Premiere 没有 + Python 入口**(UXP/JavaScript 面板 + C++ SDK)。因此正确结构不是 + "一种语言通吃",而是**一份与宿主无关的 AI core + 各宿主薄适配层**: + +``` +┌─ AI core(宿主无关,一份代码)───────────────┐ +│ Agent 编排 / LLMProvider / tool schema │ +│ 提示词策略 / 会话状态 │ +└──────┬───────────┬──────────────┬───────────┘ + Oak 适配层 Resolve 适配层 Premiere 适配层 + (OPP/1, (Resolve (UXP/JS 面板 → + oakxp SDK) Python API, localhost 调 + 直接 import) core 本地服务) +``` + + - Oak 适配层 = oakxp SDK(OPP/1),即本文的 `oak-plugin-ai`; + - Resolve 适配层直接 import core(Python 母语,零成本复用); + - Premiere 用 UXP 面板做壳,经 localhost 与本机 core 进程通信。 +3. **生态**:主流 LLM SDK(anthropic / openai / llama.cpp 绑定)均为 Python + 一等公民,Agent 框架与评测工具链也最全。 + +## 3. 工具面(LLM tool schema → OPP/1 方法映射) + +Agent 暴露给 LLM 的是约 20 个**策展工具**,每个是 OPP/1 方法的薄组合 +(协议 §8 是方法全文,下表"OPP 方法"列即最终调用): + +| Agent 工具 | OPP 方法 | 说明 | |---|---|---| -| `create_project` / `open_project` / `save_project` | facade 壳 + oakmodel | 工程生命周期 | -| `probe_media` / `import_footage` / `get_media_info` | oakcodec + facade 壳 | 媒体探测与导入 | -| `add_track` / `add_clip` / `trim_clip` / `ripple` / `add_transition` / `add_marker` | oakmodel(经 facade 时间线族) | 时间线编辑 | -| `add_effect(effect_id)` / `set_param` / `set_keyframe` | oakmodel(经 facade node 族) | 节点与关键帧 | -| `apply_lut` / `set_color_transform` | oakmodel + facade color 族 | 调色 | -| `get_frame(time)` / `get_thumbnails(range,n)` / `get_audio_levels` | oakrender + oakbackend | **视觉闭环的取帧口** | -| `export_render(params)` | oaktask + oakcodec | 导出 | +| `open_project` / `save_project` | `project.open/save` | 工程生命周期(事务 + 确认类) | +| `get_project_overview` | `project.get_info` + `timeline.get_structure` | 一次返回序列/轨道/块树,供 LLM 建立上下文 | +| `probe_media` / `import_footage` / `list_footage` | `media.probe/import_footage/list_footage` | 媒体探测与导入 | +| `add_track` / `place_clip` / `split_clip` / `trim_clip` / `move_clip` / `ripple_delete` / `add_transition` / `add_marker` | `timeline.*` | 时间线编辑,全部在事务内 | +| `add_effect` / `set_param` / `set_keyframe` / `list_effects` | `node.add_effect/set_param/set_keyframe/list_types` + `get_params` | 效果与关键帧;`get_params` 的 min/max/choices 回填进 tool schema,约束 LLM 出参 | +| `get_frame(time)` / `scan_timeline(range,n)` / `get_audio_levels` | `render.get_frame/get_thumbnails/get_audio_levels` | **视觉闭环取帧口** | +| `play` / `pause` / `seek` | `playback.*` | 回放控制 | +| `export_render(preset)` | `export.start` + `export.progress/done` 事件 | 导出(确认类) | +| `undo_last_action` | `edit.undo` | 仅用户明确要求时调用(协议 §6 规则 5) | -**取帧→PNG 通路**(视觉闭环关键路径): -`get_frame` 经 oakrender 的预览请求得到 RGBA 帧(POD:`宽/高/字节流`), -再经 oakcodec 的 OIIO 编码器出 PNG,base64 后作为图片消息发给 LLM。 -缩略图用同一路径降采样,多张拼 contact sheet。 +**事务编排是适配层的职责,不暴露给 LLM**:LLM 的一次"动作"(可能含多个 +`timeline.*` 调用)由适配层包成一个事务——先 `edit.begin(label=LLM 动作摘要)`, +串行执行(协议保证同事务内按到达顺序),任一失败则 `edit.abort` 并把错误 +回喂 LLM,全成功才 `commit`。LLM 看不到 `txn` 令牌,从根上避免"忘记 commit" +"嵌套事务"这类误用。 -## 4. 协议:MCP(Model Context Protocol) +**取帧→PNG 通路**(关键路径):`render.get_frame` 返回 `FrameRef`(shm 形态: +`bgra8` + 槽位号),SDK `frame.to_png()`(Pillow)编码 → base64 → 作为图片 +消息发给 LLM;随后**立即 `shm.release`**——批量扫描时必须流水线化释放, +否则 8 槽耗尽触发 `SHM_EXHAUSTED`(协议 §10.3)。小图(≤64 KiB)Oak 可能 +直接 inline PNG,SDK 对两种形态透明。 -工具协议**定为 MCP**,理由: +## 4. AI 面板(声明式 UI,协议 §11.1) -- render-worker 已在用 **NDJSON over stdin/stdout 的 IPC**——MCP 本质是该模式 - 的标准化,实现路径一致。 -- 暴露成 `oak-mcp-server` 后,**外部 LLM 客户端(Claude Desktop、各类 agent - 框架)可直接连接复用**,无需自研对话编排。 -- `oak-agent` 与 editor AI 面板都连同一个 MCP server,**一份工具面,多处消费**。 +面板在 `session.hello.panels` 声明 `"ui":"declarative"`,控件树: -## 5. 模型层 +``` +column +├── chat_log #log 对话与操作日志(用户/助手/系统三角色) +├── list #pending 待确认动作清单(确认模式,见 §5) +├── row +│ ├── text_input #prompt 剪辑意图输入 +│ └── button #send 执行 +└── progress #job 扫描/导出进度 +``` -抽象 `LLMProvider` 接口(输入:消息 + 图片;输出:文本 + tool_calls),两个后端: +- 交互经 `ui.event` 上行(`submit`/`click`/`select`),插件用 + `ui.set_props` 增量追加 `chat_log` 条目、更新进度。 +- **"撤销整段会话"**:面板放一个按钮,逐个 `edit.undo` 回滚本会话提交的 + 事务(插件在自己的会话状态里记事务顺序)。 +- 面板被关闭会收到 `panel_closed`,重开收到 `panel_shown` 时重发 + `ui.set_tree` 恢复(协议 §11.1)。 +- 像素面 UI(§11.2)本插件**不用**——聊天面板声明式足够。 -- **云端**:Claude / GPT 多模态(效果优先)。 +## 5. 模型层与安全 + +### 5.1 LLMProvider + +抽象接口(输入:消息 + 图片;输出:文本 + tool_calls),后端: + +- **云端 BYOK**:Claude / GPT 多模态,用户自带 key(效果优先)。 - **本地**:llama.cpp 跑 Qwen-VL / LLaVA 类多模态模型(隐私、离线优先)。 +- **自定义 endpoint**(OpenAI 兼容接口):把 provider 指向任何兼容 + `/v1/chat/completions` 的服务(自建代理、企业网关等)——为后续接入 + 托管服务预留通用通路,不绑定特定厂商。 -API key 只走环境变量,**绝不写入 config / 工程文件**。无 key 时优雅降级为 -"仅本地工具"(仍可用 MCP,但不做对话编排)。 +API key 存**插件自己的配置文件**(如 `~/.oak/plugins/oak-plugin-ai/config.toml`), +面板提供密钥输入框(`text_input`),用户无需手配环境变量;环境变量只作 +CI/无头场景的覆盖项(优先级:环境变量 > 配置文件)。配置文件权限 0600、 +不进版本库。存插件自己的配置是插件的内部事务,Oak 不感知——铁律 4 约束的 +只是 **Oak 侧**的文件。无 key 时优雅降级:面板仍可用,但只做"工具说明 + +手动执行",不做对话编排。 -## 6. 安全 +### 5.2 能力位与确认(协议 §7 的具体化) -- **可撤销**:所有编辑走 undoable 原语;AI 面板提供"撤销整段会话"。 -- **确认模式**:默认每次 Agent 动作需用户确认才 apply;可切换自动模式。 -- **沙箱会话**:Agent 默认在临时工程中操作,用户接受后才落盘到真实工程。 -- **资源**:取帧/扫描限帧率与分辨率上限,防止批量取帧拖垮渲染进程。 +manifest 声明**最小必要集**: -## 7. 可测试(与项目风格一致) +```toml +capabilities = ["project.read", "media.read", "media.import", + "timeline.read", "timeline.edit", "node.read", "node.edit", + "render.frame", "playback", "export", "ui.panel"] +``` -1. **Mock LLM server**:录制/回放 tool_call 序列与固定回复,让 Agent loop 在 - CI 无 key 无网络跑通(Google Test)。 -2. **黄金帧校验**:复用 render-worker 端到端 harness(真实渲染 ≥2 帧 + - 像素非全黑 + 一致性断言),验证"Agent 的编辑确实改变了画面"。 -3. **会话回放**:tool_call + 帧哈希落盘日志,可回放复现、可作测试夹具。 +双层确认,职责分清: -## 8. 里程碑(RIIR 完成后启动) +1. **Oak 协议层**(§7.3):`timeline.edit` 等确认类方法默认弹窗 + "插件 oak-plugin-ai 请求:split_clip …"。用户可选"本会话内允许"。 +2. **插件会话层**:Agent 把 LLM 规划出的整段动作先列入 `#pending` 清单, + 用户点"执行"才发事务——这是体验层确认,与协议层弹窗不冲突: + 建议插件引导用户在 Oak 侧对本插件设"本会话允许",确认交互集中在面板内。 -1. **M1 工具面**:`oak-mcp-server`(facade → MCP,~25 工具)+ 取帧→PNG 通路。 -2. **M2 无头闭环**:Mock LLM + `oak-agent`,跑通"LLM→工具→取帧→回喂", - CI 可测(无网络)。 -3. **M3 AI 面板**:editor 内聊天 + 操作日志 + 确认模式 + 撤销会话。 -4. **M4 本地模型与内容感知**:llama.cpp provider、时间线扫描打点、自动粗剪。 +### 5.3 其他安全约束 -## 9. 风险与边界(明确不做) +- **限流遵守**:扫描采样按握手 `limits` 下发的配额规划(默认 8 帧/s、 + 短边 ≤1080),收到 `RATE_LIMITED` 按 `retry_after_ms` 退避,不得重试轰炸。 +- **沙箱会话**(可选增强):对破坏性大改,Agent 可先 `project.save` 副本到 + 临时路径操作,用户接受后再回真实工程。有了事务 + 整段撤销后,此项降为 + 可选,默认不启用。 +- 插件崩溃不丢编辑:已 commit 的事务都在 UndoStack 里,未决事务 Oak 自动 + abort(协议 §6 规则 4)——AI 死在哪都不会留下半截剪辑。 -- **不**把 LLM/推理放进 oakmodel 或任何引擎模块(引擎对 AI 无感知)。 -- **不**为 AI 绕过 C ABI 直接调 engine C++(边界不污染)。 -- **不**把 API key 落盘到工程/config。 -- **不**让取帧回路阻塞 GUI 线程(取帧走渲染/后台路径,UI marshal 回主线程)。 -- 第三方大模型客户端的接入细节(OAuth、计费、配额)**超出本文范围**,按需另立文档。 +## 6. 可测试(与项目风格一致) + +1. **Mock LLM server**:录制/回放 tool_call 序列与固定回复,Agent loop 在 + CI 无 key 无网络跑通。 +2. **Mock Oak(协议级)**:`oakxp` SDK 自带回放 harness——把协议文档 §14 的 + 报文流水当夹具,插件不连真 Oak 也能单测工具适配层与事务编排。 +3. **黄金帧校验**:连真 Oak 的端到端测试复用 render-worker harness + (真实渲染 ≥2 帧 + 像素非全黑 + 一致性断言),验证"Agent 的编辑确实 + 改变了画面"。 +4. **会话回放**:tool_call + 帧哈希落盘日志,可回放复现、可作测试夹具。 + +## 7. 里程碑(对齐插件系统 P1–P6) + +| 里程碑 | 内容 | 依赖 | 验收 | +|---|---|---|---| +| **A1 骨架** | `oakxp` SDK + 握手/心跳/重连;插件注册空面板 | P1、P4 | 杀掉插件 Oak 不崩、面板徽标与重启正常 | +| **A2 工具适配层** | §3 全表映射 + 事务编排(自动 begin/commit/abort)+ 取帧→PNG 通路 | P2、P3 | Mock Oak 夹具全绿;真 Oak 上"导入→铺轨→切开→删除→加效果"可整段撤销 | +| **A3 无头闭环** | Agent 编排 + LLMProvider + Mock LLM | A2 | CI 无网络跑通"LLM→工具→取帧→回喂";黄金帧校验过 | +| **A4 AI 面板** | §4 面板 + 待确认清单 + 撤销会话 | A3 | 真机对话粗剪一段素材,确认/撤销交互完整 | +| **A5 内容感知** | contact sheet 扫描打点、自动粗剪策略、本地模型 provider | A4 | 对 10 分钟素材自动出粗剪版,人工抽检切点可用 | + +## 8. 风险与边界(明确不做) + +- **不**把 LLM/推理放进任何 Oak 进程或引擎模块(引擎对 AI 无感知); + MCP server 如需存在,只在插件进程内。 +- **不**绕过 OPP/1 访问 Oak(协议是唯一边界);LLM 不直接接触 `txn` + 令牌(适配层封装事务)。 +- **不**把 API key 写入 Oak 工程文件或 Oak 自身配置(插件自己的配置文件 + 除外,见 §5.1)。 +- **不**让取帧回路阻塞 Oak GUI 线程——`render.*` 本来就走引擎 ticket/进程池 + 路径(协议 §8.6),插件侧并发流水线化而不是串行等帧。 +- **不**突破协议限流;需要更高帧率的"连续回放分析"场景,先按 §13 版本 + 演进规则给协议加配额项,不在插件侧硬挤。 +- 第三方大模型客户端的接入细节(OAuth、计费、配额)**超出本文范围**, + 按需另立文档。 diff --git a/docs/zh/plans/completed/README.md b/docs/zh/plans/completed/README.md index d029963c1..248a30833 100644 --- a/docs/zh/plans/completed/README.md +++ b/docs/zh/plans/completed/README.md @@ -20,5 +20,5 @@ `r7-pure-abi-plan.md` 这些文档**只作历史参考**,其中的"当前状态/进行中"描述均已是 -过去时。后续工作在上一层:`../riir/`(模块拆分)、 -`../gtest-migration-guide.md`、`../ui-redesign-plan.md`。 +过去时。后续工作在上一层:`riir/`(模块拆分)、 +`gtest-migration-guide.md`、`ui-redesign-plan.md`。 diff --git a/docs/zh/plans/completed/c-abi-migration-handoff-v4.md b/docs/zh/plans/completed/c-abi-migration-handoff-v4.md index 1dc6a03cf..f5aa53929 100644 --- a/docs/zh/plans/completed/c-abi-migration-handoff-v4.md +++ b/docs/zh/plans/completed/c-abi-migration-handoff-v4.md @@ -81,7 +81,7 @@ cd cmake-build-debug && ctest --output-on-failure -j$(nproc) colorcodingapp、filefunctionsapp、hashstreamapp、htmlapp、xmlutilsapp、debugapp)、 各 handle 头(keyframehandle/markerhandle/cliphandle/trackhandle/colorprocessorhandle/ vieweroutpututils)、`markerpainting.*`、`app/timeline/`、`app/ui/icons/`。 -- **文档**:v3 交接文档(`c-abi-migration-handoff.md`)、RIIR 计划(`../riir.md`)、 +- **文档**:v3 交接文档(`c-abi-migration-handoff.md`)、RIIR 计划(`riir.md`)、 roadmap 批次记录(经 LocalHistory 恢复,`facade-migration-roadmap.md`)。 - **结构性改动**:B1 图标(engine 返回图标名 + app from_name 映射,已修复一致)、 B2 布局 POD(SerializedLayoutInfo 全链路,已修复一致)、B3 coreengine.h、 diff --git a/docs/zh/plans/completed/c-abi-migration-handoff-v6.md b/docs/zh/plans/completed/c-abi-migration-handoff-v6.md index e5a19b65f..7a0942aa6 100644 --- a/docs/zh/plans/completed/c-abi-migration-handoff-v6.md +++ b/docs/zh/plans/completed/c-abi-migration-handoff-v6.md @@ -102,7 +102,7 @@ UndoCommand(3)。 4. ✅ 反作弊审计:app 无 `dlfcn.h`/dlsym/QLibrary 解析 engine 符号; engine 无 inline 化(oakengine/*.h 纯 C 声明)。 5. ✅ `facade-migration-roadmap.md` 附 C R6 节已记录; - `../riir.md` §1.1 状态已更新为"边界已纯"。 + `riir.md` §1.1 状态已更新为"边界已纯"。 > 已知遗留(已论证,不泄漏符号):app 仍 include 约 40 个 engine C++ 头 > (node/render/timeline/undo/pluginSupport,用于类型与 inline 访问器), diff --git a/docs/zh/plans/eliminate-event-bridge-issues.md b/docs/zh/plans/completed/eliminate-event-bridge-issues.md similarity index 99% rename from docs/zh/plans/eliminate-event-bridge-issues.md rename to docs/zh/plans/completed/eliminate-event-bridge-issues.md index 2cc802a3d..04be9c57d 100644 --- a/docs/zh/plans/eliminate-event-bridge-issues.md +++ b/docs/zh/plans/completed/eliminate-event-bridge-issues.md @@ -34,7 +34,7 @@ infrastructure ones first — they simplify everything else). - 每完成一个 issue:删掉对应 `bridge_->subscribe` / `oakengine_event_subscribe` 调用,并在本文勾掉该条。 When done: remove the matching subscribe calls and check off the item here. -- 架构总览 / Architecture background:`docs/zh/plans/eliminate-event-bridge.md` +- 架构总览 / Architecture background:`eliminate-event-bridge.md` --- diff --git a/docs/zh/plans/eliminate-event-bridge.md b/docs/zh/plans/completed/eliminate-event-bridge.md similarity index 100% rename from docs/zh/plans/eliminate-event-bridge.md rename to docs/zh/plans/completed/eliminate-event-bridge.md diff --git a/docs/zh/plans/gtest-migration-guide.md b/docs/zh/plans/completed/gtest-migration-guide.md similarity index 99% rename from docs/zh/plans/gtest-migration-guide.md rename to docs/zh/plans/completed/gtest-migration-guide.md index 8c834d1cb..fa058a7ef 100644 --- a/docs/zh/plans/gtest-migration-guide.md +++ b/docs/zh/plans/completed/gtest-migration-guide.md @@ -132,7 +132,7 @@ TEST_F(TimelineTest, AddTrack) { - `ctest -N` 对比基线(只增不减);全量 `--output-on-failure` 绿。 - 全仓库 grep 确认无 `OAK_ADD_TEST`/`OAK_ASSERT`/`make_oakengine_test`/ `olive_add_test` 残留。 -- 更新 `docs/zh/` 相关文档与本指引标注"已完成"。 +- 更新 `../..` 相关文档与本指引标注"已完成"。 ## 6. 注意事项(别踩坑) diff --git a/docs/zh/plans/issues-9-20-dependency-plan.md b/docs/zh/plans/completed/issues-9-20-dependency-plan.md similarity index 100% rename from docs/zh/plans/issues-9-20-dependency-plan.md rename to docs/zh/plans/completed/issues-9-20-dependency-plan.md diff --git a/docs/zh/plans/completed/r6-cleanup-plan.md b/docs/zh/plans/completed/r6-cleanup-plan.md index fcab74e9c..c93f7f511 100644 --- a/docs/zh/plans/completed/r6-cleanup-plan.md +++ b/docs/zh/plans/completed/r6-cleanup-plan.md @@ -5,7 +5,7 @@ > **背景**:R5 已把 app 对 engine 的 `olive::` C++ 符号从 557 降到 58, > 剩余 58 个以"豁免清单"形式记录在 `c-abi-migration-handoff.md` §6.4。 > 本计划的目标是把它们**全部消除到 0**——这是后续 engine 模块化拆分 -> 与 Rust 重写(RIIR,见 `../riir.md`)的硬前提:C ABI 边界上不能 +> 与 Rust 重写(RIIR,见 `riir.md`)的硬前提:C ABI 边界上不能 > 残留任何 C++ 渗漏。 > > **三条红线**(违反即返工): @@ -476,7 +476,7 @@ app 侧:`manageddisplay`/`viewerdisplay` 不再 `new OpenGLRenderer`, (`grep -rn '#include "' app/ | grep -E '"(node|undo|task|render|timeline|pluginSupport)/'` 应为空或只剩极个别已论证的)。 4. `c-abi-migration-handoff.md` §6.4 豁免清单清空(改为"无豁免"), - roadmap 补 R6 批次记录,`../riir.md` 状态更新为"边界已纯"。 + roadmap 补 R6 批次记录,`riir.md` 状态更新为"边界已纯"。 ## 执行顺序与节奏建议 diff --git a/docs/zh/plans/completed/r7-pure-abi-plan.md b/docs/zh/plans/completed/r7-pure-abi-plan.md index 3a707c651..0cc7c942e 100644 --- a/docs/zh/plans/completed/r7-pure-abi-plan.md +++ b/docs/zh/plans/completed/r7-pure-abi-plan.md @@ -207,7 +207,7 @@ grep -E '"(node|render|timeline|undo|task|pluginSupport)/'`)。不产生 1. `display.h` 全文无 C++ 类型签名/契约注释;app 无 TexturePtr/FramePtr。 2. liboakengine.so ` T _Z` = 0;oak-editor ` U _ZN5olive` 保持 0。 3. 全量构建 0 error;全量 ctest 绿。 -4. 更新 `../riir.md` 状态(边界已纯 → 可进 Step 1 拆分)、 +4. 更新 `riir.md` 状态(边界已纯 → 可进 Step 1 拆分)、 `facade-migration-roadmap.md` R7 批次记录。 5. 向用户报告,由用户宣布进入 riir.md §4 的模块拆分阶段。 diff --git a/docs/zh/plans/r8-app-pure-abi-cleanup.md b/docs/zh/plans/completed/r8-app-pure-abi-cleanup.md similarity index 97% rename from docs/zh/plans/r8-app-pure-abi-cleanup.md rename to docs/zh/plans/completed/r8-app-pure-abi-cleanup.md index 2b12b9a0e..f08df3988 100644 --- a/docs/zh/plans/r8-app-pure-abi-cleanup.md +++ b/docs/zh/plans/completed/r8-app-pure-abi-cleanup.md @@ -101,7 +101,7 @@ cmake --build build && ctest --test-dir build --output-on-failure - NodeValue::Type → int, NodeValue::k_* → AppNodeValueType 常量 - keyframeproperties.h 改为前置声明 NodeKeyframe - [x] Phase 3:node/node.h + node/param.h 主体清理完成,**以双适配器(oak:: wrapper)形态落地**, - 消费侧统一经 `shared/include/oakutil/oaknode.h` 访问 engine,AppNodeInput 方案已废弃 + 消费侧统一经 `../../../../shared/include/oakutil/oaknode.h` 访问 engine,AppNodeInput 方案已废弃 (落地细节与 WRAPPER-GAP 登记见 [r8-p3-node-param-abi.md](r8-p3-node-param-abi.md) 的"落地状态"一节) - [x] Phase 4-9 + 补充批(2026-07-29 完成):node/project*、render/*、timeline/*、codec/*、audio/*、 tool/*、undo/*、pluginSupport/* 及 P3 遗留(block/track/clip/gizmo/factory 等)全部清理, @@ -110,7 +110,7 @@ cmake --build build && ctest --test-dir build --output-on-failure thumbnail/waveform/frame cache、playback cache、disk folder、sequence_track_list、 visible_block_at_time、set_length_and_media_out、node_free、footage_is_valid、 block_get_track 等) - - 新增 app 侧构件:`shared/include/oakutil/oakvideo.h`(oak::VideoParams/ColorTransform)、 + - 新增 app 侧构件:`../../../../shared/include/oakutil/oakvideo.h`(oak::VideoParams/ColorTransform)、 `app/common/`(tooltypes.h、trackreferencehandle.h、keyframetypes.h、subtitleapp.h、 serializedlayoutinfoapp.h、nodedatatypes.h、projecttypes.h、sliderdisplaytypeapp.h)、 `app/timeline/timelinecommonapp.h`(TimelineApp 枚举镜像)、 diff --git a/docs/zh/plans/r8-p3-node-param-abi.md b/docs/zh/plans/completed/r8-p3-node-param-abi.md similarity index 99% rename from docs/zh/plans/r8-p3-node-param-abi.md rename to docs/zh/plans/completed/r8-p3-node-param-abi.md index d6f2fc1d2..24d5645e2 100644 --- a/docs/zh/plans/r8-p3-node-param-abi.md +++ b/docs/zh/plans/completed/r8-p3-node-param-abi.md @@ -14,7 +14,7 @@ 本阶段最终按**双适配器**形态落地,与下文 §2.1 的原始决策不同: - 消费侧不直接调 C ABI,统一经过 C++ wrapper 层 - `shared/include/oakutil/oaknode.h`(namespace `oak`): + `../../../../shared/include/oakutil/oaknode.h`(namespace `oak`): `Node`/`Project`/`Footage`/`Input`/`Keyframe`/`KeyframeTrackRef`/`InputPair` + `NodeCategory` 枚举 + `NodeConnection`/`ContextNodeItem` 结构。 wrapper 只做转发;owned/borrowed 语义见文件头注释。 diff --git a/docs/zh/plans/riir.md b/docs/zh/plans/completed/riir.md similarity index 99% rename from docs/zh/plans/riir.md rename to docs/zh/plans/completed/riir.md index 2a7764d31..46f28ecea 100644 --- a/docs/zh/plans/riir.md +++ b/docs/zh/plans/completed/riir.md @@ -1,6 +1,6 @@ # RIIR 绞杀者模式执行计划:liboakengine 模块化拆分与渐进式 Rust 重写 -> 本文档描述在 C ABI 迁移战役(见 `completed/c-abi-migration-handoff.md`)完成之后, +> 本文档描述在 C ABI 迁移战役(见 `c-abi-migration-handoff.md`)完成之后, > 如何用绞杀者模式(Strangler Fig)把 liboakengine.so 安全地拆成若干小模块, > 再逐个重写为 Rust。 > **核心约束:每一步都可验证、可回退;任何一步失败都不影响已验证的部分。** @@ -103,7 +103,7 @@ ### Step 1 — 冻结 ABI - 评审模块对外 C ABI 头(公开 facade 已有部分直接复用;模块间内部调用需要的 - 新增内部头,按 `completed/c-abi-migration-handoff.md` §6.1 的同一套规则写:纯 C 类型、 + 新增内部头,按 `c-abi-migration-handoff.md` §6.1 的同一套规则写:纯 C 类型、 buf/size 约定、owned/borrowed 注释、错误码)。 - 用门禁脚本生成 ABI 快照(§5-G1)并入库。**此后该头的任何改动都是显式评审行为。** diff --git a/docs/zh/plans/riir/00-overview.md b/docs/zh/plans/completed/riir/00-overview.md similarity index 100% rename from docs/zh/plans/riir/00-overview.md rename to docs/zh/plans/completed/riir/00-overview.md diff --git a/docs/zh/plans/riir/01-adapter-pattern.md b/docs/zh/plans/completed/riir/01-adapter-pattern.md similarity index 100% rename from docs/zh/plans/riir/01-adapter-pattern.md rename to docs/zh/plans/completed/riir/01-adapter-pattern.md diff --git a/docs/zh/plans/riir/02-modules-and-order.md b/docs/zh/plans/completed/riir/02-modules-and-order.md similarity index 100% rename from docs/zh/plans/riir/02-modules-and-order.md rename to docs/zh/plans/completed/riir/02-modules-and-order.md diff --git a/docs/zh/plans/riir/03-testing.md b/docs/zh/plans/completed/riir/03-testing.md similarity index 100% rename from docs/zh/plans/riir/03-testing.md rename to docs/zh/plans/completed/riir/03-testing.md diff --git a/docs/zh/plans/riir/04-interfaces.md b/docs/zh/plans/completed/riir/04-interfaces.md similarity index 100% rename from docs/zh/plans/riir/04-interfaces.md rename to docs/zh/plans/completed/riir/04-interfaces.md diff --git a/docs/zh/plans/riir/M1-oakcommon.md b/docs/zh/plans/completed/riir/M1-oakcommon.md similarity index 100% rename from docs/zh/plans/riir/M1-oakcommon.md rename to docs/zh/plans/completed/riir/M1-oakcommon.md diff --git a/docs/zh/plans/riir/M10-oakstorage.md b/docs/zh/plans/completed/riir/M10-oakstorage.md similarity index 100% rename from docs/zh/plans/riir/M10-oakstorage.md rename to docs/zh/plans/completed/riir/M10-oakstorage.md diff --git a/docs/zh/plans/riir/M11-ofx-host.md b/docs/zh/plans/completed/riir/M11-ofx-host.md similarity index 100% rename from docs/zh/plans/riir/M11-ofx-host.md rename to docs/zh/plans/completed/riir/M11-ofx-host.md diff --git a/docs/zh/plans/riir/M12-app.md b/docs/zh/plans/completed/riir/M12-app.md similarity index 100% rename from docs/zh/plans/riir/M12-app.md rename to docs/zh/plans/completed/riir/M12-app.md diff --git a/docs/zh/plans/riir/M13-storage-live.md b/docs/zh/plans/completed/riir/M13-storage-live.md similarity index 100% rename from docs/zh/plans/riir/M13-storage-live.md rename to docs/zh/plans/completed/riir/M13-storage-live.md diff --git a/docs/zh/plans/riir/M14-direct-rlib.md b/docs/zh/plans/completed/riir/M14-direct-rlib.md similarity index 100% rename from docs/zh/plans/riir/M14-direct-rlib.md rename to docs/zh/plans/completed/riir/M14-direct-rlib.md diff --git a/docs/zh/plans/riir/M15-render-process-isolation.md b/docs/zh/plans/completed/riir/M15-render-process-isolation.md similarity index 100% rename from docs/zh/plans/riir/M15-render-process-isolation.md rename to docs/zh/plans/completed/riir/M15-render-process-isolation.md diff --git a/docs/zh/plans/riir/M2-oakundo.md b/docs/zh/plans/completed/riir/M2-oakundo.md similarity index 100% rename from docs/zh/plans/riir/M2-oakundo.md rename to docs/zh/plans/completed/riir/M2-oakundo.md diff --git a/docs/zh/plans/riir/M3-oaknode.md b/docs/zh/plans/completed/riir/M3-oaknode.md similarity index 100% rename from docs/zh/plans/riir/M3-oaknode.md rename to docs/zh/plans/completed/riir/M3-oaknode.md diff --git a/docs/zh/plans/riir/M4-oaktimeline.md b/docs/zh/plans/completed/riir/M4-oaktimeline.md similarity index 100% rename from docs/zh/plans/riir/M4-oaktimeline.md rename to docs/zh/plans/completed/riir/M4-oaktimeline.md diff --git a/docs/zh/plans/riir/M5-oakcodec.md b/docs/zh/plans/completed/riir/M5-oakcodec.md similarity index 100% rename from docs/zh/plans/riir/M5-oakcodec.md rename to docs/zh/plans/completed/riir/M5-oakcodec.md diff --git a/docs/zh/plans/riir/M6-oakaudio.md b/docs/zh/plans/completed/riir/M6-oakaudio.md similarity index 100% rename from docs/zh/plans/riir/M6-oakaudio.md rename to docs/zh/plans/completed/riir/M6-oakaudio.md diff --git a/docs/zh/plans/riir/M7-oakrender.md b/docs/zh/plans/completed/riir/M7-oakrender.md similarity index 99% rename from docs/zh/plans/riir/M7-oakrender.md rename to docs/zh/plans/completed/riir/M7-oakrender.md index c88ddc370..a8d8e8646 100644 --- a/docs/zh/plans/riir/M7-oakrender.md +++ b/docs/zh/plans/completed/riir/M7-oakrender.md @@ -22,7 +22,7 @@ oakrender/ ### 2.1 `oakrender/renderer.h`(渲染器/纹理/blit) -签名照 R7-A(`docs/zh/plans/completed/r7-pure-abi-plan.md` §A.2)的 display.h 重写版 +签名照 R7-A(`../r7-pure-abi-plan.md` §A.2)的 display.h 重写版 **原样采用**——R7-A 先做的话,M7 直接把它从 facade 层搬进 oakrender 并改前缀 `oakrender_display_*`;本表不重复,以 R7-A 为准。 补充后端管理: diff --git a/docs/zh/plans/riir/M8-oaktask.md b/docs/zh/plans/completed/riir/M8-oaktask.md similarity index 100% rename from docs/zh/plans/riir/M8-oaktask.md rename to docs/zh/plans/completed/riir/M8-oaktask.md diff --git a/docs/zh/plans/riir/M9-oakplugin.md b/docs/zh/plans/completed/riir/M9-oakplugin.md similarity index 100% rename from docs/zh/plans/riir/M9-oakplugin.md rename to docs/zh/plans/completed/riir/M9-oakplugin.md diff --git a/docs/zh/plans/riir/notes.md b/docs/zh/plans/completed/riir/notes.md similarity index 100% rename from docs/zh/plans/riir/notes.md rename to docs/zh/plans/completed/riir/notes.md diff --git a/docs/zh/plans/riir/single-lib.md b/docs/zh/plans/completed/riir/single-lib.md similarity index 100% rename from docs/zh/plans/riir/single-lib.md rename to docs/zh/plans/completed/riir/single-lib.md diff --git a/docs/zh/plans/ui-redesign-plan.md b/docs/zh/plans/completed/ui-redesign-plan.md similarity index 97% rename from docs/zh/plans/ui-redesign-plan.md rename to docs/zh/plans/completed/ui-redesign-plan.md index 163ed6531..f593ce6ed 100644 --- a/docs/zh/plans/ui-redesign-plan.md +++ b/docs/zh/plans/completed/ui-redesign-plan.md @@ -1,10 +1,10 @@ # Oak 主界面 UI 改版计划 > 本文是主界面重新设计的执行手册,面向 DeepSeek Flash(**不识字图,本文全部 -> 用文字精确定义目标形态**)。详细程度对齐 `completed/r5-app-migration-guide.md`。 +> 用文字精确定义目标形态**)。详细程度对齐 `r5-app-migration-guide.md`。 > 工作分支:`c-abi-migration`。**启动前提:R5(C ABI app 侧迁移)验收完成之 > 后**;启动后可与 Google Test 统一迁移并行——协调规则见 §2。 -> 依据:`design/Oak-UI设计图-主界面-标注版.png`、`...-效果栈版.png`、 +> 依据:`../../../../design/Oak-UI设计图-主界面-标注版.png`、`...-效果栈版.png`、 > `...-节点编辑器版.png`(共 10 项关键改动,本文逐项落地)。 --- @@ -198,7 +198,7 @@ R5 已验收完成(§2.1),无错峰约束。建议先做无依赖的布局 ## 5. 测试要求(沿用项目规则) -- 所有测试用 **Google Test**(`CONTRIBUTING.md` 已立规)。 +- 所有测试用 **Google Test**(`../../../../CONTRIBUTING.md` 已立规)。 - 检查器卡片模型、效果栈↔节点图数据一致性、轨道头控件状态、状态栏信息、 工具条功能:补 Google Test 用例(`tests/gtest/`,`gtest_discover_tests`)。 - UI 行为改动以现有 `olive-gtest` 不回归为底线;新增可测逻辑(卡片模型、 @@ -210,4 +210,4 @@ R5 已验收完成(§2.1),无错峰约束。建议先做无依赖的布局 - 不改 `oakengine_*` 公共 API(见 `riir.md` 的 API 冻结保证)。 - 不动 engine 内部实现、不动 R5 的 facade 工作。 - 不重写底层渲染/播放路径;本计划只改 UI 布局、容器与交互。 -- 不做 AI 相关 UI(属 `ai-agent-design.md` 范围,另行)。 +- 不做 AI 相关 UI(属 `../ai-agent-design.md` 范围,另行)。 diff --git a/docs/zh/v04-color-audio-performance-manual-test-plan.md b/docs/zh/plans/completed/v04-color-audio-performance-manual-test-plan.md similarity index 100% rename from docs/zh/v04-color-audio-performance-manual-test-plan.md rename to docs/zh/plans/completed/v04-color-audio-performance-manual-test-plan.md diff --git a/docs/zh/plans/external-plugin-protocol.md b/docs/zh/plans/external-plugin-protocol.md new file mode 100644 index 000000000..41a70d324 --- /dev/null +++ b/docs/zh/plans/external-plugin-protocol.md @@ -0,0 +1,548 @@ +# Oak 外部插件协议规范(OPP/1) + +> 本文是 [`external-plugin-system.md`](external-plugin-system.md) 的**协议全文**, +> 冻结到可实现、可写 SDK 的粒度:传输分帧、消息信封、握手、全部 RPC 方法与事件、 +> 错误码、shm 数据面布局、UI 协议。实现(`oak-plugin-host`、`oakxp-c`、`oakxp` +> Python 包)以本文为准;与设计文档冲突时**以本文为准**。 +> +> **版本**:协议主版本 `1`(`"api": 1`)。同一主版本内只增不删(§13)。 +> +> **面向**:`oak-plugin-host` 实现者、插件 SDK 作者、插件作者。 + +--- + +## 1. 传输层与分帧 + +- 通道:插件进程的 `stdin`(Oak→plugin)与 `stdout`(plugin→Oak),全双工。 +- 分帧:**NDJSON**——每条消息是**一行** UTF-8 JSON,以 `\n` 结尾;消息内 + 不得出现裸换行(JSON 序列化默认满足)。禁用 BOM。 +- 消息大小上限 **16 MiB**;超限 Oak 直接判定协议错误并杀死插件。 +- **`stdout` 纪律**:只允许协议消息。插件日志走 `stderr`(Oak 捕获进日志面板) + 或 `session.log`(§8.1)。SDK 必须在初始化时把第三方库的 stdout 输出重定向 + 到 stderr。 +- 关闭语义:Oak 关闭插件 stdin 写端 = 要求插件退出(等价于收到 + `session.shutdown` 后的超时强杀,见 §4.4)。 + +## 2. 消息信封(JSON-RPC 2.0) + +严格遵循 JSON-RPC 2.0,**双向**:两方都可以发 Request 与 Notification。 + +```json +// Request(期望响应) +{"jsonrpc":"2.0","id":42,"method":"timeline.split_clip","params":{...}} +// Response 成功 +{"jsonrpc":"2.0","id":42,"result":{...}} +// Response 失败 +{"jsonrpc":"2.0","id":42,"error":{"code":-32001,"message":"not in edit transaction"}} +// Notification(无 id,无响应) +{"jsonrpc":"2.0","method":"playback.playhead_moved","params":{...}} +``` + +- `id`:字符串或整数,由**发送方**自定命名空间(同一连接上两方的 id 可能 + 撞车,接收方配对时只看自己发出的 id——标准行为)。 +- 允许任意数量的 in-flight 请求;**同一事务(§6)内的变更请求,Oak 严格按 + 到达顺序串行执行**。其余请求不保证相对顺序。 +- `params` 一律为对象(不用位置参数)。 +- 需要用户确认的请求(§7.3),Oak 在用户裁决前**不返回响应**;插件不得假设 + 超时,SDK 默认请求超时设为 120 s。 + +## 3. 错误码 + +标准码(-32700/-32600/-32601/-32602/-32603)按 JSON-RPC 规范。应用码占用 +JSON-RPC 保留的 server-error 段: + +| code | 常量 | 含义 | +|---|---|---| +| -32000 | `CAPABILITY_DENIED` | 插件无此方法所需能力位 | +| -32001 | `NOT_IN_TRANSACTION` | 变更方法缺少有效 `txn` | +| -32002 | `TRANSACTION_CONFLICT` | 事务被其他持有者占用 | +| -32003 | `ENTITY_NOT_FOUND` | id 失效(删除/工程重载后),`data.entity` 带原 id | +| -32004 | `RATE_LIMITED` | 触发限流,`data.retry_after_ms` 给重试间隔 | +| -32005 | `SHM_EXHAUSTED` | shm 池无空闲槽,先 `shm.release` | +| -32006 | `CONFIRMATION_DENIED` | 用户在确认弹窗中拒绝 | +| -32007 | `FRAME_TOO_LARGE` | 请求帧超过槽容量,调小 `max_size` | +| -32008 | `INVALID_STATE` | 当前状态不允许(如无打开的工程) | + +`error.data` 可选,结构化附加信息(见上表)。Oak 侧合成错误(插件进程已死、 +握手失败)不进协议,直接体现在 Oak 的插件管理器 UI。 + +## 4. 生命周期 + +### 4.1 握手(细化设计文档 §2.2:由插件发起) + +Oak spawn 插件后,插件必须在 **10 s** 内发出第一个消息——`session.hello`: + +```json +// plugin → Oak +{"jsonrpc":"2.0","id":1,"method":"session.hello","params":{ + "api":1, + "name":"ai-cut", + "version":"0.1.0", + "capabilities":["project.read","timeline.read","timeline.edit","render.frame","ui.panel"], + "panels":[{"id":"chat","title":"AI 剪辑","ui":"declarative"}], + "subscribe":["project.opened","timeline.structure_changed"] +}} +// Oak → plugin +{"jsonrpc":"2.0","id":1,"result":{ + "api":1, + "oak_version":"0.4.0", + "granted":["project.read","timeline.read","timeline.edit","render.frame","ui.panel"], + "features":["ui.pixel"], + "shm":{"down":{"name":"oakxp-d-1234","slots":8,"slot_bytes":16777216}} +}} +``` + +- `capabilities` 必须是 manifest 声明集的子集;`granted` 是 Oak 实际授予的 + 子集(用户可能在安装时裁剪)。插件按 `granted` 工作。 +- `features`:Oak 支持的可选特性清单,用于同主版本内的能力探测(§13)。 +- `shm.down`:Oak→plugin 方向的帧槽池(§10),握手时已创建,插件自行 attach。 +- 超时或首消息不是 `session.hello`:Oak 杀进程,标记插件启动失败。 + +### 4.2 心跳 + +握手成功后,Oak 每 **2 s** 发一次: + +```json +{"jsonrpc":"2.0","id":"ping-317","method":"session.ping"} +``` + +插件应在 **5 s** 内响应(`"result":{}`)。**连续 3 次**超时或 stdout EOF/进程 +退出 = 崩溃:有界重启(manifest `[restart]`,默认 `max=5`、退避 1 s 起倍增)。 +重启后重新走 §4.1;所有 id 与事务令牌作废,插件须重新拉取状态。 + +### 4.3 事件订阅 + +`session.hello.subscribe` 是初始订阅;运行时用: + +```json +{"method":"events.subscribe","params":{"events":["playback.playhead_moved"],"unsubscribe":["export.progress"]}} +``` + +事件目录见 §9。订阅需要对应的 read 类能力位(§7.2 各事件标注)。 + +### 4.4 关闭 + +Oak 退出或用户禁用插件: + +```json +// Oak → plugin(notification) +{"jsonrpc":"2.0","method":"session.shutdown","params":{"reason":"app_quit"}} +``` + +插件应在 **2 s** 内自行退出(保存自己的状态);超时 SIGTERM,再 2 s SIGKILL +(Windows:`TerminateProcess`)。插件主动崩溃/退出按 §4.2 崩溃路径处理。 + +## 5. 公共数据类型 + +| 类型 | JSON 表示 | 说明 | +|---|---|---| +| `Rational`(时间) | `{"num":3,"den":25}` | 秒为单位的有理数,`den>0`。全协议**唯一**时间表示 | +| `TimeRange` | `{"in":Rational,"out":Rational}` | 左闭右开 | +| `EntityId` | 不透明字符串 | 素材/序列/轨道/块/节点/事务/作业统一为字符串 id。**插件不得解析格式**;会话内稳定,工程重载后全部作废(靠 `project.opened` + 重新拉取恢复) | +| `Color` | `"#RRGGBB"` 或 `"#RRGGBBAA"` | | +| `FrameRef` | 见下 | 一帧位图的引用,两种形态 | + +`FrameRef`: + +```json +// shm 形态(默认) +{"shm":{"region":"down","slot":3},"format":"bgra8","width":1920,"height":1080, + "stride":7680,"bytes":8294400,"time":{"num":3,"den":25}} +// inline 形态(bytes ≤ 64 KiB 时 Oak 可选用) +{"inline":"base64...","format":"png","width":320,"height":180,"time":{...}} +``` + +- `format`:`"bgra8"`(shm 原始位图,行优先、顶左原点)或 `"png"`(已编码, + inline 专用)。 +- shm 形态的槽位**借用**自 `down` 池,插件用完必须 `shm.release`(§10.3), + 否则触发 `SHM_EXHAUSTED`;借用带 30 s 租约,超时 Oak 强制回收。 + +## 6. 编辑事务协议 + +一切变更方法(§8 各方法标注"事务:是")必须携带 `txn` 参数;事务令牌由 +`edit.begin` 签发: + +```json +{"id":10,"method":"edit.begin","params":{"label":"AI: 粗剪访谈片段"}} +{"id":10,"result":{"txn":"t12"}} +{"id":11,"method":"timeline.split_clip","params":{"txn":"t12","clip":"...","time":{"num":3,"den":25}}} +{"id":12,"method":"edit.commit","params":{"txn":"t12"}} +``` + +规则(钉死): + +1. **全局单持**:同一时刻全 Oak 只有一个未决事务。`edit.begin` 冲突返回 + `TRANSACTION_CONFLICT`,`data.retry_after_ms` 提示重试。插件不得长持事务 + (建议 < 5 s);Oak 对 60 s 未提交的事务强制 `abort`。 +2. `commit` = 一组 UndoCommand 压栈,历史面板显示"插件名:label",一次 + Ctrl-Z 整体撤销。`abort` = 已执行的变更逆序回滚,不留痕迹。 +3. 事务内变更按到达顺序串行执行(§2);任一变更失败,**前面已成功的保持 + 有效**,由插件决定 `commit` 还是 `abort`——Oak 不自动回滚。 +4. 崩溃时未决事务自动 `abort`:插件崩了也不会留下半截编辑。 +5. `edit.undo`/`edit.redo` 不需要 `txn`,撤销的是整个 UndoStack(包括用户 + 自己的操作)——插件应只在用户明确要求时调用。 + +## 7. 能力位 + +### 7.1 能力清单(v1 冻结) + +| 能力 | 覆盖的方法/事件 | +|---|---| +| `project.read` | `project.get_info`;`project.opened/modified/closed` 事件 | +| `project.edit` | `project.open/save`(且需事务) | +| `media.read` | `media.probe/list_footage` | +| `media.import` | `media.import_footage`(且需事务) | +| `timeline.read` | `timeline.get_structure`;`timeline.structure_changed` 事件 | +| `timeline.edit` | `timeline.*` 全部变更方法(且需事务) | +| `node.read` | `node.list_types/get_params` | +| `node.edit` | `node.add_effect/set_param/set_keyframe/remove`(且需事务) | +| `render.frame` | `render.get_frame/get_thumbnails/get_audio_levels` | +| `playback` | `playback.*`;`playback.*` 事件 | +| `export` | `export.start/cancel`;`export.*` 事件 | +| `ui.panel` | `ui.*`(声明式);`ui.event` 事件 | +| `ui.pixel` | `ui.attach_surface/frame_ready`(像素面,§11.2) | + +### 7.2 检查时机 + +`HostApi` 在每个方法入口查 `granted`;越权返回 `CAPABILITY_DENIED` 并记 +Oak 日志。事件订阅同理(订阅未授权事件返回 `CAPABILITY_DENIED`, +`data.event` 指明哪个)。 + +### 7.3 用户确认 + +`*.edit`、`media.import`、`export`、`edit.undo/redo` 属于**确认类**:Oak 弹窗 +"插件 X 请求:split_clip n17:2 @ 3/25 [允许] [本会话内允许] [拒绝]"。拒绝返回 +`CONFIRMATION_DENIED`;"本会话内允许"缓存到 Oak 会话结束。用户在插件设置里 +可把某插件整设为"自动允许"。确认类方法清单与能力位一一对应,见 §8 各方法 +"确认"列。 + +## 8. 宿主 API 方法(v1 全量) + +通用列:**事务**=是否需要 `txn`;**确认**=是否触发 §7.3 弹窗。所有方法均可 +返回 §3 通用错误,不再逐条列出。 + +### 8.1 会话 + +| 方法 | params | result | 说明 | +|---|---|---|---| +| `session.hello` | §4.1 | §4.1 | 首消息,仅此一次 | +| `session.ping` | – | `{}` | Oak→plugin 方向 | +| `session.shutdown` | `{reason}` | notification | Oak→plugin | +| `session.log` | `{level:"debug"\|"info"\|"warn"\|"error", message}` | notification | plugin→Oak,进 Oak 日志面板。高频日志请走 stderr | +| `events.subscribe` | `{events:[], unsubscribe:[]}` | `{subscribed:[]}` | §4.3 | + +### 8.2 `project.*` + +| 方法 | 事务 | 确认 | params | result | +|---|---|---|---|---| +| `project.get_info` | 否 | 否 | `{}` | `{path\|null, name, modified, sequences:[{id,name,fps:Rational,duration:Rational}]}` | +| `project.open` | 是 | 是 | `{txn, path}` | `{name}` | +| `project.save` | 是 | 是 | `{txn, path?}` | `{path}` | + +无打开工程时读取方法返回 `INVALID_STATE`。 + +### 8.3 `media.*` + +| 方法 | 事务 | 确认 | params | result | +|---|---|---|---|---| +| `media.probe` | 否 | 否 | `{path}` | `{duration:Rational, streams:[{type:"video"\|"audio"\|"subtitle", codec, width?, height?, fps?:Rational, sample_rate?, channels?}]}` | +| `media.list_footage` | 否 | 否 | `{}` | `{footage:[{id,name,path,duration:Rational}]}` | +| `media.import_footage` | 是 | 是 | `{txn, paths:[...]}` | `{footage:[{id,name,duration:Rational}]}`(跳过失败项,`errors:[{path,message}]` 单列) | + +### 8.4 `timeline.*` + +```json +// timeline.get_structure {sequence} → +{"result":{"sequence":{"id":"…","name":"访谈成片","fps":{"num":25,"den":1}, + "duration":{"num":183,"den":25}, + "tracks":[ + {"id":"…","type":"video","index":0,"clips":[ + {"id":"…","name":"A001.mp4","footage":"…", + "in":{"num":0,"den":1},"out":{"num":72,"den":25}, + "media_in":{"num":10,"den":1},"enabled":true} + ]}, + {"id":"…","type":"audio","index":0,"clips":[…]} + ]}}} +``` + +| 方法 | 事务 | 确认 | params | result | +|---|---|---|---|---| +| `timeline.get_structure` | 否 | 否 | `{sequence}` | 见上 | +| `timeline.add_track` | 是 | 是 | `{txn, sequence, type:"video"\|"audio", index?}` | `{track}` | +| `timeline.place_clip` | 是 | 是 | `{txn, sequence, track, footage, in:Rational, media_in?}` | `{clip}` | +| `timeline.split_clip` | 是 | 是 | `{txn, clip, time:Rational}` | `{clips:[id,id]}` | +| `timeline.trim_clip` | 是 | 是 | `{txn, clip, side:"in"\|"out", time:Rational}` | `{clip}` | +| `timeline.move_clip` | 是 | 是 | `{txn, clip, in:Rational, track?}` | `{clip}` | +| `timeline.delete_clip` | 是 | 是 | `{txn, clip}` | `{}` | +| `timeline.ripple_delete` | 是 | 是 | `{txn, clip}` | `{}` | +| `timeline.add_transition` | 是 | 是 | `{txn, clip, side:"in"\|"out", type, duration:Rational}` | `{transition}` | +| `timeline.add_marker` | 是 | 是 | `{txn, sequence, time:Rational, name?, color?:Color}` | `{marker}` | +| `timeline.set_workarea` | 是 | 是 | `{txn, sequence, range:TimeRange}` | `{}` | + +`time`/`in`/`out` 一律为序列时间轴上的有理秒。越界/重叠冲突返回 +`INVALID_STATE`,`data.reason` 说明。 + +### 8.5 `node.*`(效果与参数) + +| 方法 | 事务 | 确认 | params | result | +|---|---|---|---|---| +| `node.list_types` | 否 | 否 | `{category?:"effect"\|"transition"\|"all"}` | `{types:[{id,name,category}]}`(含 OFX 动态类型,id 即 OFX identifier) | +| `node.add_effect` | 是 | 是 | `{txn, clip, effect, index?}` | `{node}` | +| `node.get_params` | 否 | 否 | `{node}` | `{params:[{key,name,type:"float"\|"int"\|"bool"\|"string"\|"color"\|"vec2"\|"choice", value, default, min?, max?, choices?:[]}]}` | +| `node.set_param` | 是 | 是 | `{txn, node, key, value}` | `{}` | +| `node.set_keyframe` | 是 | 是 | `{txn, node, key, time:Rational, value}` | `{}` | +| `node.remove` | 是 | 是 | `{txn, node}` | `{}` | + +`value` 的 JSON 类型随 `type`:`float/int`→number,`bool`→boolean, +`string/choice`→string,`color`→Color,`vec2`→`[x,y]`。 + +### 8.6 `render.*`(AI 视觉闭环的取帧口) + +| 方法 | 事务 | 确认 | params | result | +|---|---|---|---|---| +| `render.get_frame` | 否 | 否 | `{sequence?, footage?, time:Rational, max_size?:{width,height}, format?:"bgra8"\|"png"}` | `{frame:FrameRef}` | +| `render.get_thumbnails` | 否 | 否 | `{sequence?, footage?, range:TimeRange, count, height?:180}` | `{frames:[FrameRef,…]}`(等间隔采样,`count` ≤ 64) | +| `render.get_audio_levels` | 否 | 否 | `{sequence, range:TimeRange, resolution?:100}` | `{channels, peaks:inline base64 float32le 数组(channels×resolution)}` | + +- `sequence` 与 `footage` 二选一,都缺省返回 `INVALID_PARAMS(-32602)`。 +- `max_size` 超槽容量(§4.1 `slot_bytes`)返回 `FRAME_TOO_LARGE`。 +- 限流(§12):默认每插件 8 帧/s、短边 ≤ 1080,超限 `RATE_LIMITED`。 +- 渲染走引擎 ticket/进程池路径,**不阻塞 GUI 线程**;典型延迟 50–500 ms, + 插件侧应并发流水线化而不是串行等帧。 + +### 8.7 `playback.*` + +| 方法 | 事务 | 确认 | params | result | +|---|---|---|---|---| +| `playback.play` | 否 | 否 | `{sequence?}` | `{}` | +| `playback.pause` | 否 | 否 | `{}` | `{}` | +| `playback.seek` | 否 | 否 | `{time:Rational}` | `{}` | +| `playback.get_state` | 否 | 否 | `{}` | `{playing, time:Rational, sequence\|null}` | + +### 8.8 `export.*` + +| 方法 | 事务 | 确认 | params | result | +|---|---|---|---|---| +| `export.start` | 否 | 是 | `{sequence, output_path, preset?:string}` | `{job}` | +| `export.cancel` | 否 | 否 | `{job}` | `{}` | + +`preset` 引用 Oak 导出预设名;自定义编码参数(分辨率/码率/封装)v1 不开放, +需要时按 §13 加 `encoding` 对象。进度经 `export.progress` 事件推送(§9)。 + +### 8.9 `shm.*` + +| 方法 | params | result | +|---|---|---| +| `shm.release` | `{slots:[{region:"down", slot:3}, …]}` | `{}`(notification 亦可) | + +详见 §10。 + +### 8.10 `ui.*` + +见 §11(声明式与像素面两条路径共用 `ui.*` 命名空间)。 + +## 9. 事件(Oak→plugin notification) + +| 事件 | 所需能力 | params | 频率 | +|---|---|---|---| +| `project.opened` | `project.read` | `{path, name}` | – | +| `project.modified` | `project.read` | `{modified}` | 状态翻转时 | +| `project.closed` | `project.read` | `{}` | – | +| `timeline.structure_changed` | `timeline.read` | `{sequence, hint:"full"\|{"clips_added":[],"clips_removed":[],"clips_moved":[]}}` | 变更合并后发,≤ 10 Hz | +| `playback.playhead_moved` | `playback` | `{sequence, time:Rational}` | ≤ 30 Hz,只发最新值 | +| `playback.state_changed` | `playback` | `{playing}` | – | +| `export.progress` | `export` | `{job, fraction:0..1, eta_ms?\|null}` | ≤ 4 Hz | +| `export.done` | `export` | `{job, ok, output_path?, error?}` | – | +| `ui.event` | `ui.panel` | §11 | 输入事件实时;pointer_move ≤ 60 Hz 合并 | + +`hint` 是优化提示:插件可永远按 `"full"` 处理(重新 `get_structure`), +`hint` 对象仅当下发增量安全时出现。 + +## 10. shm 数据面 + +### 10.1 区域与方向 + +- `down`:Oak→plugin(渲染帧)。Oak 在握手前创建,握手响应携带 + `{name, slots, slot_bytes}`;插件 `shm_open`+`mmap` 只读 attach。 +- `up`:plugin→Oak(像素面 UI 位图)。Oak 在 `ui.attach_surface` 时按需创建 + (每个像素面板一个区域),result 携带同名结构;插件可写 attach。 +- POSIX:`shm_open`/`mmap`;Windows:`CreateFileMappingW`/`MapViewOfFile`。 + 名称不带前导 `/` 的语义差异由 SDK 抹平。 + +### 10.2 无头部、无锁(钉死) + +**shm 内不放任何元数据、不放锁**。槽位布局:槽 `i` 的字节区间 +`[i*slot_bytes, (i+1)*slot_bytes)`,位图从偏移 0 开始,格式/宽/高/步长全部 +由控制面消息携带(`FrameRef` / `ui.frame_ready`)。槽位有效性由 RPC 配对界定: + +- `down`:从携带该槽的 Response/事件到达,到插件 `shm.release`(或 30 s 租约 + 到期)为止,Oak 保证不写该槽。 +- `up`:从 `ui.frame_ready` 发出,到 Oak 回 `ui.surface_ack` 为止,插件保证 + 不写该槽。 + +因为控制面与数据面一一配对,不需要 seqlock/环形缓冲那套(render-worker 的 +SPSC ring 是高频流式场景,本协议是请求-响应场景,刻意简化)。 + +### 10.3 流控 + +- `down` 池 `slots` 个槽(默认 8)。插件未释放的借用数达到 `slots` 后, + `render.*` 一律 `SHM_EXHAUSTED`。批量取帧的插件必须流水线化 release。 +- `up` 区域固定 3 槽(三缓冲)。`ui.frame_ready` 未收到 `surface_ack` 的槽 + 不得复用;3 槽全在飞行中时插件应丢弃新帧(UI 丢帧安全)。 +- 租约:`down` 借用 30 s 未 release,Oak 强制回收并记日志(视为插件 bug)。 + +## 11. UI 协议 + +### 11.1 声明式 UI + +面板在 `session.hello.panels` 声明 `"ui":"declarative"`。Oak 为其创建 +`PluginPanel`(可关闭/可停靠的 DockPanel),初始为空。 + +**控件树下发**(全量替换): + +```json +{"id":31,"method":"ui.set_tree","params":{"panel":"chat","root": + {"type":"column","gap":8,"children":[ + {"type":"chat_log","id":"log","grow":true}, + {"type":"row","gap":4,"children":[ + {"type":"text_input","id":"prompt","placeholder":"描述你的剪辑意图…","grow":true}, + {"type":"button","id":"send","text":"执行"}]}, + {"type":"progress","id":"job","visible":false} + ]}}} +``` + +**增量更新**:`ui.set_props {panel, id, props:{…}}`,只改给出的属性; +不存在的 `id` 返回 `ENTITY_NOT_FOUND`。结构性增删用全量 `set_tree` +(树规模小,不做 diff 协议)。 + +**控件目录(v1 冻结)**: + +| type | 关键 props | 事件(`kind`) | +|---|---|---| +| `column` / `row` | `gap, grow, children[]` | – | +| `label` | `text, color?` | – | +| `button` | `text, enabled?` | `click` | +| `text_input` | `text, placeholder?, enabled?` | `change{text}`, `submit{text}` | +| `text_area` | `text, readonly?` | `change{text}` | +| `list` | `items:[{id,text}], selected?` | `select{id}` | +| `chat_log` | `entries:[{role:"user"\|"assistant"\|"system", text}]`(set_props 追加) | – | +| `image` | `source:{inline_base64}` 或 `{shm:{region,slot},width,height,stride,format}` | – | +| `progress` | `fraction:0..1, indeterminate?, text?` | – | +| `slider` | `value, min, max, step?` | `change{value}` | +| `checkbox` | `checked, text` | `change{checked}` | +| `separator` / `spacer` | – | – | + +**事件上行**: + +```json +{"jsonrpc":"2.0","method":"ui.event","params": + {"panel":"chat","id":"send","kind":"click"}} +``` + +面板被用户关闭:`ui.event {panel, kind:"panel_closed"}`;Oak 重新打开时插件 +会收到 `ui.event {kind:"panel_shown"}`,插件应重发 `ui.set_tree`。 + +**通知**:`ui.notify {level:"info"\|"warn"\|"error", text}` → Oak 状态栏 toast。 + +### 11.2 像素面 UI + +面板声明 `"ui":"pixel"`(需 `ui.pixel` 能力,握手 `features` 里有才可用)。 + +```json +// 1) 建表面:Oak 创建 up 区域 +{"id":40,"method":"ui.attach_surface","params":{"panel":"paint","width":960,"height":540,"dpi":2.0}} +{"id":40,"result":{"shm":{"region":"up","name":"oakxp-u-1234-paint","slots":3, + "slot_bytes":8294400},"format":"bgra8"}} +// 2) 插件画好一帧 → 通知(notification) +{"jsonrpc":"2.0","method":"ui.frame_ready","params": + {"panel":"paint","slot":1,"width":960,"height":540,"stride":7680, + "dirty":[0,0,960,540]}} +// 3) Oak 合成完毕 → ack(notification),槽位可复用 +{"jsonrpc":"2.0","method":"ui.surface_ack","params":{"panel":"paint","slot":1}} +``` + +**输入事件下行**(`ui.event`,`id` 固定为 `"surface"`): + +| kind | params 增量 | +|---|---| +| `resize` | `{width, height, dpi}`(插件用新尺寸重画并 `frame_ready`) | +| `pointer_move` / `pointer_down` / `pointer_up` | `{x, y, button?, modifiers:["shift","ctrl",…]}`(逻辑坐标,已除 dpi) | +| `scroll` | `{x, y, dx, dy, modifiers}` | +| `key_down` / `key_up` | `{key, text?, modifiers}`(`key` 为 USB HID usage name 字符串,如 `"A"`/`"Enter"`) | +| `focus` / `blur` | `{}` | + +pointer_move 合并到 ≤ 60 Hz。IME 合成串、剪贴板、拖拽:v1 不做(设计文档 +§4.2 已声明边界)。 + +## 12. 限流与配额(v1 默认值) + +| 资源 | 默认 | 超限行为 | +|---|---|---| +| `render.get_frame` | 8 帧/s(令牌桶,burst 4) | `RATE_LIMITED` + `retry_after_ms` | +| 渲染帧短边 | ≤ 1080 px | `RATE_LIMITED`(插件调小 `max_size`) | +| `get_thumbnails` | `count` ≤ 64/次 | `INVALID_PARAMS` | +| 单条消息 | ≤ 16 MiB | 协议错误,杀进程 | +| `down` 借用 | ≤ `slots`(8) | `SHM_EXHAUSTED` | +| 未决事务时长 | ≤ 60 s | 强制 `abort` | + +配额随握手响应的 `limits` 字段下发(v1 可缺省 = 上表默认);插件以 `limits` +为准,不要硬编码。 + +## 13. 版本演进规则 + +1. `api` 主版本只在**破坏性变更**时 +1;Oak 同时支持的旧主版本数 ≥ 1。 +2. 同主版本内:只准**新增**方法/事件/可选参数/能力位;不得改语义、不得删、 + 不得把可选参数变必填。 +3. 可选能力经握手 `features` 字符串集探测(如 `"ui.pixel"`、`"thumbs.contact_sheet"`), + 插件用前必查。 +4. 插件声明的 `api` 高于 Oak 支持:握手返回 `INVALID_PARAMS`, + `data.supported_api` 给出 Oak 侧主版本,插件应降级或退出。 + +## 14. 附录:AI 粗剪会话示例(完整报文流水) + +```jsonc +// ── 握手 +→ {"jsonrpc":"2.0","id":1,"method":"session.hello","params":{ + "api":1,"name":"roughcut","version":"0.2.0", + "capabilities":["project.read","timeline.read","timeline.edit","render.frame","ui.panel"], + "panels":[{"id":"chat","title":"AI 粗剪","ui":"declarative"}], + "subscribe":["timeline.structure_changed"]}} +← {"jsonrpc":"2.0","id":1,"result":{ + "api":1,"oak_version":"0.4.0","granted":["project.read","timeline.read","timeline.edit","render.frame","ui.panel"], + "features":["ui.pixel"], + "shm":{"down":{"name":"oakxp-d-7812","slots":8,"slot_bytes":16777216}}}} + +// ── 扫时间线:取 12 张缩略图拼 contact sheet 回喂 LLM +→ {"jsonrpc":"2.0","id":2,"method":"render.get_thumbnails","params":{ + "sequence":"sq1","range":{"in":{"num":0,"den":1},"out":{"num":600,"den":1}}, + "count":12,"height":180}} +← {"jsonrpc":"2.0","id":2,"result":{"frames":[ + {"shm":{"region":"down","slot":0},"format":"bgra8","width":320,"height":180,"stride":1280,"bytes":230400,"time":{"num":0,"den":1}}, + … ]}} +→ {"jsonrpc":"2.0","id":3,"method":"shm.release","params":{"slots":[ + {"region":"down","slot":0}, …]}} + +// ── LLM 判定 83.2s–141.6s 为废片 → 事务化下刀(用户确认后执行) +→ {"jsonrpc":"2.0","id":4,"method":"edit.begin","params":{"label":"AI 粗剪:删除 83.2–141.6s 废片"}} +← {"jsonrpc":"2.0","id":4,"result":{"txn":"t7"}} +→ {"jsonrpc":"2.0","id":5,"method":"timeline.split_clip","params":{"txn":"t7","clip":"blk9","time":{"num":416,"den":5}}} +← {"jsonrpc":"2.0","id":5,"result":{"clips":["blk9","blk9b"]}} +→ {"jsonrpc":"2.0","id":6,"method":"timeline.split_clip","params":{"txn":"t7","clip":"blk9b","time":{"num":708,"den":5}}} +← {"jsonrpc":"2.0","id":6,"result":{"clips":["blk9b","blk9c"]}} +→ {"jsonrpc":"2.0","id":7,"method":"timeline.ripple_delete","params":{"txn":"t7","clip":"blk9b"}} +← {"jsonrpc":"2.0","id":7,"result":{}} +→ {"jsonrpc":"2.0","id":8,"method":"edit.commit","params":{"txn":"t7"}} +← {"jsonrpc":"2.0","id":8,"result":{}} + +// ── 视觉验证:取切口后一帧确认画面正确 +→ {"jsonrpc":"2.0","id":9,"method":"render.get_frame","params":{ + "sequence":"sq1","time":{"num":416,"den":5},"max_size":{"width":960,"height":540}}} +← {"jsonrpc":"2.0","id":9,"result":{"frame":{"shm":{"region":"down","slot":0}, + "format":"bgra8","width":960,"height":540,"stride":3840,"bytes":2073600, + "time":{"num":416,"den":5}}}} +→ {"jsonrpc":"2.0","id":10,"method":"shm.release","params":{"slots":[{"region":"down","slot":0}]}} + +// ── 结构变化推送(Oak 合并后下发) +← {"jsonrpc":"2.0","method":"timeline.structure_changed","params":{ + "sequence":"sq1","hint":{"clips_added":["blk9b","blk9c"],"clips_removed":[],"clips_moved":[]}}} +``` diff --git a/docs/zh/plans/external-plugin-system.md b/docs/zh/plans/external-plugin-system.md new file mode 100644 index 000000000..f83bf5980 --- /dev/null +++ b/docs/zh/plans/external-plugin-system.md @@ -0,0 +1,334 @@ +# 外部功能插件系统设计(进程隔离 + JSON-RPC/shm) + +> 本文是 Oak **功能性插件系统**的总体设计,面向没有当前对话记忆的执行者,自包含。 +> +> **定位**:与 `oak-plugin`(OpenFX 宿主)正交。OFX 管"效果/滤镜"这类图像处理插件; +> 本系统管"功能/工作流"插件——插件可以**调用 Oak 内部功能**(建工程、导入素材、 +> 时间线编辑、加效果、取帧、导出)并**绘制自己的 UI 面板**。旗舰用例是 AI 剪辑插件: +> 给多模态 AI 一组工具,让它自己"看"视频(取帧回喂)并执行剪辑——外部程序因此 +> 必须能完整操作 Oak。 +> +> **红线**: +> 1. 插件代码**永不进入 Oak 主进程**(不 dlopen、不链接任何 Rust 库)。一个插件 +> 一个独立进程,插件崩溃不得连带 Oak。 +> 2. 插件可以是任何语言(C++/Python/Node…),协议必须是**语言无关的文本协议 + +> 共享内存数据面**,不发明需要链接 Rust/C ABI 的绑定。 +> 3. 插件的一切编辑动作**必须可撤销**(UndoStack 事务),默认"确认后执行"。 +> 4. 复用既有基础设施,不新造轮子:渲染进程隔离(`oak-render/src/procpool.rs` + +> `oak-render/src/ipc.rs`,M15 已落地)的 **NDJSON over stdio + shm 帧槽** 模式 +> 就是本系统传输层的范本。 +> +> **协议全文**(消息信封、握手、方法/事件目录、错误码、shm 布局、UI 协议) +> 冻结在 [`external-plugin-protocol.md`](external-plugin-protocol.md)(OPP/1); +> 实现以协议文档为准。 + +--- + +## 1. 关键决策 + +### 1.1 进程模型:插件 = 独立可执行文件(推荐),而非"库 + 宿主进程加载" + +两种候选: + +- **A. 插件即进程**:每个插件是一个独立可执行文件(Python 插件则是 + `python3 main.py` 这样的启动命令),Oak 按清单(manifest)spawn,经 stdio 说话。 + 即 LSP / MCP 模型。 +- **B. 插件即库 + 通用宿主进程**:插件编译成动态库,由一个 `oak-plugin-host` + 进程 dlopen 它,宿主进程再与 Oak 通信。 + +**定为 A**,理由: + +1. **B 只是名义上更隔离**。dlopen 进宿主进程后,插件崩溃杀掉的是宿主进程, + 效果与 A 完全相同;但 B 要求宿主进程按语言分别内嵌加载器(C++ 用 dlopen, + Python 得内嵌解释器或再起子进程),复杂度显著高于 A,没有换来任何隔离收益。 +2. **A 对解释型语言天然成立**。Python/Node 插件本来就是"一个命令",B 模型下 + 反而要多包一层。 +3. **A 与仓库既有模式一致**:`oak-worker` 就是"Oak spawn 一个可执行文件 + + NDJSON 握手 + shm 附加",含崩溃检测、有界重启(`MAX_RESTARTS=5`)、握手超时。 + 插件宿主直接照搬这套生命周期管理。 +4. **协议实现在 SDK,不在宿主**。担心"每个插件重写一遍协议"用 SDK 解决: + 官方提供 C/C++ 头文件库与 Python 包(各 ~200 行,见 §6),插件作者只写 + `on_request(method, params)` 回调。 + +代价(明说):每种语言需要一个薄 SDK;stdio 单通道对极高频事件(如逐帧 +playhead 推送)有序列化开销——用事件合并/降频缓解(§4.4),不另开 socket。 + +### 1.2 IPC:JSON-RPC 2.0 over stdin/stdout(控制面)+ shm(数据面) + +- **控制面**:严格 [JSON-RPC 2.0](https://www.jsonrpc.org/specification), + NDJSON 分帧(一行一个消息,与 `oak-render/src/ipc.rs` 相同)。**双向**: + Oak→plugin 发请求(UI 事件、配置下发、shutdown),plugin→Oak 也发请求 + (调内部功能,即 §3 宿主 API),靠 `id` 配对,notification 做事件推送。 + 选 JSON-RPC 而非自定义协议:所有语言都有现成实现,且规范本身解决了 + 双向请求/通知/错误码问题。 +- **stdio 纪律**:`stdout` 只走协议消息;插件日志一律写 `stderr`,Oak 捕获后 + 进日志面板(LSP 惯例)。绝不允许第三方库污染 stdout——SDK 提供 + `redirect_stdout_to_stderr()` 之类的防护。 +- **数据面**:帧/缩略图/波形/插件 UI 位图走 POSIX shm(Windows 用 + `CreateFileMappingW`),消息体只带 `shm 名 + 槽位元数据`(宽/高/格式/步长)。 + 直接泛化 `oak-render/src/ipc.rs` 的 `SharedMemoryRegion` / `FrameSlotPool`, + 不新设计。小数据(几 KB 的缩略图)允许内联 base64,阈值建议 64 KiB。 + +``` +Oak 主进程 插件进程(每插件一个) +┌─────────────────────┐ stdio ┌──────────────────────────┐ +│ PluginHost (每插件) │◄────────►│ 插件 SDK │ +│ ├ 后台 IO 线程 │ NDJSON │ └ 插件逻辑(任意语言) │ +│ ├ 崩溃检测/有界重启 │ JSON-RPC │ │ +│ └ 调用编排到引擎线程 │ │ │ +│ HostApi 实现 ────────┼─► 编排到 oak-app 引擎线程(mpsc/gpui)│ +│ PluginPanel (gpui) │ │ │ +└─────────┬───────────┘ └────────────┬─────────────┘ + │ shm(帧槽池,双向) │ + └────────────────────────────────────┘ +``` + +--- + +## 2. 生命周期与进程管理 + +### 2.1 清单与发现 + +插件是一个目录(或 `.oakplugin` 包),内含 `plugin.toml`: + +```toml +id = "com.example.ai-cut" +name = "AI 剪辑助手" +version = "0.1.0" +api = 1 # 协议主版本,见 §2.2 + +[process] +# {plugin_dir} 由 Oak 替换;Python 插件就写解释器命令 +command = ["python3", "{plugin_dir}/main.py"] +env_passthrough = ["PATH", "HOME"] + +# 能力声明(§5),安装时向用户展示 +capabilities = ["project.read", "media.read", "timeline.edit", + "render.frame", "export", "ui.panel"] + +[restart] +max = 5 # 对齐 procpool 的 MAX_RESTARTS +backoff_ms = 1000 +``` + +发现路径(对齐 OFX 的发现习惯):`~/.oak/plugins/`、应用内 `plugins/`、 +环境变量 `OAK_PLUGIN_PATH`。Oak 启动时扫描 → 展示在"插件管理器"面板 → +用户启用后才 spawn(不自动启动未启用插件)。 + +### 2.2 握手与心跳 + +``` +Oak ──► {"method":"handshake","params":{"protocol":1,"oak_version":"...", + "shm":{"region":"oakxp-1234","slots":8,"slot_bytes":16777216}}} +Oak ◄── {"result":{"name":"ai-cut","api":1,"capabilities":[...], + "panels":[{"id":"chat","title":"AI 剪辑"}]}} +``` + +- 握手超时(对齐 procpool 的实现)→ 判定启动失败,标记插件不可用。 +- 之后 Oak 每 2s 发 `ping`,连续 3 次未响应或 stdout EOF → 判定崩溃: + 该插件的面板显示"已崩溃 [重启]"徽标,未完成的宿主 API 调用全部以 + `PLUGIN_DEAD` 错误返回,按 `restart.max` 有界自动重启。 +- **重启无状态恢复**:协议设计为"注册式"——插件重连后重新走握手、重新注册 + 面板。Oak 侧不丢数据:已提交的编辑早已进 UndoStack,与插件存亡无关。 + +### 2.3 Oak 侧组件 + +新增叶子 crate **`oak-plugin-host`**(与 `oak-worker` 平级的消费者角色, +不动引擎模块): + +- `PluginHost`:spawn/管道/NDJSON 读写(独立 IO 线程,`std::sync::mpsc` 与 + gpui `cx.spawn` 编排回引擎线程——沿用 app 现有 `set_progress_tx` 模式, + 不引入 tokio)。 +- `HostApi`:把插件请求翻译成内部调用(§3),执行前查能力位(§5)。 +- `PluginPanel`:实现 gpui `DockPanel` 的通用面板壳,注册进 + `AppPanelRegistry`(`crates/oak-app/src/panels/mod.rs` 目前是硬编码 + panel ids——需加一处"动态 panel 注册"扩展点,这是 app 侧唯一的新机制)。 +- `ShmPool`:泛化自 `oak-render/src/ipc.rs`。 + +--- + +## 3. 宿主 API(插件调用 Oak 内部功能) + +策展而非全量。插件看不到"内部函数",看到的是一组**版本化的 RPC 方法**, +每个方法是现有 `graphops`/`renderops`/`oak_task` API 的组合(下表"落到哪里" +均为现有代码位置)。协议主版本 `api` 保证:同一主版本内只增不删。 + +### 3.1 编辑事务(铁律 3 的落地) + +所有变更类方法必须包在事务里: + +```json +{"id":10,"method":"edit.begin","params":{"label":"AI: 粗剪访谈片段"}} +{"id":11,"method":"timeline.split_clip","params":{"clip":"n17","time":"3/25"}} +{"id":12,"method":"timeline.ripple_delete","params":{"clip":"n18"}} +{"id":13,"method":"edit.commit"} +``` + +`edit.begin/commit` 映射到 `oak_undo::undostack` 的 UndoCommand 分组:一次 +事务 = 一次 Ctrl-Z。`edit.abort` 回滚整组。**未在事务内的变更调用直接报错**, +从协议上杜绝不可撤销的编辑。 + +### 3.2 方法面(v1) + +| 方法族 | 方法(摘要) | 落到哪里 | 所需能力 | +|---|---|---|---| +| `project.*` | `open` / `save` / `get_info` / 事件 `project.modified` | `oak_storage::Session`、`oak_node::serializer` | `project.read` / `project.edit` | +| `media.*` | `probe` / `import_footage` / `list_footage` / `get_streams` | `oak_app::oakui::graphops::import_footage`、`oak_codec` 探测 | `media.read` / `media.import` | +| `timeline.*` | `get_structure`(序列/轨道/块树)、`place_clip`、`split_clip`、`trim`、`move`、`ripple_delete`、`add_transition`、`add_marker`、`set_workarea` | `graphops::place_footage_clip` / `split_clip` / …、`oak-timeline` 命令族 | `timeline.read` / `timeline.edit` | +| `node.*` | `list_types`(含 OFX 动态类型)、`add_effect`、`set_param`、`set_keyframe`、`get_params` | `Factory::global()`、`engine.rs::add_effect/set_effect_param`、`set_value_at_time_command` | `node.read` / `node.edit` | +| `render.*` | `get_frame(time)`→shm、`get_thumbnails(range,n)`、`get_audio_levels(range)` | `renderops::render_sequence_frame` / `render_audio_range`、缩略图缓存 | `render.frame` | +| `playback.*` | `play` / `pause` / `seek` / 事件 `playhead_moved` | `EngineGateway`(`request_frame/play/pause/seek`) | `playback` | +| `export.*` | `start(params)` / `cancel` / 事件 `export.progress` | `renderops::spawn_export`、`oak_task::export::EncodingParams` | `export` | +| `ui.*` | 见 §4 | `PluginPanel` + gpui_widgets | `ui.panel` | +| `edit.*` | `begin` / `commit` / `abort` / `undo` / `redo` | `oak_undo` | 随变更方法 | + +**取帧→AI 通路**(旗舰用例的关键路径,对齐 `ai-agent-design.md` §2.2): +`render.get_frame {sequence, time, max_size}` → 引擎经 ticket/进程池渲染 → +BGRA 进 shm 槽 → 返回 `{shm_slot, width, height, format}`;插件侧 SDK 一行 +`frame.to_png_bytes()`(OIIO/stb_image_write 或 Pillow)即可回喂多模态模型。 +`get_thumbnails` 一次取 N 帧拼 contact sheet,供"扫时间线定位内容"。 +**限流**:取帧调用带每插件速率与分辨率上限(默认 8 fps / 1920 宽),防止批量 +取帧拖垮渲染进程池。 + +### 3.3 事件(Oak→插件 notification) + +`project.opened/modified`、`timeline.structure_changed`(增量,非全量)、 +`playhead_moved`(§4.4 降频)、`export.progress/done`、`ui.*` 输入事件(§4.2)、 +`shutdown`(Oak 退出前发,插件应在 2s 内退出,否则 SIGTERM→SIGKILL)。 + +--- + +## 4. 插件 UI + +gpui 没有 webview,也不可能让 Python 插件直接调 gpui。提供**两条路径**, +插件按需在握手时声明(可同时用): + +### 4.1 声明式 UI(v1 基线,推荐大多数插件用) + +插件用 JSON 描述控件树,Oak 用 gpui_widgets 渲染成 `PluginPanel` 内容: + +```json +{"method":"ui.set_tree","params":{"panel":"chat","root": + {"type":"column","children":[ + {"type":"chat_log","id":"log"}, + {"type":"row","children":[ + {"type":"text_input","id":"prompt","placeholder":"描述你的剪辑意图…"}, + {"type":"button","id":"send","text":"执行"}]}, + {"type":"progress","id":"job"} + ]}}} +``` + +控件集 v1 保持小:`column/row/label/button/text_input/list/chat_log/image/ +progress/slider/checkbox`。用户在面板里的交互以 `ui.event {id, kind, value}` +推给插件;插件用 `ui.set_props {id, props}` 增量更新(不做全量重绘 diff, +控件树很小,全量 `set_tree` 也行)。 + +收益:**零崩溃面**(插件不画一个像素)、风格与 Oak 一致、实现量最小。 +AI 剪辑插件的聊天面板、操作日志、确认按钮,这套完全够。 + +### 4.2 像素面 UI(完整能力路径) + +插件自己用任意工具包(Qt/imgui/web 引擎——在**自己的进程**里)离屏渲染, +把 BGRA 位图经 shm 推给 Oak,Oak 在 `PluginPanel` 里原样贴图: + +``` +插件 ──shm 写帧──► ui.frame_ready {panel, slot, dirty_rect} ──► Oak 贴图 +插件 ◄── ui.event {kind:"pointer_down|pointer_move|key|scroll|focus", + x,y,button,modifiers,dpi_scale} ◄── gpui 事件转发 +``` + +- 这是既有 **OFX Interact GL-overlay 路径**(`oak_plugin::gl_bridge` + + `oakui/ofx.rs::forward_interact_pointer/key` + `program_viewer` 合成)的 + 进程外泛化:把"插件 GL 离屏 + readback 合成 + 事件转发"换成 + "插件进程离屏 + shm + 事件经 IPC 转发",事件模型照抄 interact 的。 +- resize 时 Oak 发 `ui.resize {width,height,dpi}`,插件按新尺寸重渲染; + 帧槽数 ≥2 做双缓冲,`frame_ready` 携带脏矩形减少合成开销。 +- v1 明确不做:IME 合成串转发、剪贴板互通、跨进程拖拽(需要时另立文档)。 + +### 4.3 其他 UI 形态 + +- **独立窗口**:插件进程自己开 OS 窗口,Oak 不管——始终允许,无需协议支持, + 集成度差,适合调试工具类插件。 +- **监看器叠加层**:OFX Interact 那种画在节目监视器上的 overlay,属于 + "效果交互"范畴,继续归 OFX;功能插件如需 viewer overlay(如 AI 打点预览), + 列为 v2 候选,复用 `program_viewer` 的合成点。 + +### 4.4 事件降频 + +`playhead_moved`、`pointer_move` 这类高频事件:Oak 侧合并到 30 Hz 上限、 +只发最新值(对齐 viewer 的刷新语义),避免 stdio 被事件洪水淹没。 + +--- + +## 5. 能力、确认与安全 + +- **能力位**:manifest `capabilities` 声明,安装/升级时向用户展示差异; + `HostApi` 在每次调用入口检查,越权调用返回 `CAPABILITY_DENIED` 并记日志。 + v1 能力集合即 §3.2 表右列。 +- **确认模式**(继承 `ai-agent-design.md` §6):`*.edit` 与 `export` 类调用 + 默认弹"插件 X 请求执行:split_clip n17 @ 3/25 [允许] [允许本会话] [拒绝]"; + 用户可在插件设置里改为自动。 +- **可撤销**:事务分组进 UndoStack,历史面板里显示为"插件名:事务标签", + 用户可整段撤销(§3.1)。 +- **限流与配额**:取帧速率/分辨率上限(§3.2);单插件 shm 池有上限; + 单请求参数大小上限(防内存炸弹)。 +- **密钥**:插件需要 API key 走自己的环境变量/自己的配置文件, + **绝不写入 Oak 工程文件**(.ove 里只允许存插件 id + 版本,对齐 OFX + `` 段的语义)。 +- **不做沙箱**:本系统隔离的是"崩溃",不是"恶意"——插件进程与 Oak 同用户 + 权限。恶意插件防护(seccomp/签名/商店审核)明确出范围。 + +--- + +## 6. 插件 SDK 与参考插件 + +- **`oakxp-c`**(头文件-only C/C++ SDK,放 `shared/include/oakxp/`): + NDJSON 分帧、JSON-RPC 收发、shm 附加、回调注册。无第三方依赖 + (JSON 用内置极简 parser,或允许作者自选)。这是 C ABI 纪律下唯一 + 允许插件 #include 的东西——**纯协议,不含任何 Oak 内部类型**。 +- **`oakxp`(Python 包)**:`pip install oakxp` 或随 Oak 分发; + `asyncio` 友好但非强制;`frame.to_png()` 依赖 Pillow(可选 extra)。 +- **参考插件**(验收的一部分): + 1. `examples/plugin-echo`:C++,注册一个声明式面板,按钮触发 + `project.get_info` 并显示——验证协议与 UI 基线。 + 2. `examples/plugin-roughcut`:Python,接多模态 LLM,实现 + "聊天指令 → get_thumbnails 扫时间线 → 事务化 split/ripple_delete → + get_frame 验证"的 AI 粗剪闭环——**它就是 ai-agent-design.md 的落地形态**。 + +### 与 `ai-agent-design.md` 的关系 + +该文档写于 RIIR 拆分前,假设"C ABI 小库 + 引擎内置 MCP server"。RIIR 与 +M15(渲染进程隔离)完成后,更优路径是:**AI 能力不进引擎,作为一个外部 +插件**跑在本系统上;MCP 仍可作为该插件对外的协议(插件自己起 MCP server +连 LLM 客户端),Oak 内核始终对 AI 无感知。本文落地后,`ai-agent-design.md` +的 M1/M2(工具面、取帧通路)由 §3.2 取代,M3(AI 面板)由 §4.1 取代。 + +--- + +## 7. 里程碑 + +| 里程碑 | 内容 | 验收 | +|---|---|---| +| **P1 传输与生命周期** | `oak-plugin-host`:spawn/握手/心跳/崩溃检测/有界重启;JSON-RPC 双向收发;`oakxp-c` 最小 SDK;echo 插件跑通 `project.get_info` | 杀掉插件进程:Oak 不崩、面板显示崩溃徽标、可重启;握手超时路径有测试 | +| **P2 宿主 API 核心** | `edit.*` 事务 + `project/media/timeline/node` 方法族 + 能力检查 | 插件完成"导入素材→铺轨→切开→波纹删除→加效果→改参数",逐步可在历史面板撤销;越权调用被拒 | +| **P3 取帧与导出** | `render.*` shm 数据面、`export.*` 事件、限流 | 黄金帧校验(复用 render-worker 端到端 harness):插件取到的帧与 viewer 一致;连续取帧不拖垮进程池 | +| **P4 声明式 UI** | `PluginPanel` + 动态 panel 注册 + `ui.*` 控件集 | echo 插件面板交互全通;控件树快照测试 | +| **P5 像素面 UI** | shm 贴图 + 输入转发 + resize/DPI | 参考 imgui 插件 60fps 交互无撕裂;事件转发对齐 interact 语义 | +| **P6 Python SDK 与 AI 粗剪** | `oakxp` 包 + `plugin-roughcut` | Mock LLM 录制/回放(无网络 CI)跑通"看图→下刀→验证"闭环 | + +P1–P3 是系统地基,任何插件都依赖;P4/P5 可并行;P6 随时可开始(SDK 与 +宿主 API 稳定后)。 + +--- + +## 8. 明确不做(边界) + +- **不**取代 OFX:图像处理节点仍走 `oak-plugin`(渲染在 worker 进程内已有 + 隔离)。功能插件如需注册新节点类型,v2 再评估(机制上是现成的 + `Factory::register_dynamic`)。 +- **不**做插件沙箱、签名、商店(§5)。 +- **不**做跨机器/网络插件(stdio only;socket 传输变体留作以后,协议本身 + 不绑定 stdio)。 +- **不**为插件发明新的引擎内部机制:宿主 API 全部是现有 + `graphops/renderops/oak_task` 的组合(铁律 3 同源于 ai-agent-design)。 +- **不**引入 tokio 到 app 路径;IO 线程 + mpsc + gpui executor 足够。 diff --git a/docs/zh/project-storage.md b/docs/zh/project-storage.md index d09d0bad8..ae71f997a 100644 --- a/docs/zh/project-storage.md +++ b/docs/zh/project-storage.md @@ -104,6 +104,6 @@ v1 假设单写者(SQLite `busy_timeout`,PG 行锁);多写者协作是 PG 全量运行,未设置则跳过并打印说明。该 URL 应指向专用测试库:每个 测试会重置四张表。 -另见:[M10 oak-storage 手册](plans/riir/M10-oak-storage.md)、 -[M13 写穿计划](plans/riir/M13-storage-live.md)、 +另见:[M10 oak-storage 手册](plans/completed/riir/M10-oak-storage.md)、 +[M13 写穿计划](plans/completed/riir/M13-storage-live.md)、 [工程文件格式参考](project-file-reference.md)。