# 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。
