Files
mnote/design/07-ai/process/7-43-onlyoffice-plugin-bridge-design-v1.md
T

274 lines
15 KiB
Markdown
Raw Normal View History

2026-06-01 09:29:12 +08:00
# 7-43 ONLYOFFICE Plugin Bridge Design v1
> 状态:process
>
> Owner07-ai / ONLYOFFICE live bridge / Page AI target runtime
>
> 创建时间:2026-06-01
>
> 当前口径:工具层 session/scope 安全阻断、真实 iframe 非 dry-run 写入落点验证、Page AI target picker 到真实 Office iframe session 的端到端绑定均已落地;本文继续留在 `process`,用于承接后续 recipe 扩展、Office 主文档加载噪音治理和 P2 plugin bridge 产品化收口,不再作为 P0 安全阻断项。
## 背景
当前本机 ONLYOFFICE DocumentServer 由 Docker 暴露在宿主机 `8082`
- `mnote-onlyoffice-documentserver`: `onlyoffice/documentserver:9.4.0`
- 端口映射:`0.0.0.0:8082 -> container:80`
- MNote 公开入口:`http://127.0.0.1:3000`
- MNote 通过 `/onlyoffice-server` 反代 DocumentServer 静态资源和 editor iframe
实测当前社区镜像不暴露 `docEditor.createConnector()`,因此不能依赖 ONLYOFFICE Automation API。MNote 的 live Office agent 能力应建立在社区版可用的 Plugin API 上:插件 iframe 使用 `window.Asc.plugin.executeMethod()``window.Asc.plugin.callCommand()` 调用 Office JavaScript API。
## 目标
建立一个完整、可扩展、可验证的 MNote ONLYOFFICE bridge 插件,让 Hermes、Reasonix 和页面 AI 能通过白名单 recipe 操作当前打开的 ONLYOFFICE 文档。
目标能力:
- 支持当前浏览器内打开的 Word、Excel、PPT live editor session。
- 通过 MNote bridge API 派发命令,插件执行后回传 JSON-safe result。
- 不暴露任意 JavaScript 执行能力。
- 写操作继续遵守 MNote tool 权限、`dryRun``idempotencyKey` 模型。
- 所有新增 recipe 必须同时支持 browser direct smoke 和 tool API smoke。
非目标:
- 不绕过 ONLYOFFICE Developer / Automation API 授权。
- 不后台批量编辑任意 Office 文件;离线批处理仍优先使用 `officecli`
- 不把 ONLYOFFICE iframe DOM 当作编辑接口。
## 端口与 URL 边界
有两条 base URL,必须严格区分:
1. 浏览器可达的 MNote origin
- 示例:`http://127.0.0.1:3000`
- 用途:插件 iframe 调用 MNote bridge API。
- 来源:`location.origin`
- 禁止替换成 `host.docker.internal`,因为浏览器侧可能无法解析。
2. DocumentServer 容器可达的 MNote origin
- 示例:`http://host.docker.internal:3000`
- 用途:`document.url``callbackUrl`、local file proxy。
- 来源:`ONLYOFFICE_DOCUMENT_URL_BASE`
- 依赖 compose 中 `extra_hosts: ["host.docker.internal:host-gateway"]`
DocumentServer 静态资源访问:
- Browser -> MNote -> DocumentServer
- URL 形态:`/onlyoffice-server/web-apps/apps/api/documents/api.js`
- 插件 SDK`/onlyoffice-server/sdkjs-plugins/v1/plugins.js`
## 加载流程
1. 用户打开 `/onlyoffice?...`
2. MNote 页面加载 `/onlyoffice-server/web-apps/apps/api/documents/api.js`
3. MNote 构造 `DocsAPI.DocEditor` config。
4. `document.url``callbackUrl` 使用 `ONLYOFFICE_DOCUMENT_URL_BASE`,供 DocumentServer 容器访问。
5. `editorConfig.plugins.pluginsData` 指向:
`/api/onlyoffice/bridge/plugin/config?sessionId=...&apiBase=http://127.0.0.1:3000`
6. ONLYOFFICE 加载 MNote bridge 插件 iframe。
7. 插件 iframe 调用 `POST /api/onlyoffice/bridge/session` 注册 session。
8. 插件通过 `GET /api/onlyoffice/bridge/commands/next` 长轮询取命令。
9. 插件执行白名单 recipe。
10. 插件调用 `POST /api/onlyoffice/bridge/results` 回传结果。
## Bridge API
当前 API
- `GET /api/onlyoffice/bridge/plugin/config`
- `GET /api/onlyoffice/bridge/plugin/index`
- `GET /api/onlyoffice/bridge/plugin/index/config.json`
- `GET /api/onlyoffice/bridge/plugin/index/{state}`
- `POST /api/onlyoffice/bridge/session`
- `GET /api/onlyoffice/bridge/session/current`
- `POST /api/onlyoffice/bridge/session/close`
- `GET /api/onlyoffice/bridge/sessions`
- `GET /api/onlyoffice/bridge/capabilities`
- `POST /api/onlyoffice/bridge/commands`
- `GET /api/onlyoffice/bridge/commands/next`
- `POST /api/onlyoffice/bridge/results`
- `GET /api/onlyoffice/bridge/results`
## Session 模型
每个打开的 ONLYOFFICE 页面生成一个 `sessionId`
```text
mnote-oo-{docKey}-{tabRandomSuffix}
```
服务端保存:
- `sessionId`
- `editorType`: `word` / `cell` / `slide`
- `documentId`
- `assetId`
- `fileType`
- `docKey`
- `pageOrigin`
- `lastSeenMillis`
- `pendingCommands`
- `pendingResults`
多 session 规则:
- 插件每次 `/commands/next` 都刷新 `lastSeenMillis`
- `session.current` 返回最近活跃 session。
- 严肃写操作应显式传 `onlyofficeSessionId`,避免多标签页误写。
- 不同 editorType 的 recipe 必须在插件或 wrapper 层做类型保护。
2026-06-01 复核:`sessionId` 已加入浏览器 tab 级随机后缀,tool 侧读写均要求显式 `onlyofficeSessionId` / `bridgeSessionId`,并按 `aiAccessScope.allowedResourceIds` 校验 session resource。当前 HTTP / mock plugin 层已有 `task515``task516``task517` 证据;真实 ONLYOFFICE iframe / DocumentServer 层新增 `task518` 证据,覆盖插件 autostart 注册、同一文档双 tab 不共用 session、A/B 资源 scope mismatch 403、授权 dry-run 200,以及非 dry-run 写入 B 后导出验证 A 不含 marker、B 含 marker。随后 `task523` 补齐 Page AI target picker 到 Office session 的端到端 UI 绑定:真实文档页打开 Office resource tab 后,Page AI run payload 会把 iframe live `onlyofficeSessionId` 冻结进 `editorTarget``targetPackage``targetPackage.targets[0]`,且不再把 Office target 当作 Markdown buffer 查询 `/api/documents/buffer-state`
2026-06-01 续补:已新增 `GET /api/onlyoffice/bridge/session/current``POST /api/onlyoffice/bridge/session/close`session state 已写入 `docKey` / `pageOrigin``/onlyoffice` 页面会把当前文档 `docKey` 与页面 origin 透传给 bridge plugin config。已通过 `cargo test --manifest-path rust/Cargo.toml -p mnote-web onlyoffice_bridge -- --test-threads=1``node scripts/task517-onlyoffice-bridge-plugin-direct-smoke.js``MNOTE_UI_BASE_URL=http://127.0.0.1:3302 node scripts/task516-onlyoffice-bridge-multisession-browser-smoke.js`。本文继续保留在 `process`,只跟踪 Office 主文档加载噪音治理和第三批 recipe 逐项实测。
## Recipe 分层
### 已实现 recipe
Word / 通用:
- `selection.get`
- `document.insert_text`
- `document.replace_selection`
- `document.insert_html`
- `document.export`
- `document.search_replace`
- `document.insert_table`
- `document.get_comments`
- `document.add_comment`
Excel
- `sheet.get_sheets`
- `sheet.add_sheet`
- `sheet.rename_sheet`
- `sheet.get_range`
- `sheet.get_range_values`
- `sheet.get_values`
- `sheet.set_value`
- `sheet.set_formula`
- `sheet.batch_set_values`
- `sheet.set_range_values`
- `sheet.format_range`
- `sheet.set_dimensions`
- `sheet.sort_range`
- `sheet.add_chart`
PPT
- `presentation.get_slides`
- `presentation.get_slide_texts`
- `presentation.get_shapes`
- `presentation.add_text_slide`
- `presentation.replace_text`
- `presentation.set_shape_text`
- `presentation.delete_slide`
- `presentation.add_table`
- `presentation.clear_slide`
- `presentation.add_shape`
### 第二批 recipe 说明
Word
- `document.search_replace``Api.GetDocument().SearchAndReplace(...)`,适合精确文本替换。纯搜索暂不单独暴露,全文读取先用 `document.export`
- `document.insert_table``Api.CreateTable(cols, rows)``doc.Push(table)`,可用二维 `data` 填充单元格;默认插入到文档末尾。
- `document.get_comments``doc.GetAllComments()`,返回评论 id、作者、正文和引用文本。
- `document.add_comment` 支持给当前选区或 `document_start` 添加评论;选区模式依赖 `doc.GetRangeBySelect()`,没有选区时应显式用 `target=document_start`
Excel
- `sheet.get_sheets``Api.GetSheets()` / `worksheet.GetName()`,用于让 agent 明确当前 workbook sheet 结构。
- `sheet.add_sheet``Api.AddSheet(name)`,用于创建新 sheet。
- `sheet.rename_sheet``worksheet.SetName(name)`,用于重命名指定 `sheetIndex` 的 sheet。
- `sheet.get_range_values` / `sheet.set_range_values` 接受 A1 地址(例如 `B2:C3`),适合按用户可见坐标读写二维区域。
- `sheet.set_range_values` 只写入 A1 range 与 `values` 的交集;调用方需要保证二维数据尺寸符合预期。
- `sheet.format_range``ApiRange` 格式 API,只暴露小范围字体粗斜体、下划线、填充色、字体色、字号、字体名、对齐、数字格式。
- `sheet.set_dimensions``worksheet.SetColumnWidth` / `worksheet.SetRowHeight`,行列索引沿插件内部零基坐标。
- `sheet.sort_range``range.SetSort(...)`,只暴露 A1 range、range 内 1 基 `keyColumn`、升降序和是否有表头。
- `sheet.add_chart``worksheet.AddChart(...)`,只暴露 A1 数据区、图表类型、基础尺寸和插入位置。
PPT
- `presentation.get_slide_texts` 读取 slide 文本框纯文本。
- `presentation.replace_text` 替换文本框纯文本;命中的文本框会按纯文本段落重建,不承诺保留复杂 run 格式。
- `presentation.get_shapes` 返回每页 shape 的 `slideIndex` / `shapeIndex` / `shapeId` / text 预览,用于精确定位文本框。
- `presentation.set_shape_text``slideIndex + shapeIndex` 重写指定 shape 文本;只面向文本 shape,复杂样式不承诺保留。
- `presentation.delete_slide` 删除指定 `slideIndex` 的幻灯片;调用前应先读 slide 列表确认目标页。
- `presentation.add_table``Api.CreateTable(...)` + `slide.AddObject(table)`,支持二维 `data` 和基础位置/尺寸。
- `presentation.clear_slide``slide.RemoveAllObjects()`,用于清空指定页对象;这是高影响写操作,调用前必须确认目标页。
- `presentation.add_shape``Api.CreateShape(...)` + `slide.AddObject(shape)`,只暴露基础形状、文字、填充色、位置和尺寸。
### 第三批 recipe
- 已实装:Excel 排序、Excel 图表、PPT 表格。
- 已实装:PPT 清空页对象、PPT 添加基础形状。
- 待验证后再扩:Word 图片、修订、content controls、表格增删行列和样式。
- 待验证后再扩:Excel 筛选、工作表删除/移动。
- 待验证后再扩:PPT 图片、重排 slide、主题/布局、shape 样式与位置。
- PDF/forms 字段读取与填写。
第三批必须逐项实测,不允许凭 API 名称直接暴露给 agent。
## 权限与安全
插件只负责执行当前 editor session 允许的动作;MNote 负责业务权限:
- 当前用户是否能打开文档。
- agent 是否拥有 `office.read` / `office.write`
- 写操作是否显式 `dryRun`
- 写操作是否带 `idempotencyKey`
安全规则:
- 不暴露 raw JavaScript。
- action 必须是白名单。
- payload 必须 schema 校验。
- 批量单元格读写默认限制为 5000 cells。
- result 必须 JSON-safe。
- command timeout 默认 25 秒。
## 验证基线
每个 recipe 必须三层验证:
1. Rust unit test
- manifest 暴露
- route dispatch
- dryRun / 权限 / queue result
2. Browser direct smoke
- 直接调用 `window.__MNOTE_ONLYOFFICE_BRIDGE__.run(...)`
- 不经过 Hermes/Reasonix
- 必须保存截图并检查 `pageErrors=[]`
3. Tool API smoke
- 通过 `/api/hermes/tools/mnote/call`
- 验证 agent 实际可调用
完成标准:
- `cargo test -p mnote-web onlyoffice_ -- --test-threads=1`
- `cargo test -p mnote-web hermes_tools_manifest_returns_first_batch_tools -- --test-threads=1`
- `cargo test -p mnote-web skill_registry_exposes_onlyoffice_live_skill_to_agents -- --test-threads=1`
- `node scripts/task517-onlyoffice-bridge-plugin-direct-smoke.js` 覆盖 bridge plugin index 在 mock `Asc.plugin` 环境中的 session 注册、`docKey/pageOrigin` 元数据、command 消费、result 回传和 token 拒绝。
- `node scripts/task518-onlyoffice-real-iframe-session-scope-smoke.js` 覆盖真实 ONLYOFFICE iframe / DocumentServer 下插件 autostart、同文档双 tab session salt、bridge command 回收、resource scope 拒写、授权 dry-run,以及授权 B session 非 dry-run 写入后 A/B 导出内容不串台。
- `node scripts/task523-page-ai-onlyoffice-real-target-session-smoke.js` 覆盖真实文档页内 Office resource tab -> Page AI target picker -> run payload 的 live session 绑定,并断言 Office target 不触发 Markdown buffer-state 404。
- 关键 browser direct smoke 通过
- `git diff --check` 通过
- `codegraph sync .` 已运行
2026-06-01 进展:已新增并通过 `task517-onlyoffice-bridge-plugin-direct-smoke.js`,证明 MNote bridge plugin index 与 bridge HTTP loop 可在真实 Chromium 中直接运行。随后新增并通过 `task518-onlyoffice-real-iframe-session-scope-smoke.js`,在真实 ONLYOFFICE iframe / DocumentServer 下打开同一 docx 两个 tab 和另一个 docx,验证三者 sessionId 均不同、同文档双 tab 共享 docKey 但使用不同随机 salt、`selection.get` 可各自回收、scope=A 显式调用 B session 返回 403、scope=B 授权 dry-run 返回 200。`task518` 已继续扩展为非 dry-run 写入 B 并导出 A/B 内容验证不串台,截图保存在 `tmp/task518-onlyoffice-real-iframe-session-scope-smoke/screenshots/office-a-after-write.png``office-b-after-write.png``task523-page-ai-onlyoffice-real-target-session-smoke.js` 已补齐 Page AI target picker 到 `onlyofficeSessionId` 的真实 UI 绑定,截图保存在 `tmp/task523-page-ai-onlyoffice-real-target-session-smoke/screenshots/`
`task518` 已把 Office 主文档加载噪音分类为三类:bridge plugin translation 404、ONLYOFFICE 内置插件噪音、主文档加载失败。smoke 会断言 bridge session 注册和 command loop 正常,并把 `errorCode=-18` 或 editor 未 ready 归为主文档失败,避免把打不开文档误判为插件噪音。
补充验证矩阵:
- `task515-onlyoffice-live-scope-http-smoke.js`:覆盖缺 explicit session、scope mismatch、缺 scope 和授权 dry-run。
- `task516-onlyoffice-bridge-multisession-browser-smoke.js`:覆盖 A/B bridge session token、`docKey/pageOrigin` 元数据、command queue、result 回收互不串台,以及 `session/current` / `session/close`
- `task517-onlyoffice-bridge-plugin-direct-smoke.js`:覆盖 mock `Asc.plugin` 下 plugin index 注册、`docKey/pageOrigin` 透传和 command loop。
- `task518-onlyoffice-real-iframe-session-scope-smoke.js`:覆盖真实 ONLYOFFICE iframe / DocumentServer 下插件 autostart、同文档双 tab session 隔离、A/B session command 回收、`resource:onlyoffice` scope mismatch 403、授权 scope dry-run、授权非 dry-run 写入 B 后导出验证 A 不含 marker / B 含 marker。
- `task523-page-ai-onlyoffice-real-target-session-smoke.js`:覆盖真实文档页打开 Office resource tab、等待 iframe bridge ready、通过 Page AI target picker 选择 Office target,并验证 live `onlyofficeSessionId` 写入 run payload。