- 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.
19 KiB
外部功能插件系统设计(进程隔离 + JSON-RPC/shm)
本文是 Oak 功能性插件系统的总体设计,面向没有当前对话记忆的执行者,自包含。
定位:与
oak-plugin(OpenFX 宿主)正交。OFX 管"效果/滤镜"这类图像处理插件; 本系统管"功能/工作流"插件——插件可以调用 Oak 内部功能(建工程、导入素材、 时间线编辑、加效果、取帧、导出)并绘制自己的 UI 面板。旗舰用例是 AI 剪辑插件: 给多模态 AI 一组工具,让它自己"看"视频(取帧回喂)并执行剪辑——外部程序因此 必须能完整操作 Oak。红线:
- 插件代码永不进入 Oak 主进程(不 dlopen、不链接任何 Rust 库)。一个插件 一个独立进程,插件崩溃不得连带 Oak。
- 插件可以是任何语言(C++/Python/Node…),协议必须是语言无关的文本协议 + 共享内存数据面,不发明需要链接 Rust/C ABI 的绑定。
- 插件的一切编辑动作必须可撤销(UndoStack 事务),默认"确认后执行"。
- 复用既有基础设施,不新造轮子:渲染进程隔离(
oak-render/src/procpool.rs+oak-render/src/ipc.rs,M15 已落地)的 NDJSON over stdio + shm 帧槽 模式 就是本系统传输层的范本。协议全文(消息信封、握手、方法/事件目录、错误码、shm 布局、UI 协议) 冻结在
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,理由:
- B 只是名义上更隔离。dlopen 进宿主进程后,插件崩溃杀掉的是宿主进程, 效果与 A 完全相同;但 B 要求宿主进程按语言分别内嵌加载器(C++ 用 dlopen, Python 得内嵌解释器或再起子进程),复杂度显著高于 A,没有换来任何隔离收益。
- A 对解释型语言天然成立。Python/Node 插件本来就是"一个命令",B 模型下 反而要多包一层。
- A 与仓库既有模式一致:
oak-worker就是"Oak spawn 一个可执行文件 + NDJSON 握手 + shm 附加",含崩溃检测、有界重启(MAX_RESTARTS=5)、握手超时。 插件宿主直接照搬这套生命周期管理。 - 协议实现在 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,
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:
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与 gpuicx.spawn编排回引擎线程——沿用 app 现有set_progress_tx模式, 不引入 tokio)。HostApi:把插件请求翻译成内部调用(§3),执行前查能力位(§5)。PluginPanel:实现 gpuiDockPanel的通用面板壳,注册进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 的落地)
所有变更类方法必须包在事务里:
{"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 内容:
{"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
<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)。- 参考插件(验收的一部分):
examples/plugin-echo:C++,注册一个声明式面板,按钮触发project.get_info并显示——验证协议与 UI 基线。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 足够。