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

19 KiB
Raw Blame History

外部功能插件系统设计(进程隔离 + JSON-RPC/shm

本文是 Oak 功能性插件系统的总体设计,面向没有当前对话记忆的执行者,自包含。

定位:与 oak-pluginOpenFX 宿主)正交。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.rsM15 已落地)的 NDJSON over stdio + shm 帧槽 模式 就是本系统传输层的范本。

协议全文(消息信封、握手、方法/事件目录、错误码、shm 布局、UI 协议) 冻结在 external-plugin-protocol.mdOPP/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, 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.rsSharedMemoryRegion / 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 平级的消费者角色, 不动引擎模块):

  • PluginHostspawn/管道/NDJSON 读写(独立 IO 线程,std::sync::mpsc 与 gpui cx.spawn 编排回引擎线程——沿用 app 现有 set_progress_tx 模式, 不引入 tokio)。
  • HostApi:把插件请求翻译成内部调用(§3),执行前查能力位(§5)。
  • PluginPanel:实现 gpui DockPanel 的通用面板壳,注册进 AppPanelRegistrycrates/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::Sessionoak_node::serializer project.read / project.edit
media.* probe / import_footage / list_footage / get_streams oak_app::oakui::graphops::import_footageoak_codec 探测 media.read / media.import
timeline.* get_structure(序列/轨道/块树)、place_clipsplit_cliptrimmoveripple_deleteadd_transitionadd_markerset_workarea graphops::place_footage_clip / split_clip / …、oak-timeline 命令族 timeline.read / timeline.edit
node.* list_types(含 OFX 动态类型)、add_effectset_paramset_keyframeget_params Factory::global()engine.rs::add_effect/set_effect_paramset_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 EngineGatewayrequest_frame/play/pause/seek playback
export.* start(params) / cancel / 事件 export.progress renderops::spawn_exportoak_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/modifiedtimeline.structure_changed(增量,非全量)、 playhead_moved(§4.4 降频)、export.progress/doneui.* 输入事件(§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 推给 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_movedpointer_move 这类高频事件:Oak 侧合并到 30 Hz 上限、 只发最新值(对齐 viewer 的刷新语义),避免 stdio 被事件洪水淹没。


5. 能力、确认与安全

  • 能力位manifest capabilities 声明,安装/升级时向用户展示差异; HostApi 在每次调用入口检查,越权调用返回 CAPABILITY_DENIED 并记日志。 v1 能力集合即 §3.2 表右列。
  • 确认模式(继承 ai-agent-design.md §6):*.editexport 类调用 默认弹"插件 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 内部类型
  • oakxpPython 包)pip install oakxp 或随 Oak 分发; asyncio 友好但非强制;frame.to_png() 依赖 Pillow(可选 extra)。
  • 参考插件(验收的一部分):
    1. examples/plugin-echo:C++,注册一个声明式面板,按钮触发 project.get_info 并显示——验证协议与 UI 基线。
    2. examples/plugin-roughcutPython,接多模态 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 足够。