Skip to content

Commit 7f73d1b

Browse files
chaxusclaude
andcommitted
test(vendor): the sdkjs plugin framework is alive in v9, pin it
We have carried "the offline build stripped the plugin infrastructure" since the Phase 0 spike of 2026-06-23. That was drawn against v7.5 and is why lib/agent-plugin calls window.editor.pluginMethod_* directly. It is not true of v9: passing editorConfig.plugins.pluginsData makes the Plugins controller fetch the config, register the plugin with g_asc_plugins, and run() creates iframe_<guid>; the toolbar carries its plugins tab. Two things make it look dead from outside, and either alone is enough for a probe to report "not supported". Asc.createPluginsManager lives in sdk-all.js, not sdk-all-min.js -- the minified bundle is the bootstrap and AscCommon.loadSdk pulls the full SDK afterwards, so a read right after onDocumentReady finds it undefined. And _checkLicenseApiFunctions returns false here (the offline patch sends onLicense({license:{}})) but has zero call sites in either bundle. What we do not ship is the sdkjs-plugins/ tree itself -- the v1/plugins.js bridge and the plugin bundles. That is a packaging decision, not a missing capability, and it is left open here. Reverse-validated: with the editorConfig.plugins injection removed, the poll for a registered plugin times out and the case fails. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MKNHFQ1kek1cHeqhbH37Pk
1 parent 9a5b2ca commit 7f73d1b

3 files changed

Lines changed: 342 additions & 8 deletions

File tree

‎CLAUDE.md‎

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ test/setup/vitest.ts # 全局 mock:matchMedia、URL.createObjectURL
199199
单一配置 `playwright.config.ts`(端口 4173,webServer 自动 build + preview,
200200
不需要手动先 build;`E2E_PORT=<port>` 另起一套并隔离 `dist-e2e-<port>/` 与
201201
`test-results-<port>/`,`E2E_BASE_URL=<站点>` 则不起本地服务、直接打线上)。
202-
`test/e2e/` 现有 49 个 spec,下面先说三条主线,再给全量清单:
202+
`test/e2e/` 现有 51 个 spec,下面先说三条主线,再给全量清单:
203203

204204
- `app-smoke.spec.ts` — 应用加载、PWA manifest 冒烟
205205
- `embed-api.spec.ts` — embed postMessage 协议
@@ -240,7 +240,7 @@ test/setup/vitest.ts # 全局 mock:matchMedia、URL.createObjectURL
240240
| 站点 / 入口 | `app-smoke`、`main-site`(hero 打开 + Ctrl+S 下载)、`entry-paths`(`?file=` / `document:open-url` / `?open=local`)、`sw-warm`(SW 已控制页面)、`font-cache`(第二次打开字体全走缓存) |
241241
| embed 协议 | `embed-api`、`embed-regression`(真实编辑器主回归)、`embed-save-default`(裸 save 用文档自身格式) |
242242
| 格式与内容 | `filename-matrix`、`format-parity`(docx/pptx 导出 PDF + 只读 + 运行时切换)、`resave-idempotence`、`xlsx-features`(合并/公式/2 万行)、`xlsx-panes`(冻结窗格/筛选)、`docx-features`(修订/页眉页脚)、`docx-ruby`(注音底文)、`comments`、`image-insert`、`csv-encoding`(GBK)、`html-as-xls`、`pdf-route`、`pdf-roundtrip`(打开/注释/存回/只读) |
243-
| 失败与守卫 | `open-failure`(-82 可见 + 保存快速拒绝,兼作 L0 自检)、`comment-bulk-actions`(守卫 8)、`wasm-memory`(守卫 10:40 MB x2t 二进制用完即还)、`offline-seam`(vendor 的进程内服务端应答器 + x2t 的唯一接缝 + 跨 realm 安全) |
243+
| 失败与守卫 | `open-failure`(-82 可见 + 保存快速拒绝,兼作 L0 自检)、`comment-bulk-actions`(守卫 8)、`wasm-memory`(守卫 10:40 MB x2t 二进制用完即还)、`offline-seam`(vendor 的进程内服务端应答器 + x2t 的唯一接缝 + 跨 realm 安全)、`plugin-availability`(插件框架经 `editorConfig.plugins` 可达)、`bad-image-url-locale`(守卫 13) |
244244
| 视觉 / 性能 | `visual-roundtrip`(无基线:原始 vs 存回再打开逐像素)、`slow-network` _opt-in_ `SLOW_NET=1` |
245245
| 交互面(策略 §9) | `api-surface` _opt-in_ `API_SWEEP=1`、`shortcut-surface` _opt-in_ `SHORTCUT_SWEEP=1`、`ui-crawl` _opt-in_ `UI_CRAWL=1`(逐页签点遍工具栏按钮,归因到按钮)、`monkey` _opt-in_ `MONKEY=1`(定种子随机序列,可精确回放) |
246246
| 字体 | `font-substitution`(被替换的名字与背后的开源 family 指着同一位置,两次渲染逐像素相同)、`pdf-cjk-export`(纯中文文档导出 PDF 后墨迹不得消失——CFF 字体会让它变空白) |
@@ -748,16 +748,32 @@ pi agent(earendil-works/pi)是一套轻量的多 Provider LLM 调用框架
748748
- API Key 存储在 localStorage,不经过中间服务器
749749
- **不涉及 WASM 模型量化**,"剪枝"指裁剪掉 Node.js 专属依赖,保留纯浏览器可运行的部分
750750

751-
#### 关键前提:需先验证
751+
#### 关键前提:已验证(2026-09-12,结论与 v7.5 时相反)
752752

753-
本项目使用的是 **OnlyOffice Web Apps(离线 WASM 版)**,而非 OnlyOffice Docs Server。两者在插件 API 支持上存在差异——需要实际验证 `window.Asc.plugin` 对象在当前本地加载方式下是否可用,以及 `AddComment`、Review 模式等 API 是否完整暴露。
753+
**v9 的插件框架是活的,而且走公开配置就能到达。**曾经"离线构建裁掉了插件基建"的结论
754+
是对 **v7.5** 那个包说的,现在不成立了。实测(`test/e2e/plugin-availability.spec.ts`):
755+
给 DocEditor 传 `editorConfig.plugins.pluginsData`,Plugins 控制器会去取 config.json、
756+
向 `g_asc_plugins` 注册,`run()` 建出 `iframe_<guid>`,工具栏的 `plugins` 页签也在。
754757

755-
#### 建议实施路径(分三阶段)
758+
两个细节让它从外面看像死的,别再被绊一次:
759+
760+
1. **`Asc.createPluginsManager` 在 `sdk-all.js` 里,不在 `sdk-all-min.js` 里。**`-min` 只是
761+
引导包,`AscCommon.loadSdk` 之后才拉 14 MB 的完整 SDK,而 `onDocumentReady` 可能在它
762+
落地之前就触发——看早了,manager 确实是 undefined。(`lib/prefetch.ts` 两个都预取,
763+
本来就知道这件事。)
764+
2. **`_checkLicenseApiFunctions()` 在这里恒为 false**(离线补丁发的是
765+
`onLicense({license:{}})`,`licenseResult.plugins` 是 undefined)。它长得像一道闸,
766+
但两个 bundle 里**没有任何地方调用它**。
756767

757-
**阶段一:验证 Plugin API 可用性**(1~2 天)
768+
我们**没有**的是 `sdkjs-plugins/` 这棵树本身——插件页面要加载的 `v1/plugins.js` 桥
769+
(`Asc.plugin` 就是它给的)和各插件包。manager 的默认 `path` 是
770+
`../../../../sdkjs-plugins/`,在本站 404;配置里带绝对 `baseUrl` 则完全绕开它。所以这是
771+
**打包决策,不是能力缺失**:要上插件就把这棵树放进来(官方 documentserver 镜像里有),
772+
或指到自己的插件源站。见 docs/explorations/2026-09-12-plugin-framework-is-alive.md。
773+
774+
#### 建议实施路径(分三阶段)
758775

759-
- 在 `public/` 下新建一个最小插件,验证 `window.Asc.plugin.init` / `callCommand` / `PasteHtml` 是否在当前离线版本中可用
760-
- 若不可用,需评估是否升级到 OnlyOffice Docs Server
776+
**阶段一:Plugin API 可用性** — 已完成,见上。
761777

762778
**阶段二:Agent 工具层**(新建 `lib/agent-plugin.ts`)
763779

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# The plugin framework is alive in v9, and we had it recorded as dead
2+
3+
2026-09-12
4+
5+
## The claim we were carrying
6+
7+
From the Phase 0 spike on 2026-06-23, against the **v7.5** offline package:
8+
9+
> 标准 OnlyOffice 插件模型**不可用**——离线构建裁掉了插件资源基建(无 `plugins.js`、
10+
> 无 `api/plugins` 目录,编辑器 iframe 内 `Asc.plugin` 单例为 undefined)。
11+
> **不要建 `public/plugins/` 验证插件。**
12+
13+
That is why `lib/agent-plugin` reaches into `window.editor.pluginMethod_*` and `asc_*`
14+
directly instead of running as a plugin. It was a correct conclusion for the build it
15+
was drawn against. We migrated to v9 and never re-checked it.
16+
17+
The question came back while comparing our integration to a third-party local-first
18+
Office site that ships 46 sdkjs plugins including the marketplace. If plugins work,
19+
a lot of what `agent-plugin` hand-rolls has an upstream home.
20+
21+
## What is actually true in v9
22+
23+
Everything works, through the public config, first try.
24+
25+
```
26+
editorConfig.plugins.pluginsData -> Plugins controller fetched the config
27+
registered -> ["asc.{1111...5555}"]
28+
run(guid) -> runnedPluginsMap = ["asc.{1111...5555}"]
29+
frames -> ["iframe_asc.{1111...5555}"]
30+
toolbar tabs -> [file, home, ins, draw, layout, links,
31+
review, view, plugins, headerfooter]
32+
```
33+
34+
The Plugins controller is in the app (`Common.Controllers.Plugins`), it reads
35+
`editorConfig.plugins`, fetches each entry of `pluginsData`, registers what it gets with
36+
`g_asc_plugins`, and `run()` creates the plugin frame. The toolbar has its `plugins` tab.
37+
38+
`test/e2e/plugin-availability.spec.ts` pins all of it, with a plugin invented on the
39+
spot: its `config.json` is handed over as a blob URL, so nothing is added to `public/`.
40+
41+
## Why it looked dead
42+
43+
Two things, and both are worth writing down because either one alone is enough to make
44+
a probe report "not supported".
45+
46+
**1. The plugin manager is not in the bundle you first see.**
47+
48+
`Asc.createPluginsManager` is defined only in `sdkjs/<app>/sdk-all.js`. The file the
49+
requirejs config names is `sdk-all-min` -- but that is the _bootstrap_, and it calls
50+
51+
```js
52+
AscCommon.loadSdk = function (dir, cb) {
53+
window.AscNotLoadAllScript ? cb() : loadScript('.../sdkjs/' + dir + '/sdk-all.js', cb);
54+
};
55+
```
56+
57+
so the full 14 MB SDK arrives afterwards, asynchronously. `onDocumentReady` can fire
58+
before it lands -- `lib/onlyoffice/save-stream.ts` already has a comment saying exactly
59+
that. Our first probe read the frame right after `document:open-buffer` resolved and got
60+
61+
```
62+
createPluginsManager: "undefined" g_asc_plugins: "undefined" pluginsManager: null
63+
```
64+
65+
which reads like a stripped build. Polling for another second gives
66+
`function` / `object` / `[object Object]`.
67+
68+
**2. There is a license check that looks like the gate, and it is not.**
69+
70+
```js
71+
baseEditorsApi.prototype._checkLicenseApiFunctions = function () {
72+
return this.licenseResult && true === this.licenseResult.plugins;
73+
};
74+
```
75+
76+
It returns `false` here: the offline patch's `Offline` controller calls
77+
`api.onLicense({ type: 'license', license: {}, advancedApi: true })`, so
78+
`licenseResult.plugins` is undefined. But **nothing calls it** -- zero call sites in
79+
`sdk-all-min.js`, zero in `sdk-all.js`. It is a leftover.
80+
81+
(The third-party site sends a fabricated `license` with `type: 3`, `branding: false`,
82+
`customization: true`. On this evidence that is not what makes its plugins work, and we
83+
should not copy it -- `branding: false` is the commercial flag, and we deliberately keep
84+
ONLYOFFICE's branding for AGPL §7(b).)
85+
86+
## What we do not have
87+
88+
The `sdkjs-plugins/` tree itself. A plugin page loads `v1/plugins.js` from it to get its
89+
`Asc.plugin` object, and the plugin bundles live under it. We ship none of it:
90+
`public/sdkjs-plugins/` does not exist, and the manager's default `path` --
91+
`../../../../sdkjs-plugins/` -- 404s on this origin. A config that carries its own
92+
absolute `baseUrl` bypasses the default entirely, which is how a plugin hosted elsewhere
93+
would work.
94+
95+
So this is a packaging decision, not a missing capability. Two ways to close it:
96+
97+
- vendor the tree (the official `onlyoffice/documentserver` image has
98+
`/var/www/onlyoffice/documentserver/sdkjs-plugins`), which costs deploy size and
99+
brings each plugin's own licence and network behaviour along with it; or
100+
- host plugins on a separate origin and point `pluginsData` at absolute URLs, which is
101+
what the third-party site does.
102+
103+
Neither is decided here. What is decided is that the door is not locked.
104+
105+
## What this changes for `agent-plugin`
106+
107+
Nothing immediately -- direct `pluginMethod_*` calls keep working and cost no iframe, no
108+
bridge and no round trip, which is why `editor-bridge.ts` has zero imports. But the
109+
options that were closed are open again: shipping the agent panel as a real plugin,
110+
reusing upstream plugins (translator, thesaurus, photo editor) instead of writing
111+
equivalents, and the marketplace.
112+
113+
Worth knowing before the next thing gets hand-rolled.
114+
115+
## Reverse validation
116+
117+
Convention 5. With the `editorConfig.plugins` injection removed from the test's DocEditor
118+
wrapper, the poll for a registered plugin times out and the case fails -- nothing else in
119+
the site registers plugins, so the test is measuring what it claims to.

0 commit comments

Comments
 (0)