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.
This commit is contained in:
2026-08-25 03:03:28 +08:00
parent 8679bef6de
commit 3cc6f75ccc
46 changed files with 1120 additions and 280 deletions
+2 -2
View File
@@ -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)).
<!-- DIAGRAM: component / ABI layout -->
![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
+1 -1
View File
@@ -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`)
+2 -2
View File
@@ -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))。
<!-- 架构图:组件与 ABI 布局 -->
![架构图](../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)。
## 许可证
@@ -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_edgesat_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 disconnectproject.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.cppAdd/RemoveCommand redo/undo)、
node.cpp disconnect_edge、nodeviewcontext.cppedge 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)。
@@ -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 输出绑定。
+3 -18
View File
@@ -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/回放夹具测试、A1A5 里程碑 | external-plugin-system P1P3 完成(面板需 P4 |
| [`external-plugin-system.md`](external-plugin-system.md) | **外部功能插件系统**:插件=独立进程(非库加载),JSON-RPC over stdio 控制面 + shm 数据面(泛化 M15 render-worker 传输);策展宿主 API(事务化可撤销编辑、取帧回喂 AI)、声明式/像素面双 UI 路径、能力位与确认模式;含与 ai-agent-design.md 的关系与 P1P6 里程碑 | 已解锁(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-R7nm 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)
## 其他参考
+209 -96
View File
@@ -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 既定的模块划分,只在模块化树上**新增叶子**
- 插件系统 **P1P3 完成**:传输与生命周期、宿主 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 oakbackendGPU 插件)
ffmpeg_bridge
┌─ Oak 主进程 ────────────────────────────────┐
oak-plugin-hostP1P4 提供)
│ ├ OPP/1 控制面(JSON-RPC over stdio
│ └ shm down/up 区域
───────┬────────────────────────────────────┘
│ stdio + shm
┌───────┴─── oak-plugin-ai(独立进程)────────┐
│ ⑤ LLMProviderClaude/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 sheetLLM 扫图定位
"人何时进画面""哪里该切"),Agent 据此下刀——自动粗剪/打点的雏形。
- **连续回放**:经 playback 族起范围播放,按间隔采样帧。
- **验证式**`edit.commit` 后立即 `render.get_frame` 取切口/效果帧,LLM 判断
"效果对不对",不对则 `edit.undo` 或追加修正事务。
- **内容感知式**`render.get_thumbnails(range, count≤64)` 等间隔采样拼
contact sheetLLM 扫图定位("人何时进画面""哪里该切"),据此下刀——
自动粗剪/打点的雏形。
- **连续回放**`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,解释器嵌入式发行**:插件包内含私有 CPythonPyInstaller 或
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 SDKOPP/1),即本文的 `oak-plugin-ai`
- Resolve 适配层直接 import corePython 母语,零成本复用);
- Premiere 用 UXP 面板做壳,经 localhost 与本机 core 进程通信。
3. **生态**:主流 LLM SDKanthropic / 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 编码器出 PNGbase64 后作为图片消息发给 LLM。
缩略图用同一路径降采样,多张拼 contact sheet。
**事务编排是适配层的职责,不暴露给 LLM**LLM 的一次"动作"(可能含多个
`timeline.*` 调用)由适配层包成一个事务——先 `edit.begin(label=LLM 动作摘要)`
串行执行(协议保证同事务内按到达顺序),任一失败则 `edit.abort` 并把错误
回喂 LLM,全成功才 `commit`。LLM 看不到 `txn` 令牌,从根上避免"忘记 commit"
"嵌套事务"这类误用。
## 4. 协议:MCPModel Context Protocol
**取帧→PNG 通路**(关键路径):`render.get_frame` 返回 `FrameRef`shm 形态:
`bgra8` + 槽位号),SDK `frame.to_png()`Pillow)编码 → base64 → 作为图片
消息发给 LLM;随后**立即 `shm.release`**——批量扫描时必须流水线化释放,
否则 8 槽耗尽触发 `SHM_EXHAUSTED`(协议 §10.3)。小图(≤64 KiBOak 可能
直接 inline PNGSDK 对两种形态透明。
工具协议**定为 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、计费、配额)**超出本文范围**,
按需另立文档。
+2 -2
View File
@@ -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`
@@ -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 布局 PODSerializedLayoutInfo 全链路,已修复一致)、B3 coreengine.h、
@@ -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 访问器),
@@ -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`
---
@@ -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. 注意事项(别踩坑)
+2 -2
View File
@@ -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` 状态更新为"边界已纯"。
## 执行顺序与节奏建议
+1 -1
View File
@@ -207,7 +207,7 @@ grep -E '"(node|render|timeline|undo|task|pluginSupport)/'`)。不产生
1. `display.h` 全文无 C++ 类型签名/契约注释;app 无 TexturePtr/FramePtr。
2. liboakengine.so ` T _Z` = 0oak-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 的模块拆分阶段。
@@ -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 3node/node.h + node/param.h 主体清理完成,**以双适配器(oak:: wrapper)形态落地**
消费侧统一经 `shared/include/oakutil/oaknode.h` 访问 engineAppNodeInput 方案已废弃
消费侧统一经 `../../../../shared/include/oakutil/oaknode.h` 访问 engineAppNodeInput 方案已废弃
(落地细节与 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 枚举镜像)、
@@ -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 语义见文件头注释。
@@ -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)并入库。**此后该头的任何改动都是显式评审行为。**
@@ -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 为准。
补充后端管理:
@@ -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` 范围,另行)。
+548
View File
@@ -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 → pluginnotification
{"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 线程**;典型延迟 50500 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 合成完毕 → acknotification),槽位可复用
{"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.2s141.6s 为废片 → 事务化下刀(用户确认后执行)
{"jsonrpc":"2.0","id":4,"method":"edit.begin","params":{"label":"AI 粗剪:删除 83.2141.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":[]}}}
```
+334
View File
@@ -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 按清单(manifestspawn,经 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 IPCJSON-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 shmWindows 用
`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 推给 OakOak 在 `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
`<plugins>` 段的语义)。
- **不做沙箱**:本系统隔离的是"崩溃",不是"恶意"——插件进程与 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` 最小 SDKecho 插件跑通 `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 足够。
+2 -2
View File
@@ -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)。