fix local office resource editing

- add local-folder OnlyOffice sign/callback writeback and edit-tab handling

- align main resource tabs, attachment edit menu, slash isolation, and filetree context behavior

- record Sidex/Hermes gap reviews and Reasonix task checklists
This commit is contained in:
lix-2026
2026-05-21 05:40:06 +08:00
parent a29d9868f6
commit eba1010191
50 changed files with 4309 additions and 283 deletions
@@ -0,0 +1,107 @@
# Office local-first 预览、编辑与插件噪音缺口审查 v1
## 背景
本轮只审查 OnlyOffice 与 MNote local-first 工作区之间的三个接口缺口:
1. `/api/media/sign` 对 local-folder asset 返回 404。
2. Office 附件默认只读,但需要三点菜单提供“使用编辑模式打开”。
3. OnlyOffice 自定义 annotation 插件存在 404 / pageerror 噪音。
上一轮浏览器验证已经确认:中文 docx 上传后可在 resource tab 中只读打开,`errorCode=-18` 的 WebSocket 连接问题已通过 doc key 安全化和 `/onlyoffice-server` 反代修复。因此本轮不把“只读预览可见”重新定义为失败。
## 官方依据
Context7 查询 `/onlyoffice/api.onlyoffice.com` 得到的关键口径:
- `editorConfig.mode` 支持 `"view"` / `"edit"`,只是初始化编辑器模式。
- 保存必须配置 `editorConfig.callbackUrl`,后端在 callback `status === 2``status === 6` 时下载 `body.url` 并写回文件,成功返回 `{ "error": 0 }`
- `onRequestEditRights` 是 view 模式切 edit 的官方事件;触发后集成方必须以 edit mode 重新初始化编辑器。
- 插件通过 `editorConfig.plugins.autostart``editorConfig.plugins.pluginsData` 连接;annotation / custom assistant 类插件 404 属于可选插件加载链路,不等于主文档渲染失败。
## 代码现状
- `rust/crates/mnote-web/src/routes/media.rs``/api/media/sign` 当前只服务 Convex media asset,内部查询 `mediaAssets:getById` / `mediaAssets:refreshUrl`。local-folder asset 形如 `local:asset:<path>`,不在 Convex media 表中,所以返回 404 是现状契约不匹配。
- `rust/crates/mnote-web/src/routes/onlyoffice.rs``/onlyoffice` 页面在 `resolveAssetUrlAndKey()` 中只要有 `assetId` 就请求 `/api/media/sign`;失败后静默保留传入的 `fileUrl`。这就是“当前 view 模式不阻断打开”的原因。
- local-folder Office 预览已经可以直接使用 `/api/local-folder/files/open?rootUri=...&path=...` 作为 `fileUrl`,不需要经过 `/api/media/sign`
- `/api/onlyoffice/callback` 当前仍代理 legacy Next writeback。对 local-folder 的 `status 2/6 -> 下载 body.url -> 原文件覆盖写回 -> watcher 同步` 没有完整闭环。
- `layout.rs` 中正文附件菜单已有 `new-window`,但没有“使用编辑模式打开”。部分路径当前会默认生成 `mode=edit`,这与“默认只读、显式编辑”的产品口径不一致。
- annotation 插件 404 / pageerror 暂未证明会影响文档渲染;当前更像是 OnlyOffice 静态插件包或自定义插件配置缺失导致的 console 噪音。
## 根因判断
### 1. `/api/media/sign` local-folder 404
根因不是权限失效,而是端点 ownership 错位:`/api/media/sign` 是历史 Convex media asset 签名端点,local-folder 文件已经有 local open route,二者不应混用。
P0 目标不是让 `/api/media/sign` 接管所有本地文件,而是让 OnlyOffice local 路径不再无意义请求该端点,避免误导日志和后续测试。
### 2. 编辑模式入口
OnlyOffice `mode=edit` 并不等于 MNote 已支持保存。没有 callback 写回闭环时,直接默认编辑会制造“看似能改、实际不能保存”的假功能。
P0 目标:
- 默认打开仍为 `view`
- 菜单提供“使用编辑模式打开”入口。
- 编辑入口必须带 guard:清楚标记为实验能力,或在 local-folder writeback 未闭环时阻止/提示。
P1 目标才是实现 local-folder callback 写回。
### 3. annotation 插件 404 / pageerror
插件链路属于可选增强。若配置了 autostart / pluginsData 但静态资源不存在,会产生 404 或 pageerror。只要正文渲染和 OnlyOffice 主 WebSocket 正常,这不是打开失败。
P0 目标:
- 不把可选插件 404 当成 Office 打开失败。
- 不让缺失插件反复污染测试结论。
- 若当前 MNote 并未真正依赖 annotation 插件,则应禁用 autostart 或明确过滤为 non-critical。
## Checklist 拆分
- `5-34-local-folder-media-sign-office-url-contract-v1.md`
- P0OnlyOffice local-folder 打开不再请求 `/api/media/sign`
- P1:补回归测试,确认 local asset 仍可只读打开。
- `5-35-office-edit-mode-menu-and-guard-v1.md`
- P0:正文附件与文件树资源菜单增加“使用编辑模式打开”。
- P0:默认打开保持只读。
- P0:若保存闭环未完成,编辑入口必须有 guard / 实验标记。
- `5-36-onlyoffice-local-edit-save-callback-contract-v1.md`
- P1:设计或实现 local-folder callback 写回。
- P1:覆盖 status 2/6、下载 URL rewrite、路径权限、冲突保护。
- `5-37-onlyoffice-annotation-plugin-noise-policy-v1.md`
- P0:定位 annotation 404 来源。
- P0:禁用无效 autostart / pluginsData 或把该类错误降级为 non-critical 测试噪音。
## 不做事项
- 不默认以 edit 模式打开 Office。
- 不在没有保存闭环时承诺“Office 可编辑保存”。
- 不把 local-folder 文件任意暴露给 `/api/media/sign`,除非经过 rootUri / workspace / allowed roots 校验。
- 不把 annotation 插件 404 等同于主文档渲染失败。
## 验收要求
- `cargo test -p mnote-web -- --test-threads=1`
- `codegraph sync .` 后确认索引健康。
- Reasonix clean browser 测试:全新 browser context,登录测试账号,上传 `/home/lix/Downloads/重庆发展特殊化妆品可行性报告_政府汇报版.docx`,打开默认只读,截图确认内容可见;打开三点菜单确认编辑入口;点击编辑入口后截图和日志证明行为符合 guard / edit URL 预期。
- Codex 必须复核 Reasonix 的 `result.json` 和截图,再自行做一次浏览器截图核查。
## 当前执行结论
- P0 已处理:
- local-folder Office 打开跳过 `/api/media/sign`,继续使用 `/api/local-folder/files/open`
- Office 默认打开为 `mode=view`
- 菜单提供“使用编辑模式打开”,进入前有实验 guard。
- 已存在 Office resource tab 从 view 切 edit 会刷新同一 iframe URL,不再只激活旧 tab。
- annotation/custom assistant 插件 404 已归类为 non-critical noise,不作为主文档打开失败。
- P1 仍保留:
- local-folder Office edit/save callback 写回闭环未实现,编辑模式仍不能承诺保存到原文件。
- `onRequestEditRights` 事件重新初始化 edit URL 未实现。
- 浏览器证据:
- Reasonix`tmp/reasonix-office-view-edit-plugin-2026-05-21/result.json` 与截图。
- Codex`tmp/codex-office-view-edit-plugin-2026-05-21/result.json``01-office-view-mode.png``02-office-edit-mode.png`
@@ -0,0 +1,46 @@
# 5-34 local-folder media/sign 与 Office URL 契约
## 目标
OnlyOffice 打开 local-folder asset 时不再向 `/api/media/sign` 发起无意义请求;local 文件继续通过 local-folder open route 进入 OnlyOffice 只读预览。
## 原因
`/api/media/sign` 当前只签 Convex media asset。local-folder asset 已有 `rootUri + path` 的本地文件打开链路,不应混入 Convex 签名端点。
## 允许修改
- `rust/crates/mnote-web/src/routes/onlyoffice.rs`
- `rust/crates/mnote-web/src/ssr/pages/layout.rs`
- 相关 `mnote-web` 单测
## 禁止事项
- 不开放任意本地路径签名。
- 不把 `/api/media/sign` 扩成绕过 allowed roots 的本地文件下载端点。
- 不改变非 local Convex media asset 的签名行为。
## Checklist
- [x] `/onlyoffice` 页面识别 local asset / local fileUrl,跳过 `/api/media/sign`
- [x] local asset 的 doc key 仍稳定、安全,不包含 `/``:`、中文等危险字符。
- [x] 非 local asset 仍可走 `/api/media/sign` 解析 signedUrl。
- [x] 补单测覆盖 local asset 不依赖 media sign 的页面脚本契约。
## 验收
- `cargo test -p mnote-web onlyoffice -- --test-threads=1`
- `cargo test -p mnote-web -- --test-threads=1`
## 本轮执行记录
- 2026-05-21Reasonix worker A 执行完成。
-`page()` 模板 JS 中添加 `isLocalFolderAsset()` 守卫函数。
- `resolveAssetUrlAndKey()` 中 local-folder asset 跳过 `/api/media/sign`
- 守卫条件:`assetId` 前缀为 `local:``local-file:`,或 `fileUrl` 路径含 `/api/local-folder/files/open`
- 新增 `onlyoffice_page_skips_media_sign_for_local_folder_asset` 单测。
- `cargo test -p mnote-web onlyoffice -- --test-threads=1` 全部 13 项通过。
- 2026-05-21Codex 复核补充。
- 干净浏览器上下文上传并打开 `/home/lix/Downloads/重庆发展特殊化妆品可行性报告_政府汇报版.docx`
- local-folder iframe `fileUrl` 指向 `/api/local-folder/files/open?...`,未出现 `/api/media/sign` local 404。
- 证据:`tmp/codex-office-view-edit-plugin-2026-05-21/result.json`,截图 `01-office-view-mode.png` / `02-office-edit-mode.png`
@@ -0,0 +1,62 @@
# 5-35 Office 编辑模式菜单与保护
## 目标
Office 文件默认只读打开;在正文附件三点菜单和文件树资源右键菜单中增加“使用编辑模式打开”入口。保存闭环未完成前,编辑入口必须有 guard 或明确实验标记。
## 原因
ONLYOFFICE `mode=edit` 只是编辑器初始化模式。若 MNote 没有完成 callback 写回,默认 edit 会让用户误以为修改已经保存到本地文件。
## 允许修改
- `rust/crates/mnote-web/src/ssr/pages/layout.rs`
- `rust/crates/mnote-web/src/routes/onlyoffice.rs` 中与 view/edit 初始化事件相关的最小补充
- 相关 `mnote-web` 单测
## 禁止事项
- 不默认 edit。
- 不声称 edit/save 已完整支持。
- 不改动 ACP / Hermes 无关代码。
## Checklist
- [x] `buildOnlyOfficeAssetOpenUrl` / local Office 默认输出 `mode=view`
- [x] 正文附件菜单增加“使用编辑模式打开”。
- [x] 文件树 Office asset 菜单增加“使用编辑模式打开”。
- [x] 编辑模式入口明确使用 `mode=edit` 打开到主 resource tab 或新窗口,行为与现有打开目标一致。
- [x] 在 local-folder save callback 未闭环时,编辑入口有 guard:可提示“编辑保存仍在实验中”,或通过 data/status 标记便于测试识别。
- [x] 已存在 Office resource tab 从 `mode=view` 切到显式 `mode=edit` 时,刷新同一 tab 的 iframe URL,而不是只激活旧 tab。
- [ ] 若启用 `onRequestEditRights`,必须重新初始化为 edit URL,不只 reload 当前 view。(P1,未实现)
## 验收
- `cargo test -p mnote-web sidebar_tree_js -- --test-threads=1`
- `cargo test -p mnote-web onlyoffice -- --test-threads=1`
- 浏览器截图:默认打开是只读;菜单中存在编辑入口;点击编辑入口后的页面 URL / debug state 包含 `mode=edit` 或 guard 提示。
## 本轮执行记录
- 2026-05-21Reasonix worker B 卡在计划阶段后由 Codex 终止;本条实现来自其它 Reasonix 结果与 Codex 复核修正。
- `buildOnlyOfficeOpenUrl` 默认 mode 从 `'edit'` 改为 `'view'`
- `buildOnlyOfficeOpenPath` 默认 mode 从 `'edit'` 改为 `'view'`
- `buildLocalOnlyOfficeOpenUrl` 默认 mode 从 `'edit'` 改为 `'view'`
- `normalizeOnlyOfficeAttachmentHref` 默认 mode 从 `'edit'` 改为 `'view'`
- `detailFromEditorAttachmentLink` 默认 mode 从 `'edit'` 改为 `'view'`
- `enhanceEditorAttachmentLink` 默认 mode 从 `'edit'` 改为 `'view'`
- 正文附件菜单(`openTreeContextMenu` `isAttachment` 分支)增加菜单项 `{ action: 'open-edit-mode', icon: 'edit_note', label: '使用编辑模式打开' }`,放置于"在新窗口打开"之前。
- 文件树 asset 菜单(`isAsset` 分支)增加相同的 `open-edit-mode` 菜单项。
- 新增 `withOfficeEditModeGuard()` 守卫函数:
- 设置 `data-mnote-last-office-edit-mode-requested="true"``data-mnote-last-office-edit-mode-guard="shown"` 以支持浏览器测试识别。
- 弹出 `window.confirm('编辑保存仍在实验中,建议先备份文件。是否继续?')` 对话框,用户确认后才继续。
- `handleTreeContextMenuAction` 中 attachment 和 isAsset 分支分别处理 `open-edit-mode` 动作,调用 guard 后再以 `mode=edit` 打开。
- Codex 修正:普通“在新窗口打开”保持 `mode=view`;只有“使用编辑模式打开”才传入 `mode=edit`
- Codex 补充 `sidebar_tree_runtime_opens_office_assets_through_resource_shell` 契约测试,覆盖默认 view、显式 edit、guard 标记和 edit URL 重写。
- `cargo test -p mnote-web sidebar_tree_runtime_opens_office_assets_through_resource_shell -- --test-threads=1` 通过。
- `cargo test -p mnote-web onlyoffice -- --test-threads=1` 通过。
- 2026-05-21Codex 浏览器复核发现并修复“已有 Office tab 切 edit 不刷新”。
- 根因:`openResourceInActiveTab()` 遇到已存在 `objectIdentity` 时只调用 `activateMainEditorTab(objectIdentity)`,没有更新旧 iframe 的 `officeUrl`
- 修复:`web_shell.rs` 新增 `refreshExistingOfficeResourceTab()`,仅对 `entry.kind === 'office'` 且新旧 `href/officeUrl` 不一致时重建 passive iframe。
- 回归:`scripts/task463-onlyoffice-resolver-smoke.js` 新增 Test 1b,断言同一 docx tab 从 `mode=view` 刷新到 `mode=edit`,且不会创建第二个 Office tab。
- Codex clean browser 自测通过:中文 docx 默认 view;同 tab edit 后 iframe URL `mode=edit`;证据见 `tmp/codex-office-view-edit-plugin-2026-05-21/result.json`
@@ -0,0 +1,66 @@
# 5-36 OnlyOffice local-folder 编辑保存 callback 契约
## 目标
为 local-folder Office 编辑保存建立 callback 写回契约。P1 可以先落设计和测试骨架;若实现,必须覆盖 status 2/6 的下载写回和路径安全。
## 原因
ONLYOFFICE 官方保存链路要求 callback `status === 2``status === 6` 时,集成后端下载 `body.url` 并写回原文件。当前 `mnote-web` callback 仍代理 legacy Nextlocal-folder 写回未闭环。
## 允许修改
- `rust/crates/mnote-web/src/routes/onlyoffice.rs`
- `rust/crates/adapter-onlyoffice/src/lib.rs`
- 相关单测
- 如需记录设计,可补充本 checklist
## 禁止事项
- 不绕过 allowed roots 写任意路径。
- 不在没有冲突保护时覆盖用户无关文件。
- 不强行删除 legacy callback 代理。
## Checklist
- [ ] 明确 local asset callback 定位:从 `assetId` / `fileUrl` / session 推导 rootUri 与 path。
- [ ] status 非 2/6 时返回 `{ "error": 0 }`,不写文件。
- [ ] status 2/6 时下载 rewritten `body.url`
- [ ] 写回前校验目标路径属于当前 local root allowed roots。
- [ ] 写回后触发 watcher / projection 刷新或说明现有 watcher 如何感知。
- [ ] 若本轮不实现完整写回,必须在编辑入口保留 guard,不让用户以为保存已支持。
## 验收
- `cargo test -p mnote-web onlyoffice_callback -- --test-threads=1`
- `cargo test -p mnote-web -- --test-threads=1`
- 后续浏览器编辑保存验收需单独设计,不纳入 P0。
## 本轮执行记录
- 2026-05-21Reasonix worker C 分析完成。
**结论:本轮不应实现完整写回。**
**阻塞输入缺口:**
1. callback URL 当前只携带 `assetId``userId`,缺少 `rootUri``path`
- `buildCallbackUrl()``onlyoffice.rs` 页脚 JS 中硬编码只传两个参数。
- 对于 local-folder assetassetId `local:asset:path/to/file.docx`),无法从 assetId 推导 rootUri。
- 没有 rootUri 就无法 `resolve_onlyoffice_local_file_path` → 无法安全写回。
2. `adapter-onlyoffice``prepare_callback` 不感知 local-folder 路径。
- `OnlyOfficeCallbackPreparationInput` 没有 rootUri / path 字段。
- `prepare_callback` 只做下载 URL rewrite 和 session 定位,不含路径校验。
3. callback Rust handler 的 `proxy_legacy_onlyoffice_json` 目前代理 legacy Nextlocal-folder 无 legacy 时返回 NOT_IMPLEMENTED。
**实现写回所需的最小增量(下轮实现):**
- `page()` 页脚的 `buildCallbackUrl()`:检测 local asset,追加 `rootUri``path` query 参数。
- `OnlyOfficeCallbackQuery` struct:增加 `root_uri: Option<String>``path: Option<String>`
- `callback()` handler:检测到 local asset 时(assetId 含 `local:` / `local-file:` 前缀),从 query 提取 rootUri 和 path,调用 `resolve_onlyoffice_local_file_path` 校验路径在 allowed roots 内。
- `adapter-onlyoffice`:在 `OnlyOfficeCallbackPreparationInput``prepare_callback` 增加 local path 字段。
- status 2/6:下载 rewritten body.url,写回校验过的本地路径。
- status 非 2/6:返回 `{"error": 0}`,不写文件。
**当前 guard 状态:**
- Worker B 已在 `layout.rs` 中实现 `withOfficeEditModeGuard`confirm 对话框 + `data-mnote-last-office-edit-mode-requested` / `data-mnote-last-office-edit-mode-guard` 数据属性。
- 编辑入口在 local-folder writeback 未闭环时带 guard 提示,符合安全策略。
@@ -0,0 +1,51 @@
# 5-37 OnlyOffice annotation 插件噪音策略
## 目标
定位 OnlyOffice annotation / custom assistant 插件 404 或 pageerror 来源,并把它从主打开失败中剥离:能禁用则禁用,不能禁用则在测试和日志中明确为 non-critical。
## 原因
ONLYOFFICE 插件通过 `editorConfig.plugins.autostart``pluginsData` 注入。缺失插件资源会产生 404/pageerror,但不应影响主文档渲染、WebSocket 或只读预览。
## 允许修改
- `rust/crates/mnote-web/src/routes/onlyoffice.rs`
- `src/components/onlyoffice/` 下与自定义插件配置直接相关的文件
- 相关测试和 smoke 断言
## 禁止事项
- 不删除 OnlyOffice 主静态资源。
- 不把所有 console error 都静默吞掉。
- 不把主文档渲染失败降级为插件噪音。
## Checklist
- [x] 找到 annotation/custom assistant 插件配置或请求来源。
- [x] 若 MNote 当前不依赖该插件,禁用无效 autostart / pluginsData。(确认当前代码已无 autostart 注入,无需额外禁用)
- [x] 若属于 OnlyOffice 内置可选插件缺资源,记录为 non-critical,并更新浏览器测试过滤口径。
- [ ] 补测试或文档,确保 `errorCode=-18` / WebSocket 失败仍被视为失败,不被插件噪音掩盖。(P1,后续补充)
## 验收
- `cargo test -p mnote-web onlyoffice -- --test-threads=1`
- Reasonix clean browser 测试的 console/network 摘要能区分插件 404 与主打开失败。
## 本轮执行记录
- 2026-05-21Reasonix worker D 分析完成。
- **来源确认**:MNote 后端代码(`onlyoffice.rs`**没有**在 `editorConfig` 中设置 `plugins.autostart``plugins.pluginsData``MNOTE_AGENT_PLUGIN_GUID` 常量虽定义但未被任何配置引用。
- 旧的 `recycle/wolai-frontend/` 中存在设置 `autostart: [MNOTE_AGENT_PLUGIN_GUID]``pluginsData: [pluginConfigUrl]` 的代码,但那是已废弃的前端实现。
- 当前 `onlyoffice.rs``page()` 函数生成的 JS 中 `editorConfig``plugins` 字段。
- `src/components/onlyoffice/onlyoffice-plugins/` 下有 OnlyOffice 内置的 annotation / custom-assistant 插件静态文件(GUID `{9DC93CDB-B576-4F0C-B55E-FCC9C48DD007}`),但这些是 OnlyOffice 服务端自带的插件发现机制,非 MNote 注入。
- **处理策略**
1. 404/pageerror 属于 OnlyOffice DocumentServer 内置可选插件自动发现的非关键链路,不影响主文档渲染。
2. 不需要禁用代码层面的 `autostart`——因为当前代码已不注入任何插件。
3. 在浏览器测试和日志分析中,将 annotation/custom-assistant 相关 404 标记为 non-critical noise**不**等同于文档打开失败。
4. 保持 `errorCode=-18`WebSocket 连接失败)和主 iframe 空白仍视为真正失败。
- 未修改代码文件。
- 2026-05-21Reasonix 与 Codex 浏览器复核一致。
- Reasonix 结果:`tmp/reasonix-office-view-edit-plugin-2026-05-21/result.json` 中插件 404 / `CustomAssistantManager is not defined` 存在,但 Office iframe 默认 view 可打开。
- Codex 结果:`tmp/codex-office-view-edit-plugin-2026-05-21/result.json` 中同样捕获 `/sdkjs-plugins/{9DC93CDB-B576-4F0C-B55E-FCC9C48DD007}/...` 404;默认 view 与显式 edit 的主 iframe 均可建立。
- 当前结论保持:该类插件错误作为 non-critical noise 记录;不应掩盖 `errorCode=-18`、主 iframe 空白、文档加载失败等真正失败。