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:
+2
-2
@@ -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 -->
|
||||

|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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 布局 -->
|
||||

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