- 归档 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 .
274 lines
15 KiB
Markdown
274 lines
15 KiB
Markdown
# 7-43 ONLYOFFICE Plugin Bridge Design v1
|
||
|
||
> 状态:process
|
||
>
|
||
> Owner:07-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。
|