Files
mnote/design/07-ai/process/7-43-onlyoffice-plugin-bridge-design-v1.md
T
lix-2026 1882db7681 收口 MNote P0 P1 P2 审查尾项
- 归档 OnlyOffice live bridge、Page AI、mindmap、design governance 与相关 bug 条目
- 补齐 MinerU OCR 后端 runtime 合同与 smoke/test 基线
- 收口 ChatOnly/Doubao、ObjectIdentity、Page Aggregate compat 与 runtime owner 文档口径

验证:
- cargo test --manifest-path rust/Cargo.toml -p mnote-web local_ocr -- --test-threads=1
- cargo test --manifest-path rust/Cargo.toml -p mnote-web onlyoffice_bridge -- --test-threads=1
- git diff --check
- git diff --cached --check
- codegraph index . --force && codegraph status .
- codegraph sync . && codegraph status .
2026-06-01 09:29:12 +08:00

274 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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。