Files
oak-editor/docs/zh/plans/external-plugin-system.md
T
Mike-Solar 3cc6f75ccc 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.
2026-08-25 03:03:28 +08:00

335 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 外部功能插件系统设计(进程隔离 + 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 足够。