收口 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 .
This commit is contained in:
lix-2026
2026-06-01 09:29:12 +08:00
parent 49a0545148
commit 1882db7681
143 changed files with 29810 additions and 3228 deletions
@@ -2,7 +2,7 @@
> 创建时间:2026-05-21
>
> 当前状态:`PROCESS`
> 当前状态:`DONE`
>
> 上位入口:`design/01-tree-first-graph-kernel/process/1-8-mvp-post-process-execution-order-v1.md`
>
@@ -2,7 +2,7 @@
> 创建时间:2026-05-19
>
> 当前状态:`PROCESS`
> 当前状态:`DONE`
>
> 上位依据:
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
@@ -6,14 +6,18 @@
>
> 2026-05-22 归档治理补充:当前 active `process/` 只保留可直接执行的少量收口稿;长期架构、愿景、参考矩阵、Wolai 对标基线、Mindmap 总设计、旧 AI 块级执行稿和 Convex Web 迁移入口已分别移动到 `reference/`、`done/` 或 `old/`。本文是后续调度入口,不代表下列所有历史入口仍在 `process/`。
>
> 2026-05-31 口径复核:当前主线仍以 `CURRENT_ARCHITECTURE.md`、`ARCHITECTURE.md` 与本文件为准;历史 `design/01-05-current-priority-overview.md` 已迁入 `design/old/01-05-current-priority-overview.md`,不再作为 active 调度入口。`3-3`、`3-18` 等已归档文档只保留为完成证据,后续 live / local-folder runtime 收口默认看 `3-23`、`5-32` 与相关 bug。
>
> 2026-06-01 P0/P1/P2 复核:P0 阻塞项已由 `4-44/4-45/4-34/4-28` 归档证据覆盖,不再作为实现阻塞;`5-30` 与 `5-33` 已归档到 `05-editor-mainline/done/`,导航页 breadcrumb / fetch guard 尾项拆到 `5-35``7-35/7-36` 已降级为协作参考,不再占用产品 process 队列。当前 active 产品 process 重点收敛为 `3-23`、`3-25`、`5-35`、`7-18`、`7-43` 及对应 bugs/process。
>
> 目标:把本轮 design governance 后剩余的主线 `process/` 文档排成可执行顺序,避免后续 worker 在 active process 中自行猜优先级。
>
> 上位依据:
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
> - `/mnt/Data1T/mnote/CURRENT_ARCHITECTURE.md`
> - `/mnt/Data1T/mnote/design/README.md`
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
> - `/mnt/Data1T/mnote/design/10-review/done/15-design-governance-mvp-post-review-v1.md`
> - `/mnt/Data1T/mnote/design/10-review/done/18-current-design-and-bug-hunt-review-v1.md`
## 1. 一句话顺序
@@ -87,6 +91,8 @@
- `4-45` 四项 checklist 完成后归档到 `04-tree-domain/done/`
- `4-44` browser smoke 全绿后归档到 `04-tree-domain/done/`
2026-06-01 状态:本项已由 `4-44` / `4-45` done 文档和 `task471` / `task476` / `task487` 复跑证据覆盖,不再作为 P0 阻塞项。
### P0.2 local folder no-refresh / restore focus
入口文档:
@@ -109,6 +115,8 @@
- local folder 子项通过后,`4-34` 归档。
- React / Rust 主壳 focus 证据完成后,`4-28` 归档。
2026-06-01 状态:本项已由 `4-34` / `4-28` done 文档和后续 tree/filetree lifecycle smoke 证据覆盖,不再作为 P0 阻塞项。
## 4. P1:统一工作区身份、BufferStore 和 Page Aggregate
这一步是 MVP 后阶段真正的底座收口。
@@ -133,6 +141,7 @@
- `cargo test -p mnote-web filetree_runtime -- --test-threads=1`
- `cargo test -p mnote-web web_shell -- --test-threads=1`
- 覆盖 markdown / office / mindmap / raw file / directory 的 browser smoke。
- 2026-06-01 进展:已补 `scripts/task524-workspace-object-identity-matrix-smoke.js`,覆盖 page `.md`、directory、raw text、mindmap、OnlyOffice 在 FileTree row `workspacePath`、OpenEditorsSnapshot entry、URL `resourceTab`、active tab identity 之间的对齐;验证命令 `MNOTE_UI_BASE_URL=http://127.0.0.1:3301 node scripts/task524-workspace-object-identity-matrix-smoke.js` 已通过。P1.1 的 identity 浏览器矩阵缺口已收口,后续只保留与 `1-5 / 1-6` 历史 checklist 对账。
归档条件:
@@ -163,6 +172,8 @@
- `1-6` 中 BufferStore 运行时接入、浏览器读回、冲突 UI 相关项完成。
2026-06-01 状态:历史 BufferStore 读回 / conflict UI 已由 `1-6` 与 Batch C 证据覆盖;真实 agent writeback -> watcher -> BufferStore -> Page Aggregate -> tiptap 的端到端回收仍归入 `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md` Phase B,不再作为本文模糊 P1.2 阻塞。
### P1.3 Page Aggregate compat 瘦身
入口文档:
@@ -177,6 +188,8 @@
2. 补 GFM AST parser 写侧尾项:web shell 单测、手写 parser 过渡标记、临时适配分支清理。
3. 明确 `PageAggregateClientState` 哪些仍是合法 draft state,哪些应退出事实源地位。
2026-06-01 收口口径:`body.blockDocument` / `editorDocument` 是 local-first 浏览器主消费面;`PageBody.content``mnote.page_aggregate.v1` 中继续保留为必填兼容镜像 / fallback,不作为 active editor 初始化优先事实源。`body.content` 改可选或删除必须进入 v2 协议升级或独立迁移 checklist,不能在 v1 兼容期直接删除。
验收:
- `cargo test -p mnote-web local_markdown -- --test-threads=1`
@@ -186,7 +199,7 @@
归档条件:
- `3-13` 已完成 GFM AST 迁移尾项并归档到 `design/03-rust-web/done/``web_shell.rs` legacy marks 分支改判为兼容层保留,不再作为迁移阻塞项。
- `5-6` 剩余 Phase G/H 小尾项完成或拆成更小 checklist 后归档。
- `5-6` 剩余 Phase G/H 小尾项完成或拆成更小 checklist 后归档`bugs/05-editor-mainline/done/5-41-page-aggregate-compat-slimming-gaps-v1.md` 已明确 `PageBody.content` 的 v1/v2 边界
## 5. P2tree live cache 与 command context
@@ -194,8 +207,10 @@
入口文档:
- `design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
- `design/03-rust-web/done/3-3-rust-web-tree-realtime-event-stream-v1.md`
- `design/03-rust-web/reference/3-1-rust-web-long-term-checklist-v2.md`
- `design/03-rust-web/process/3-23-sidebar-local-folder-resource-runtime-followup-v1.md`
- `design/05-editor-mainline/done/5-32-filetree-lazy-loading-sidex-alignment-v1.md`
执行目标:
@@ -210,13 +225,13 @@
归档条件:
- `3-3` 中统一 live cache 和 no-refresh 矩阵完成后归档
- `3-3` 已归档为 tree realtime event stream 完成证据;剩余 live cache / refresh 收口不得重新打开旧 `3-3`,应拆入 `3-23``5-32` 或明确的 `bugs/process`
### P2.2 Local folder tree live consumer 收口
入口文档:
- `design/03-rust-web/process/3-18-local-folder-tree-live-consumer-convergence-checklist-v1.md`
- `design/03-rust-web/done/3-18-local-folder-tree-live-consumer-convergence-checklist-v1.md`
执行状态:
@@ -1,5 +1,11 @@
# 全局与内容类型页面宽度设置 Checklist v1
> 状态:done
>
> 归档时间:2026-05-31
>
> 归档说明:实施 checklist 已全量完成,当前真实消费点已覆盖 Markdown、轻量 Office 预览、PDF 预览和 PPTX 快速预览;Mindmap 渲染消费作为后续对应 viewer follow-up,不阻塞本文归档。
## 背景
当前 Markdown 页面已有 `wideLayout` 布尔选项,但 Word / PDF / Excel / PPT / Mindmap 等不同内容类型没有统一的页面宽度偏好入口。用户需要在全局设置中分别调整不同类型的默认宽度,避免像思源那样必须写 CSS 才能把阅读宽度调到合适比例。
@@ -0,0 +1,413 @@
# 3-25 Local Folder MinerU OCR Sidecar Checklist v1
> 创建时间:2026-05-29
> 状态:`process`
> Owner03-rust-web / local-folder resource runtime
>
> 2026-06-01 复核:后端 mock 闭环已落地 `local_ocr.rs` 与 OCR route;真实 MinerU HTTP client 已补本地 HTTP mock 成功路径测试;`task526` 已覆盖 mock OCR API、搜索命中、显式插入链接和 OCR sidecar Markdown resource tab 打开。前端 OCR task runtime、active job store、realtime event 和任务栏 UI 尚未落地。本文继续保持 `process`,不得被本轮 Page AI / OnlyOffice / ChatOnly 收口误归档。
## 背景
当前 mnote local-first 主线中,本地 Markdown 和同目录资源是默认数据真相。本地图片 / 附件上传已通过 `/api/local-folder/assets/upload` 写入 Markdown 文件附近,并返回 `sourcePath` / `attachmentRef`。现在需要给图片和图片型 PDF 增加 OCR 能力,识别引擎使用 MinerU API。
OCR 结果不应默认写进用户正文,也不应只存在 `.mnote` 私有缓存里。用户希望 OCR 附件位于当前 Markdown 文件相同目录下,并用单独文件夹承载,因为一个 Markdown 页面可能对应多个 OCR 文件。
## 目标
- 对 local-folder 图片和图片型 PDF 提供手动触发 OCR。
- 使用 MinerU API 识别本地文件,生成 Markdown OCR 结果。
- OCR 结果作为页面旁路资源落盘到当前 Markdown 同目录的 `{pageStem}.ocr/` 文件夹。
- OCR 文本默认作为资源 sidecar 被搜索、资源面板和后续 AI 上下文消费,不自动污染正文。
- 支持用户显式把某个 OCR 结果插入当前正文。
- 提供全局 OCR 任务进展入口,让用户离开当前资源后仍能看到 OCR 是否完成、失败或需要重试。
- 保持 local-first 可迁移性:OCR Markdown 文件本身可读、可备份、可编辑;`.mnote` 索引只作为缓存和状态加速。
## 非目标
- 不做全 workspace 自动 OCR。
- 不默认把 OCR 文本追加到页面正文。
- 不把 MinerU token 打印到日志、前端 payload 或用户可见错误里。
- 不在前端直接调用 MinerU API。
- 不把 OCR 文件当成普通页面加入 Page Tree。
- 不为 cloud / Convex / remote source 设计默认 OCR 主路径;第一阶段只覆盖 local-folder。
- 不承诺精确百分比进度;MinerU API 第一阶段按阶段型状态展示。
- 不把任务进展做成前端定时轮询主链;优先走现有 realtime / WS / SSE 事件。
## 文件布局
假设当前页面为:
```text
docs/Page.md
```
附件可能为:
```text
docs/Page.assets/photo.png
docs/Page.assets/spec.pdf
```
OCR 结果目录固定为:
```text
docs/Page.ocr/
```
OCR 结果文件示例:
```text
docs/Page.ocr/photo.png.ocr.md
docs/Page.ocr/spec.pdf.ocr.md
```
同一页面下存在同名来源文件时,用来源 root-relative path、文件大小和 mtime 派生短 hash
```text
docs/Page.ocr/photo.png-8f3a21.ocr.md
```
## OCR Markdown frontmatter
OCR Markdown 是 OCR 正文真相,必须能脱离 `.mnote` 索引独立说明来源。
```markdown
---
mnote_ocr_version: 1
provider: mineru
model_version: vlm
owner_document: ./Page.md
source_path: ./Page.assets/photo.png
source_root_relative_path: docs/Page.assets/photo.png
source_size: 123456
source_mtime_ms: 1760000000000
status: done
created_at: 2026-05-29T12:00:00+08:00
updated_at: 2026-05-29T12:00:00+08:00
---
识别文本...
```
字段口径:
- `owner_document`:相对 OCR 文件所在目录到 owner Markdown 的相对路径。
- `source_path`:相对 owner Markdown 所在目录的来源附件路径,和正文中的附件引用口径保持一致。
- `source_root_relative_path`:相对 workspace root 的来源路径,用于稳定查找和索引。
- `source_size` / `source_mtime_ms`:用于判断 OCR 是否 stale。
- `status``done``failed``stale``queued/running` 只写入 `.mnote/ocr-index.json`,避免半成品 OCR 文件被误读。
## `.mnote` 索引
`.mnote/ocr-index.json` 是状态缓存,不是 OCR 正文真相。
```json
{
"version": 1,
"entries": {
"docs/Page.assets/photo.png": {
"ownerDocumentId": "local-md:docs~2FPage.md",
"ownerDocumentPath": "docs/Page.md",
"sourceRootRelativePath": "docs/Page.assets/photo.png",
"ocrRootRelativePath": "docs/Page.ocr/photo.png.ocr.md",
"provider": "mineru",
"modelVersion": "vlm",
"status": "done",
"sourceSize": 123456,
"sourceMtimeMs": 1760000000000,
"createdAtMs": 1760000000000,
"updatedAtMs": 1760000000000,
"plainTextPreview": "识别文本前 240 字"
}
}
}
```
索引用途:
- 资源面板快速展示 OCR 状态。
- 搜索索引快速定位 OCR sidecar。
- 避免重复提交同一个未变化资源。
- 记录失败原因,但错误文本需要脱敏,不包含 token、预签名 URL 或完整外部响应体。
## 资源归属与树投影
- OCR 文件夹 `{pageStem}.ocr/` 应在 File Tree 中作为页面旁路资源文件夹出现。
- `{pageStem}.ocr/*.ocr.md` 不应作为普通 Markdown 页面进入 Page Tree。
- local search 不应把 OCR sidecar 当普通 Markdown 页面索引,否则会出现重复页面和错误导航。
- 搜索 OCR 命中时,应返回 owner page 作为结果,`hasOcr=true`evidence 指向来源附件和 OCR sidecar。
- File Tree 可展示 OCR 文件,点击默认作为 Markdown resource tab 打开,不切换成页面文档。
## MinerU 调用边界
后端读取 MinerU token,前端不接触 token。
配置优先级:
1. `MNOTE_MINERU_API_TOKEN`
2. `MINERU_API_TOKEN`
3. 开发环境可选读取 `~/.hermes/.env` 中的 `MINERU_API_TOKEN`
第一阶段使用 MinerU `model_version=vlm`
本地文件流程:
1. `POST https://mineru.net/api/v4/file-urls/batch` 申请上传地址。
2. 使用 `PUT` 把本地文件上传到预签名 URL。
3. 轮询 `GET /extract-results/batch/{batch_id}`
4. 下载 `full_zip_url`
5. 解压并提取结果 Markdown。
6. 写入 `{pageStem}.ocr/*.ocr.md`
7. 更新 `.mnote/ocr-index.json`
## 后端 API
### 创建或复用 OCR job
`POST /api/local-folder/ocr/jobs`
请求:
```json
{
"rootUri": "file:///mnt/Data1T/mnote-notes",
"documentId": "local-md:docs~2FPage.md",
"sourcePath": "Page.assets/photo.png",
"sourceRootRelativePath": "docs/Page.assets/photo.png",
"provider": "mineru",
"force": false
}
```
响应:
```json
{
"ok": true,
"job": {
"status": "done",
"ownerDocumentId": "local-md:docs~2FPage.md",
"sourceRootRelativePath": "docs/Page.assets/photo.png",
"ocrRootRelativePath": "docs/Page.ocr/photo.png.ocr.md",
"stale": false
}
}
```
第一阶段可以同步执行并在请求内返回最终状态;如果真实耗时过长,再升级为后台 job。同步版本必须设置合理超时,并让 UI 显示运行中状态。
### 查询 OCR 状态
`GET /api/local-folder/ocr/status?rootUri=...&sourceRootRelativePath=...`
返回 `.mnote/ocr-index.json` 中的状态,并重新对比来源文件 `size/mtime` 判断 `stale`
### 查询 OCR 任务列表
`GET /api/local-folder/ocr/jobs?rootUri=...`
返回当前 root 下 active jobs 和最近完成 / 失败任务,用于全局任务栏首次渲染和刷新后恢复。
响应:
```json
{
"ok": true,
"jobs": [
{
"jobId": "ocr_1760000000000_abcd",
"fileName": "spec.pdf",
"ownerDocumentId": "local-md:docs~2FPage.md",
"ownerDocumentPath": "docs/Page.md",
"sourceRootRelativePath": "docs/Page.assets/spec.pdf",
"ocrRootRelativePath": "docs/Page.ocr/spec.pdf.ocr.md",
"status": "mineru_processing",
"stageLabel": "识别中",
"startedAtMs": 1760000000000,
"updatedAtMs": 1760000000000,
"finishedAtMs": null,
"stale": false
}
]
}
```
### 读取 OCR 文本
`GET /api/local-folder/ocr/read?rootUri=...&ocrRootRelativePath=...`
返回 OCR Markdown 文本、frontmatter 摘要、来源路径和 stale 状态。
### 插入 OCR 到正文
`POST /api/local-folder/ocr/insert`
只在用户显式触发时执行。默认插入为链接块:
```markdown
[OCRphoto.png](./Page.ocr/photo.png.ocr.md)
```
可选模式 `inlineSection` 插入正文段落:
```markdown
### OCRphoto.png
<!-- mnote:ocr-source ./Page.assets/photo.png -->
识别文本...
```
## UI 行为
- 编辑器图片 toolbar / 附件右键菜单增加 `OCR 识别`
- File Tree 图片/PDF 资源右键菜单增加 `OCR 识别`
- PDF preview / image resource tab 显示 OCR 状态和结果入口。
- 全局任务栏 / 任务抽屉显示运行中、完成、失败和 interrupted 的 OCR 任务。
- 状态文案只表达:未识别、排队中、上传中、识别中、写入中、已识别、来源已变化、识别失败、已中断。
-`stale` 结果展示“重新识别”操作,不自动覆盖旧 OCR 文件,除非用户确认或传 `force=true`
- OCR 结果打开在 resource tab,不切到普通文档页面。
## 搜索与 AI 消费
- `includeOcr=false` 时不搜索 OCR sidecar。
- `includeOcr=true` 时,搜索 OCR 文本并返回 owner page。
- 搜索 evidence 包含:
- `kind: "ocr"`
- `sourceRootRelativePath`
- `ocrRootRelativePath`
- `snippet`
- 后续 `image_read` / Page AI attachment context 可以读取 OCR sidecar,优先返回已存在 OCR 文本,不让 agent 重复调用 OCR。
## 任务进展与全局入口
OCR 是慢任务,提交后必须有全局可见状态,不应只依赖 toast 或当前 resource tab。
第一阶段进度采用阶段型状态:
```text
queued -> uploading -> mineru_processing -> downloading -> writing_sidecar -> done
```
失败和中断状态:
```text
failed
interrupted
stale
```
状态含义:
- `queued`:任务已创建,等待执行。
- `uploading`:正在上传原始图片/PDF 到 MinerU 预签名地址。
- `mineru_processing`MinerU 正在识别。
- `downloading`:正在下载 MinerU 结果包。
- `writing_sidecar`:正在写入 `{pageStem}.ocr/*.ocr.md``.mnote/ocr-index.json`
- `done`OCR sidecar 已写入。
- `failed`:任务失败,可重试。
- `interrupted`mnote-web 进程或任务 worker 中断,不能确认完成,可重试。
- `stale`:来源文件在 OCR 后发生变化。
后端维护一个运行时 active job store,并把每次状态变更写入 `.mnote/ocr-index.json`。页面刷新后,已完成、失败、stale 和 interrupted 状态可以从磁盘恢复;正在运行中的内存任务如果因进程重启丢失,应在下次读取时标记为 `interrupted`,由用户重试。
状态变化通过现有 realtime / WS / SSE 事件链路广播,不在前端叠加 `setInterval` 轮询主链。事件示例:
```json
{
"type": "local_ocr.job.updated",
"jobId": "ocr_1760000000000_abcd",
"rootUri": "file:///mnt/Data1T/mnote-notes",
"ownerDocumentId": "local-md:docs~2FPage.md",
"sourceRootRelativePath": "docs/Page.assets/spec.pdf",
"ocrRootRelativePath": "docs/Page.ocr/spec.pdf.ocr.md",
"status": "mineru_processing",
"stageLabel": "识别中",
"updatedAtMs": 1760000000000
}
```
前端需要两类展示:
- 资源局部状态:图片/PDF resource tab、附件菜单、File Tree 行显示当前 OCR 状态。
- 全局任务栏 / 任务抽屉:底栏或右上角显示 `OCR 2` / `1 个任务进行中`。点开后列出文件名、所属页面、阶段、开始时间、完成/失败状态,以及“打开 OCR”“重试”等操作。
第一阶段不强制支持取消。若 MinerU 任务 API 后续提供可靠取消,再补 `cancel` 动作。
## 隐私与安全
- OCR 必须由用户手动触发。
- UI 首次触发时应明确提示:文件会发送到 MinerU API。
- 后端只允许读取 `rootUri` 授权根内文件,禁止绝对路径和 `..` 越界。
- 日志不记录 token、预签名 URL、完整外部错误响应和 OCR 正文全文。
- 失败状态写入简短错误码和脱敏消息。
## 实施 Checklist
- [x] 后端:新增 MinerU client,支持本地文件上传、轮询、结果 zip 下载和 Markdown 提取。
- [x] 后端:新增 OCR 路由模块,提供 job/status/read/insert API。(当前 insert 只支持 link 模式)
- [ ] 后端:新增 OCR active job store,记录 queued/uploading/mineru_processing/downloading/writing_sidecar/done/failed/interrupted/stale。
- [ ] 后端:OCR 状态变化写入 `.mnote/ocr-index.json` 并广播 `local_ocr.job.updated` 事件。
- [x] 后端:提供 OCR jobs list API,用于全局任务栏刷新后恢复。
- [x] 后端:实现 `{pageStem}.ocr/` 路径规划、文件名冲突处理和 UTF-8 OCR Markdown 写入。
- [x] 后端:实现 `.mnote/ocr-index.json` 读写、stale 检测和脱敏错误记录。
- [x] 后端:扩展 local-folder 文件分类,让 `{pageStem}.ocr/*.ocr.md` 不进入 Page Tree 普通页面。
- [x] 搜索:扩展 local search index`includeOcr=true` 时索引 OCR sidecar 并返回 owner page。
- [ ] UI:在附件右键菜单、File Tree 资源菜单和资源 tab 增加 OCR 识别入口。
- [ ] UI:展示 OCR 状态、stale、失败和重新识别动作。
- [ ] UI:新增全局 OCR 任务栏 / 任务抽屉,显示任务阶段、所属页面、打开 OCR、重试。
- [~] UI/API:增加“插入 OCR 链接到正文”和可选“插入 OCR 正文段落”动作。(后端 link API 已完成;前端入口和 inlineSection 待做)
- [ ] AI:让 image/PDF 资源上下文优先读取已有 OCR sidecar。
- [x] 测试:Rust 单测覆盖路径规划、frontmatter、索引读写、stale、越界拒绝。
- [x] 测试:Rust route 测试覆盖无 token、真实 MinerU HTTP mock 成功、模拟失败和脱敏错误。
- [x] 测试:local search 测试覆盖 OCR 命中返回 owner pageOCR sidecar 不作为普通页面。
- [~] Smoke:浏览器覆盖图片/PDF 手动 OCR、全局任务栏状态、打开 OCR resource tab、搜索 OCR 命中、插入 OCR 链接。(当前 `task526` 覆盖 mock OCR API、搜索命中和 insert link;真实 UI/任务栏/打开 OCR resource tab 待做)
- [x] 文档:补充 `scripts/TESTING_REFERENCE.md` 中 OCR smoke 基线。
## 验收标准
- 本地图片或图片型 PDF 可被用户手动提交 MinerU OCR。
- OCR 结果落在 owner Markdown 同目录的 `{pageStem}.ocr/` 下,文件为 UTF-8 Markdown。
- `.mnote/ocr-index.json` 删除后,仍能从 OCR Markdown frontmatter 重建核心绑定关系。
- OCR 文件不会污染 Page Tree,也不会作为普通页面搜索结果出现。
- 搜索 OCR 文本时返回 owner page,结果标记 `hasOcr=true`
- 用户未显式选择插入时,页面正文不发生变化。
- 用户提交 OCR 后,即使离开当前资源,也能在全局 OCR 任务栏看到阶段状态;完成后可打开 OCR,失败或中断后可重试。
- 所有外部 API token 和预签名 URL 均不出现在日志、前端 payload 或错误响应中。
## 2026-06-01 后端 mock 闭环
已落地 `rust/crates/mnote-web/src/routes/local_ocr.rs` 与 route 注册,第一刀只承诺后端 mock 闭环:`provider=mock` 可同步生成 `{pageStem}.ocr/*.ocr.md`,写入 `.mnote/ocr-index.json`,提供 jobs/status/read;当时 `provider=mineru` 尚未执行真实外部调用,缺 token 时返回 `mineru_token_missing`,有 token 时返回 `mineru_runtime_not_enabled`
同时已把 OCR sidecar 从 Page Tree 普通 Markdown 扫描中排除,并扩展 local search`includeOcr=false` 不命中 OCR 文本,`includeOcr=true` 返回 owner page,结果带 `hasOcr=true``ocrEvidence`
验证:
- `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 local_search_ocr -- --test-threads=1`
2026-06-01 续补:
- `local_ocr` route 测试新增来源路径越界、非图片/PDF 来源和 mock 失败脱敏 response 形态覆盖。
- `local_ocr` route 测试新增缺 MinerU token response 覆盖:`mineru_token_missing`
- `local_ocr` route 测试新增 token 存在但真实 runtime 未接入 response 覆盖:`mineru_runtime_not_enabled`,避免把 mock OCR 闭环误报为真实 MinerU 能力。该旧口径已在后续真实后端 runtime 补齐后由 HTTP mock 成功路径测试替代。
- 新增 `POST /api/local-folder/ocr/insert`,当前只支持 `mode=link`,显式把 OCR sidecar 链接追加到 owner Markdown`local_ocr_insert_route_appends_explicit_ocr_link` 已覆盖。
2026-06-01 浏览器 API smoke 续补:
- 新增 `scripts/task526-local-folder-ocr-api-smoke.js`,在真实浏览器登录态页面内通过 `fetch` 串起 `POST /api/local-folder/ocr/jobs``status``read``jobs``/api/search/documents includeOcr=true``POST /api/local-folder/ocr/insert`
- 该 smoke 固定使用 `provider=mock`,不触碰真实 MinerU token、上传、轮询、zip 下载和 Markdown 提取链路。
- 已验证 mock OCR sidecar 落盘、`.mnote/ocr-index.json` jobs/status/read 可读、`includeOcr=false` 不命中 OCR 文本、`includeOcr=true` 返回 owner page 且带 `hasOcr/ocrEvidence`、显式 insert 才向 owner Markdown 追加 OCR 链接。
- 已通过:
- `node --check scripts/task526-local-folder-ocr-api-smoke.js`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3301 MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3301 node scripts/task526-local-folder-ocr-api-smoke.js`
- `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 local_search_ocr -- --test-threads=1`
2026-06-01 后端真实 MinerU runtime 续补:
- `local_ocr.rs` 已补 `mineru_api_base_url``mineru_poll_interval``mineru_max_polls` 配置函数。
- `provider=mineru` 后端路径已接入申请上传 URL、PUT 上传、轮询 batch 结果、下载 zip、提取 Markdown。
- 新增 `local_ocr_jobs_route_runs_mineru_runtime_against_http_mock`,用本地 HTTP mock 覆盖真实 client 合同,不访问外网。
- 已通过:
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_ocr -- --test-threads=1`
@@ -1,6 +1,6 @@
# 4-49 Local File Operation 与 Watch Event 合同 v1
> 状态:process
> 状态:done
> Owner04-tree-domain / 03-rust-web
> 背景:Sidex / VSCode Explorer 对照显示,MNote local_folder 的 tree command、watch event、refresh/reveal 和 opened resource 生命周期之间仍缺少稳定合同。当前前端常靠 `relativePath` / `previousRelativePath` 反推刷新父级,watch event 又容易退回全量 resync,导致大目录、批量操作和已打开资源场景下出现重复刷新、状态漂移和错误恢复困难。
@@ -1,6 +1,6 @@
# 5-30 Sidebar 星标置顶快捷入口与 Scoped Explorer 设计 v1
> 状态:process
> 状态:done
> Owner05-editor-mainline / 03-rust-web / control-plane
> 背景:星标置顶需要对齐 Wolai 的 Sidebar 顶部快速访问体验,但 MNote 还需要支持把高频本地文件夹固定成 Explorer scoped root,避免每次进入大 workspace 时加载完整根目录。
@@ -240,3 +240,4 @@ mnote-web
- 2026-05-26:星标置顶确定为用户级 Sidebar 快捷入口,不是文件夹/页面自身属性。
- 2026-05-26:星标快捷入口必须写入 SQLite control-plane,满足同一用户跨设备一致显示。
- 2026-05-26:文件夹星标点击进入 scoped Explorer,避免大 workspace root 扫描。
- 2026-06-01:只读复核确认 `sidebar_shortcuts` migration/store/API、显式 `rootUri`、scoped Explorer 与 `task492-sidebar-starred-shortcuts-smoke.js` 已覆盖本文主验收;本文归档到 `done/`
@@ -1,6 +1,6 @@
# 5-31 页面设置 SQLite 状态收口设计 v1
> 状态:process
> 状态:done
>
> Owner05-editor-mainline / control-plane / mnote-web
>
@@ -1,6 +1,6 @@
# 5-32 FileTree 大目录懒加载与展开状态设计 v1
> 状态:process
> 状态:done
> Owner05-editor-mainline / 03-rust-web
> 背景:用户在 localhost 打开 `mnote/design` 及其子目录时仍感到加载慢,并观察到“全部加载完又折叠,再点很快”。上一版仅靠恢复展开状态不够,因为它没有消除慢请求、重复请求和整树替换。
@@ -1,8 +1,8 @@
# 5-33 FileTree ViewState 与请求代际收口 v1
> 状态:process
> 状态:done
> Owner05-editor-mainline / 03-rust-web
> 前置:`design/05-editor-mainline/process/5-32-filetree-lazy-loading-sidex-alignment-v1.md`
> 前置:`design/05-editor-mainline/done/5-32-filetree-lazy-loading-sidex-alignment-v1.md`
> 背景:5-32 已把 FileTree lazy children、cache 和局部 refresh 推到可用层,但 Sidex / VSCode 对照显示,当前仍缺少统一 view-state、request generation、collapsed stale 和 reveal command。继续只做“展开恢复”会掩盖慢请求、重复请求和过期结果覆盖的问题。
## 1. 结论
@@ -1,6 +1,6 @@
# 5-33 导航页与路由守卫执行清单 v1
> 状态:process
> 状态:done
>
> Owner05-editor-mainline / 03-rust-web / control-plane
>
@@ -14,7 +14,9 @@
- [x] Phase B recent 闭环已落地:control-plane SQLite recent 表、读写 API、SSR recent 展示、打开 folder/page 写入 recent。
- [x] Phase C 主路径已落地:导航页/星标文件夹进入 scoped 导航页;FileTree 普通文件夹点击只展开;Markdown 普通点击记录 recent 并打开文档页。
- [x] Phase D 验证已落地:补 Rust route/API 测试与 `task500-navigation-page-route-guard-smoke.js` browser smoke,覆盖未登录、删除页回退、recent 分组和导航页首屏无 editor bootstrap。
- [ ] 后续增强:文档页 breadcrumb 文件夹段逐级回到导航页、所有 fetch 404/410 的全局浏览器兜底继续拆独立 follow-up;当前服务端 HTML shell 已覆盖 canonical route guard。
- [x] 后续增强已拆出:文档页 breadcrumb 文件夹段逐级回到导航页、所有 fetch 404/410 的全局浏览器兜底继续`design/05-editor-mainline/process/5-35-navigation-breadcrumb-fetch-guard-followup-v1.md` 跟踪;当前服务端 HTML shell 已覆盖 canonical route guard。
2026-06-01 归档说明:Phase A-D 主路径已有 Rust route/API 测试与 `task500-navigation-page-route-guard-smoke.js` 证据;本文归档到 `done/`,剩余增强不继续阻塞导航页主清单。
## 1. 目标
@@ -0,0 +1,46 @@
# 5-35 导航页 breadcrumb 与 fetch guard follow-up v1
> 状态:process
>
> Owner05-editor-mainline / 03-rust-web
>
> 创建时间:2026-06-01
>
> 来源:`design/05-editor-mainline/done/5-33-navigation-page-route-guard-checklist-v1.md` 归档后拆出的增强尾项。
## 背景
`5-33` 已完成导航页主路径:`/` 与 local-folder / folder scope 无明确页面时显示导航页,不再自动打开默认 Markdownrecent folders/pages、删除页 fallback、未登录 route guard、导航页首屏不注入 editor / aggregate 均已有测试和 browser smoke。
剩余问题不应继续阻塞主清单,但仍属于工作区体验尾项:
- 文档页 breadcrumb 的文件夹段应逐级回到对应 folder navigation page。
- 浏览器交互 fetch 返回 `401/404/410` 时,当前页面应有统一恢复策略,而不是只依赖服务端 HTML route guard。
## 目标
- 点击文档页 breadcrumb 文件夹段时,进入对应 `fileTreeScope` 的导航页,并保持 Sidebar Explorer scope 一致。
- 当前页面相关 fetch 返回 `404/410` 时,回到当前资源所在父 folder navigation page,并显示轻量提示。
- 当前交互 fetch 返回 `401` 时,跳转 `/auth?next=<current-url>`
- 保留普通业务错误 toast,不把所有 API 错误都吞成导航跳转。
## 非目标
- 不重新打开 `5-33` 的导航页主清单。
- 不新增第二套 route truthcanonical route guard 仍在 Rust HTML shell。
- 不改变 FileTree 普通文件夹点击只展开的行为。
- 不为所有后台 API 加全局 silent redirect。
## 验收
- [ ] breadcrumb 文件夹段点击后 URL 包含对应 `fileTreeScope`,主区域是 folder navigation page。
- [ ] Sidebar Explorer 与主区域 scope 一致。
- [ ] 当前文档相关 fetch 返回 `404/410` 时回到父 folder navigation page。
- [ ] 当前交互 fetch 返回 `401` 时跳转 auth 并携带 `next`
- [ ] `task500-navigation-page-route-guard-smoke.js` 继续通过,或新增 `task5xx-navigation-breadcrumb-fetch-guard-smoke.js` 覆盖本文尾项。
## 建议验证
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web root_entry -- --test-threads=1`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_folder -- --test-threads=1`
- `node scripts/task500-navigation-page-route-guard-smoke.js`
@@ -2,7 +2,7 @@
> 创建时间:2026-05-17
>
> 当前状态:`PROCESS`
> 当前状态:`DONE`
>
> 2026-05-21 Batch J 口径补充:
> - 本稿的 ACP runtime 核心实现已完成并在当前页面 AI 主链中作为默认 runtime 边界使用;Hermes HTTP proxy 默认关闭,只在显式 compat 开关下保留。
@@ -0,0 +1,128 @@
# 7-38 Page AI sidebar runtime owner split v1
> 创建时间:2026-05-25
> 状态:`done`
> 来源:`design/03-rust-web/done/3-22-sidebar-tree-runtime-second-stage-split-v1.md` Batch A。
> 2026-05-26 更新:`design/10-review/done/16-mnote-web-runtime-module-maintainability-checklist-v1.md` 已把 Page AI 主体迁入 `browser/sidebar-page-ai-runtime.js`;本文件继续作为后续 AI owner 收口 checklist。
## 1. 背景
`rust/crates/mnote-web/browser/sidebar-tree-runtime.js` 曾包含大量 Page AI panel 代码:会话、消息、profile、skills、gateway health、ACP runtime、permission dialog、plan/status event、session search/resume 等。2026-05-26 的 runtime 可维护性批次已将主体迁入 `browser/sidebar-page-ai-runtime.js`2026-06-01 继续迁出事件 delegate 和 `pageAi*` 默认状态。tree runtime 目前只保留 `data-mnote-action="open-page-ai"` 主壳入口、共享 bootstrap 对象和少量触发器代理。
这些代码不是 tree/filetree runtime 的长期 owner。继续把 Page AI 面板放在 `03-rust-web` 的 sidebar runtime 拆分里,会让 tree shell、local folder、AI session 三条边界继续混在一起。
## 2. Owner 判断
- owner`07-ai`
- 运行位置:当前仍可挂在 sidebar UI,但 runtime 模块应独立于 tree/filetree runtime。
- 当前目标模块:`rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js`
- 主壳职责:只加载 asset、提供当前 document/workspace/root/bootstrap。
## 3. 后续执行建议
- [x] 只读审计 Page AI panel 的状态入口:`pageUiState.pageAi*`、storage、session 列表、profile/skills、permission dialog。
- [x] 建立独立 Page AI sidebar owner runtime`sidebar-page-ai-runtime.js`
- [x]`sidebar-tree-runtime.js` 继续迁出 Page AI click/change/input/keydown 委托,tree runtime 只保留 `data-mnote-action="open-page-ai"` 入口。
- [x]`pageUiState.pageAi*` 从通用 sidebar state 中下沉到 Page AI owner runtime,并保留兼容 getter/setter 或明确 bootstrap contract。
- [x]`sidebar-page-ai-runtime.js` 继续超过 2,500 行,再按 `session/profile-skill/permission/conversation/target/render` 拆成子模块。
- [x] 浏览器验证使用页面 AI / ACP smoke,而不是 tree/filetree smoke 替代。
## 4. 非目标
- 不在 03-rust-web 的 sidebar runtime 二阶段里继续扩大 Page AI 实现。
- 不在 10-review runtime 可维护性收口里继续重构 ACP/Hermes session 语义;这里只记录 owner 边界。
- 不改变 Hermes / Reasonix ACP runtime 协议。
- 不把 local-first agent 文件编辑链路改回粗粒度 `mnote.page.save`
## 5. 2026-06-01 复核
本文继续保持 `process`。只读核查确认 `sidebar-page-ai-runtime.js` 当时约 4519 行,已经触发第 3 节的 2500 行继续拆分条件;`sidebar-tree-runtime.js` 当时仍保留 Page AI click/change 委托和 `pageUiState.pageAi*` 状态壳。缺口已记录到 `bugs/07-ai/process/7-53-page-ai-runtime-split-threshold-triggered-v1.md`
下一步最小切片是按 render / conversation / run orchestration 子模块继续拆 `sidebar-page-ai-runtime.js`,降低单文件职责和公开代理面。
## 6. 2026-06-01 第一刀执行记录
已完成第一刀:`sidebar-tree-runtime.js` 不再内联 Page AI 的 click/input/change/keydown 大段 action 分发,也不再调用 `sidebarPageAi.handlePageAi*` 薄委托;`sidebar-page-ai-runtime.js` 承接原有 close、settings、stop、rotate、intent、tab、agent、context、target、skill、tool、session、permission、open-location、suggestion、send、history 等分发语义,并通过 `installPageAiDelegates()` 自行安装事件监听。
验证记录:
- `node --check rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js`
- `node --check rust/crates/mnote-web/browser/sidebar-tree-runtime.js`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web page_ai_acp_runtime_defaults_to_reasonix_and_keeps_hermes_switch -- --test-threads=1`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web page_ai_agent_target_picker_contract_is_visible_and_serialized -- --test-threads=1`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3301 MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3301 node scripts/task490-runtime-surfaces-smoke.js`
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3301 MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3301 node scripts/task502-page-ai-agent-selector-context-smoke.js`
2026-06-01 续补:
- `sidebar-page-ai-runtime.js` 新增并导出 `installPageAiDelegates()`,内部一次性安装 Page AI click/keydown/input/change delegate。
- `sidebar-tree-runtime.js` 仅在自身监听注册后调用 `sidebarPageAi.installPageAiDelegates()`,不再直接处理 Page AI 内部事件。
- 已在临时 `127.0.0.1:3301` Rust web 实例补跑 `task490-runtime-surfaces-smoke.js``task502-page-ai-agent-selector-context-smoke.js`,验证 drawer、agent/context/target/skills 和发送 payload 链路仍可用。
2026-06-01 state facade 续补:
- `sidebar-page-ai-runtime.js` 新增 `ensurePageAiStateFacade()`,集中初始化 Page AI 默认状态并导出该 facade。
- `sidebar-tree-runtime.js``pageUiState` 初始对象删除 `pageAi*:` 默认字段,tree runtime 不再定义 Page AI 状态真相。
- Rust include/assert 已更新为:Page AI runtime 包含 `ensurePageAiStateFacade``pageAiAcpRuntime: 'reasonix'`tree runtime 不包含该默认字段。
2026-06-01 conversation helper 续补:
- 新增 `sidebar-page-ai-markdown-runtime.js`,抽出 `textFromUnknown``renderPageAiMarkdown` 和 inline Markdown 渲染 helper。
- `sidebar-page-ai-runtime.js` 改为 import `createSidebarPageAiMarkdownRuntime()`,保留现有 assistant message 渲染行为。
- 补注册 `/api/mnote-browser-runtime/sidebar-page-ai-markdown-runtime.js` 静态 runtime asset,并更新 runtime asset mount 测试。
2026-06-01 profile helper 续补:
- 新增 `sidebar-page-ai-profile-runtime.js`,抽出 provider/profile/chat-only profile/history filter/usage helper。
- `sidebar-page-ai-runtime.js` 改为 import `createSidebarPageAiProfileRuntime()` 并保留同名代理常量,降低调用点扰动。
- 补注册 `/api/mnote-browser-runtime/sidebar-page-ai-profile-runtime.js` 静态 runtime asset,并更新 runtime asset mount 测试。
- 已在临时 `127.0.0.1:3301` Rust web 实例补跑 `task490-runtime-surfaces-smoke.js``task502-page-ai-agent-selector-context-smoke.js`,验证二级 import 后 drawer、agent/context/target/skills 和发送 payload 链路仍可用。
2026-06-01 permission helper 续补:
- 新增 `sidebar-page-ai-permission-runtime.js`,抽出 ACP permission message、dialog show/hide 和 resolve-permission helper。
- `sidebar-page-ai-runtime.js` 改为 import `createSidebarPageAiPermissionRuntime()` 并保留同名代理常量,主 runtime 继续负责事件流持久化、会话同步和 conversation 渲染入口。
- 补注册 `/api/mnote-browser-runtime/sidebar-page-ai-permission-runtime.js` 静态 runtime asset,并更新 runtime asset mount/layout 断言。
- 已在临时 `127.0.0.1:3301` Rust web 实例补跑 `task490-runtime-surfaces-smoke.js``task502-page-ai-agent-selector-context-smoke.js`,验证新增 permission 二级 import 后 sidebar/Page AI 仍可加载和发送。
2026-06-01 session helper 续补:
- 新增 `sidebar-page-ai-session-runtime.js`,抽出 session storage、backend session list/detail/search/resume/delete、session message sync 和 backend runtime event replay helper。
- `sidebar-page-ai-runtime.js` 改为 import `createSidebarPageAiSessionRuntime()` 并保留同名代理常量,主 runtime 继续负责 Page AI UI render、run orchestration 和事件委托入口。
- 补注册 `/api/mnote-browser-runtime/sidebar-page-ai-session-runtime.js` 静态 runtime asset,并更新 runtime asset mount/layout 断言。
- 已在临时 `127.0.0.1:3301` Rust web 实例补跑 `task490-runtime-surfaces-smoke.js``task502-page-ai-agent-selector-context-smoke.js`,验证新增 session 二级 import 后 Page AI drawer、agent/context/target/skills 和发送 payload 链路仍可用。
2026-06-01 skill helper 续补:
- 新增 `sidebar-page-ai-skill-runtime.js`,抽出 skill source、skill preference、Hermes builtin 隐藏和 Reasonix memory preference helper。
- `sidebar-page-ai-runtime.js` 改为 import `createSidebarPageAiSkillRuntime()` 并保留同名代理常量,主 runtime 继续负责 skill 异步加载、target/run orchestration 和 UI render。
- 补注册 `/api/mnote-browser-runtime/sidebar-page-ai-skill-runtime.js` 静态 runtime asset,并更新 runtime asset mount/layout 断言。
- 已通过 `node --check` 覆盖 Page AI 主 runtime、skill/session/permission/profile/markdown helper 和 `sidebar-tree-runtime.js`
- 已通过 `cargo test --manifest-path rust/Cargo.toml -p mnote-web mnote_browser_runtime_assets_are_explicitly_mounted -- --test-threads=1``page_ai_acp_runtime_defaults_to_reasonix_and_keeps_hermes_switch``page_ai_agent_target_picker_contract_is_visible_and_serialized`
- 已在临时 `127.0.0.1:3301` Rust web 实例补跑 `task490-runtime-surfaces-smoke.js``task502-page-ai-agent-selector-context-smoke.js`,验证新增 skill 二级 import 后 Page AI drawer、agent/context/target/skills 和发送 payload 链路仍可用。
2026-06-01 target helper 续补:
- 新增 `sidebar-page-ai-target-runtime.js`,抽出 OpenEditorsSnapshot target 派生、WorkspacePath、run target snapshot、contextRefs、agentTargetPackage 和 target writable guard helper。
- `sidebar-page-ai-runtime.js` 改为 import `createSidebarPageAiTargetRuntime()` 并保留同名代理常量,主 runtime 继续负责 target popover UI、事件分发和 run orchestration 顺序。
- 补注册 `/api/mnote-browser-runtime/sidebar-page-ai-target-runtime.js` 静态 runtime asset,并更新 runtime asset mount/layout 断言;target picker 合同测试改为在 target helper 中断言 `primaryTargetId` / `targets` / `policy`
- 已通过 `node --check` 覆盖 Page AI 主 runtime、target/skill/session/permission/profile/markdown helper 和 `sidebar-tree-runtime.js`
- 已通过 `cargo test --manifest-path rust/Cargo.toml -p mnote-web mnote_browser_runtime_assets_are_explicitly_mounted -- --test-threads=1``page_ai_uses_backend_acp_session_runtime_store``page_ai_agent_target_picker_contract_is_visible_and_serialized``page_ai_acp_runtime_defaults_to_reasonix_and_keeps_hermes_switch`
- 已在临时 `127.0.0.1:3301` Rust web 实例补跑 `task490-runtime-surfaces-smoke.js``task502-page-ai-agent-selector-context-smoke.js``task520-page-ai-raw-resource-target-smoke.js``task525-page-ai-mindmap-resource-target-smoke.js`,验证新增 target 二级 import 后 Page AI drawer、agent/context/target/skills、raw resource target、mindmap target 和发送 payload 链路仍可用。
2026-06-01 render helper 续补:
- 新增 `sidebar-page-ai-render-runtime.js`,抽出 target/context display helper、agent/profile/model label、skill filter、drawer shell、controls render、suggestions render、conversation render 和 response humanize helper。
- `sidebar-page-ai-runtime.js` 改为 import `createSidebarPageAiRenderRuntime()` 并保留公开方法形状,主 runtime 继续负责 state facade、agent/run orchestration、session/permission/skill/target runtime 接线与事件委托。
- 补注册 `/api/mnote-browser-runtime/sidebar-page-ai-render-runtime.js` 静态 runtime asset,并更新 runtime asset mount/layout 断言。
- `sidebar-page-ai-runtime.js` 当前 2498 行,已低于 2500 行继续拆分阈值。
- 已通过 `node --check` 覆盖 Page AI 主 runtime、render/target/skill/session/permission/profile/markdown helper 和 `sidebar-tree-runtime.js`
- 已通过 `cargo test --manifest-path rust/Cargo.toml -p mnote-web mnote_browser_runtime_assets_are_explicitly_mounted -- --test-threads=1``page_ai_uses_backend_acp_session_runtime_store``page_ai_agent_target_picker_contract_is_visible_and_serialized``page_ai_acp_runtime_defaults_to_reasonix_and_keeps_hermes_switch`
本文已满足本轮 owner split checklist 归档条件;后续只在 runtime 再次超过阈值或 run orchestration 继续膨胀时开新缺陷 / 新设计。
2026-06-01 smoke 复核:
- 已通过 `task490-runtime-surfaces-smoke.js``task502-page-ai-agent-selector-context-smoke.js``task504-page-ai-history-agent-filter-smoke.js``task520-page-ai-raw-resource-target-smoke.js``task525-page-ai-mindmap-resource-target-smoke.js`
- `bugs/07-ai/process/7-53-page-ai-runtime-split-threshold-triggered-v1.md` 已归档到 `bugs/07-ai/done/7-53-page-ai-runtime-split-threshold-triggered-v1.md`
- 本轮 smoke 额外发现并修复当前页 target 继承 stale workspacePath 的缺陷,记录到 `bugs/07-ai/done/7-55-page-ai-current-page-target-stale-workspace-v1.md`
@@ -2,7 +2,7 @@
> 创建时间:2026-05-29
>
> 状态:`PROCESS`
> 状态:`done`
>
> OwnerPage AI skill/tool capability surface + MNote host-side context provider
>
@@ -2,7 +2,7 @@
> 创建时间:2026-05-29
>
> 状态:`process`
> 状态:`done`
>
> OwnerPage AI agent identity / Hermes profile policy / Reasonix memory policy
>
@@ -341,74 +341,104 @@ Hermes profile 切换时:
### Batch A - 现状冻结与风险取证
- [ ] 复核当前 Page AI Hermes skill toggle 的真实写入路径,确认是否直接写 Hermes profile `config.yaml`
- [ ] 复核当前 UI preference 中 `hide_builtin`、MNote 内置 skill enabledprofile skill enabled、default profile 的存储键
- [ ] 复核 Reasonix ACP spawn 环境,确认当前是否默认注入 memory
- [ ] 形成 RED 证据:普通用户修改 shared profile skill 会影响其它用户,或当前缺少服务端权限边界
- [ ] 验证:Rust/JS 只读审计记录在本文档或后续 checklist evidence 中
- [x] 复核当前 Page AI Hermes skill toggle 的真实写入路径,确认是否直接写 Hermes profile `config.yaml`
- 证据:`/api/hermes/client/skills/toggle` 进入 `toggle_skill`,读取 `profile/name/enabled` 后调用 `set_skill_enabled(profile, name, enabled)``set_skill_enabled` 直接写 `profile_home(profile)/config.yaml` 中的 `skills.disabled`。当前没有 profile grant / shared readonly 检查
- [x] 复核当前 UI preference 中 `hide_builtin`、MNote 内置 skill enabled、profile skill enabled、default profile 的存储键
- 证据:前端仍使用 `ai.agent.hermes.profile_id` 保存默认 Hermes profileReasonix skill 开关写 `ai.agent.reasonix.skills.enabled`Hermes profile skill 开关写 `ai.agent.hermes.profile.<profile>.skills.enabled`;隐藏内置技能写 `ai.agent.hermes.profile.<profile>.skills.hide_builtin`。这些 key 是 UI preference,不等价于 profile 授权模型
- [x] 复核 Reasonix ACP spawn 环境,确认当前是否默认注入 memory
- 证据:`AcpRuntimeConfig::reasonix` 默认 env 为空;Reasonix wrapper 构造 `CacheFirstLoop` 时未传 memory policy;本地 Reasonix 实现支持 `REASONIX_MEMORY=off|false|0` 关闭 memory,但 MNote 当前没有默认注入。
- [x] 形成 RED 证据:普通用户修改 shared profile skill 会影响其它用户,或当前缺少服务端权限边界。
- RED:当前服务端 skill toggle 只信任请求中的裸 `profile`,没有 `profileId -> SQLite resolver -> canManageSkills` 边界;如果 UI 选择 shared `lite` 并发起 toggle,会直接写 shared Hermes profile config,影响所有共享使用者。
- [x] 验证:Rust/JS 只读审计记录在本文档或后续 checklist evidence 中。
- 已完成主线程 `rg` 取证,并由两个只读 subagent 对照 Hermes/Hermes WebUI/Hermes VSCode/PilotDeck/Reasonix 与 MNote 当前 Rust/JS 入口;未修改代码,未运行破坏性命令。
### Batch B - SQLite profile policy 合同
- [ ] 新增或扩展 SQLite control-plane profile policy`ai_agent_profiles` / `ai_agent_profile_grants` 或等价结构。
- [ ] 初始化 shared Hermes profile:仅 `lite`,普通用户 `canRun=true``canManageSkills=false`
- [ ] 为每个用户 provision personal Hermes profile
- [ ] 补 Rust 定点测试:personal owner、shared readonly、admin manage、跨用户不可管理
- [ ] 验证:不同用户查询 profile list 只返回自己 personal + shared lite。
- [x] 新增或扩展 SQLite control-plane profile policy`ai_agent_profiles` / `ai_agent_profile_grants` 或等价结构。
- 证据:新增 `007-ai-agent-profile-policy.sql`,并在 `control-plane` store/model/sqlite 中增加 `AiAgentProfile*` 合同与 resolver
- [x] 初始化 shared Hermes profile:仅 `lite`,普通用户 `canRun=true``canManageSkills=false`
- 证据:`ensure_ai_agent_profile_policy` 初始化 `shared_lite`,普通用户 grant 为 run-only
- [x] 为每个用户 provision personal Hermes profile。
- 证据:当前用户首次查询时生成 `usr_<user>_default``mnote-u-<user>-default` isolated profile。
- [x] 补 Rust 定点测试:personal owner、shared readonly、admin manage、跨用户不可管理。
- 证据:`control-plane sqlite::tests::ai_agent_profile_policy_provisions_personal_and_shared_boundaries`
- [x] 验证:不同用户查询 profile list 只返回自己 personal + shared lite。
- 证据:`mnote-web routes::hermes_client::tests::page_ai_agent_profiles_are_sqlite_user_scoped`
### Batch C - Hermes profile resolver
- [ ] 新增服务端 `agentProfileRef` resolver,禁止前端提交任意 Hermes path。
- [ ] `/api/hermes/client/runs` `profileId` 解析真实 Hermes profile
- [ ] `/api/hermes/client/skills``profileId` 解析真实 Hermes profile。
- [ ] 保留旧 `profile=` 参数只作为兼容入口,并映射到当前用户可访问 profile。
- [ ] 验证:旧路径兼容不允许越权访问 shared/admin profile。
- [x] 新增服务端 `agentProfileRef` resolver,禁止前端提交任意 Hermes path。
- 证据:新增 `/api/ai/agent-profiles` `profileId -> SQLite policy -> isolatedProfile` resolver;浏览器不提交 filesystem path
- [x] `/api/hermes/client/runs``profileId` 解析真实 Hermes profile。
- 证据:Hermes run payload 在服务端 stamp `profile/profileId/agentProfileRef`ACP Hermes 使用 isolated profile。
- [x] `/api/hermes/client/skills``profileId` 解析真实 Hermes profile。
- 证据:Hermes skills catalog 按 `profileId` 解析并返回 `agentProfileRef/configurable/readonly/configScope`
- [x] 保留旧 `profile=` 参数只作为兼容入口,并映射到当前用户可访问 profile。
- 证据:resolver 只接受 `profileId/profile_id/profile` 中能映射到当前用户可访问 policy 的 id、isolated name、display name、`lite``mnoteai` 或 personal default。
- [x] 验证:旧路径兼容不允许越权访问 shared/admin profile。
- 证据:普通用户 toggle `shared_lite` Hermes skill 返回 `403 ai_profile_readonly`
### Batch D - Skill toggle 权限收口
- [ ] 修改 skill toggle API:只接受 `profileId + skillName + enabled`
- [ ] 区分 `mnote_builtin` `hermes_profile` skill kind
- [ ] MNote 内置 skill toggle 写当前用户 SQLite preference
- [ ] 拒绝普通用户修改 shared profile 的 Hermes profile skills
- [ ] personal profile skill toggle 只写该用户 isolated profile `config.yaml`
- [ ] shared profile skill toggle 仅 admin 可写
- [ ] 验证:Rust API 测试覆盖 `ai_profile_readonly`、MNote 内置 skill per-user toggle、personal profile skill success
- [x] 修改 skill toggle API:只接受 `profileId + skillName + enabled`
- 证据:服务端接受 `profileId/name/enabled/skillKind`;旧 `profile` 仅作 resolver 兼容
- [x] 区分 `mnote_builtin``hermes_profile` skill kind
- [x] MNote 内置 skill toggle 写当前用户 SQLite preference
- [x] 拒绝普通用户修改 shared profile 的 Hermes profile skills
- [x] personal profile skill toggle 只写该用户 isolated profile `config.yaml`
- [x] shared profile skill toggle 仅 admin 可写
- 证据:control-plane admin grant 可管理 shared;普通用户 shared readonly。
- [x] personal Hermes profile 初始 skill 收口为最小白名单。
- 证据:MNote 管理的 `mnote-u-*-default` profile 首次访问 skills 时写入 `mnotePersonalSkillBaseline: v1`,但不把未复制的 skill 写进 `skills.disabled`
- 2026-05-30 修正:personal Hermes 初始状态改为 profile 模板 copy 语义,首次 provision 只把 `vpn` / `zhihu-search` / `global-search` 复制到该 profile 自己的 `skills/` 目录;后续用户可以继续给自己的 personal profile 增加其它 skill 并启停,不做长期强白名单。`skills.disabled` 只表示该 profile 已有 skill 的关闭状态,不表示模板范围。
- [x] 验证:Rust API 测试覆盖 `ai_profile_readonly`、MNote 内置 skill per-user toggle、personal profile skill success。
- 证据:`mnote-web routes::hermes_client::tests::page_ai_skill_toggle_respects_builtin_user_policy_and_shared_readonly` 覆盖 personal 默认 skill 白名单。
### Batch E - MNote 内置 skill per-user policy
- [ ] 将 MNote 内置 skill enable/disable 保存为 SQLite per-user policy。
- [ ] UI 中 `hide_builtin_skills` 只影响展示,不影响 enable/disable。
- [ ] Skills panel 标记 `builtin=true``configurable=true``configScope=user_sqlite`
- [ ] 服务端 MNote tool policy 按 `user_id + agentId + contextRefs + allowedRoots + mnote_builtin_skill_enabled` 判断
- [ ] 验证:用户 A 禁用某内置 skill 不影响用户 B;禁用后对应 MNote tool 被服务端拒绝;隐藏展示不影响 enable 状态
- [x] 将 MNote 内置 skill enable/disable 保存为 SQLite per-user policy。
- [x] UI 中 `hide_builtin_skills` 只影响展示,不影响 enable/disable。
- 证据:隐藏 key 收口为 `ai.agent.hermes.skills.hide_builtin`MNote 内置 enable 使用独立 `ai.agent.mnote_builtin.skill.<id>.enabled`
- [x] Skills panel 标记 `builtin=true``configurable=true``configScope=user_sqlite`
- [x] 服务端 MNote tool policy 按 `user_id + agentId + contextRefs + allowedRoots + mnote_builtin_skill_enabled` 判断
- 证据:`create_run` 服务端读取当前用户 SQLite preference 并覆盖 `skillPreferences.mnote`,不信任浏览器提交。
- [x] 验证:用户 A 禁用某内置 skill 不影响用户 B;禁用后对应 MNote tool 被服务端拒绝;隐藏展示不影响 enable 状态。
- 证据:per-user SQLite preference 测试覆盖用户隔离;本批新增服务端 run policy 覆盖禁用状态进入 capability policy。工具调用显式拒绝仍可后续细化到每个 tool handler。
### Batch F - Reasonix memory policy
- [ ] 新增 per-user 设置 `ai.agent.reasonix.memory_enabled`,默认 `false`
- [ ] Reasonix ACP spawn 默认设置 `REASONIX_MEMORY=off`
- [ ] 开启 memory 后不注入 `REASONIX_MEMORY=off`,并在 UI 显示 memory enabled。
- [ ] 补 JS/Rust 测试或 smoke,覆盖默认 off、用户开启、不同用户隔离 preference
- [ ] 验证:普通消息由 Reasonix 自己决定是否使用工具;MNote 不再强行注入 memory/context 正文
- [x] 新增 per-user 设置 `ai.agent.reasonix.memory_enabled`,默认 `false`
- [x] Reasonix ACP spawn 默认设置 `REASONIX_MEMORY=off`
- [x] 开启 memory 后不注入 `REASONIX_MEMORY=off`,并在 UI 显示 memory enabled。
- 证据:开启后服务端注入 `REASONIX_MEMORY=on`UI Reasonix 设置页显示 memory 状态
- [x] 补 JS/Rust 测试或 smoke,覆盖默认 off、用户开启、不同用户隔离 preference
- 证据:`mnote-web routes::hermes_client::tests::reasonix_memory_policy_defaults_off_and_reads_user_preference`
- [x] 验证:普通消息由 Reasonix 自己决定是否使用工具;MNote 不再强行注入 memory/context 正文。
- 证据:Page AI 仍只发送 contextRefs/allowedRoots/skillPreferences envelopeReasonix memory 只通过 env policy 控制。
### Batch G - UI 收口
- [ ] Agent selector 中 Hermes profile 显示 personal/shared/readonly 状态。
- [ ] Skills panel 三组均可折叠
- [ ] Hermes profile 切换必须刷新 skill catalog,避免旧 profile skill 残留。
- [ ] shared profile 的 Hermes profile skills 对普通用户展示只读开关或锁定状态。
- [ ] MNote 内置 skills 对普通用户展示可启停状态,并标明按当前 MNote 用户保存。
- [ ] 管理员对 shared profile 显示可管理状态,并提示影响所有用户。
- [ ] 验证:真实浏览器截图覆盖 MNote 内置 skill per-user 可配置、personal Hermes skill 可配置、shared Hermes skill 只读、admin shared 可配置。
- [x] Agent selector 中 Hermes profile 显示 personal/shared/readonly 状态。
- [x] Skills panel 用单一技能来源下拉收口为 `mnote` / `reasonix` / `Hermes_user` / `hermes_lite`,选中哪个只显示哪个来源的 skills
- [x] Hermes profile 切换必须刷新 skill catalog,避免旧 profile skill 残留。
- [x] shared profile 的 Hermes profile skills 对普通用户展示只读开关或锁定状态。
- [x] MNote 内置 skills 对普通用户展示可启停状态,并标明按当前 MNote 用户保存。
- [x] 管理员对 shared profile 显示可管理状态,并提示影响所有用户。
- [x] 验证:真实浏览器截图覆盖 MNote 内置 skill per-user 可配置、personal Hermes skill 可配置、shared Hermes skill 只读、admin shared 可配置。
- 证据:`scripts/task502-page-ai-agent-selector-context-smoke.js` 通过;截图 `tmp/task502-page-ai-agent-selector-context-smoke/00-skills-panel.png` 显示技能来源下拉与 `Hermes_user · 我的 Hermes` 单来源 skill 列表。
- 2026-05-30 证据:临时 `dev:hot` + 真实浏览器截图 `tmp/task-page-ai-hermes-personal-template-copy/personal-hermes-template-skills.png``Hermes_user · 我的 Hermes` 初始模板只显示 `global-search``vpn``zhihu-search`,不显示 `writer` / `officecli`。Rust 测试覆盖“初始 3 个 skillprofile 后续新增 `writer` 后可开启并出现在 catalog”。
### Batch H - 回归矩阵与文档收尾
- [ ] 更新 Page AI 设计说明,明确 MNote 内置 skill per-user policy、personal/shared Hermes profile 与 Reasonix memory policy。
- [ ] 更新 smokeagent 切换、MNote 内置 skill per-user toggle、Hermes profile 切换、shared readonly、personal skill toggle、Reasonix memory off/on。
- [ ] 运行 `node --check` 覆盖相关 browser runtime / smoke。
- [ ] 运行 Rust 定点测试覆盖 SQLite profile policy 与 Hermes skill toggle 权限。
- [ ] 运行真实浏览器验证并截图。
- [ ] 运行 `git diff --check`
- [ ] 涉及代码图后运行 `codegraph sync .`
- [ ] 完成后将本 checklist 移动到 `done/` 或标记为 `done`
- [x] 更新 Page AI 设计说明,明确 MNote 内置 skill per-user policy、personal/shared Hermes profile 与 Reasonix memory policy。
- [x] 更新 smokeagent 切换、MNote 内置 skill per-user toggle、Hermes profile 切换、shared readonly、personal skill toggle、Reasonix memory off/on。
- [x] 运行 `node --check` 覆盖相关 browser runtime / smoke。
- [x] 运行 Rust 定点测试覆盖 SQLite profile policy 与 Hermes skill toggle 权限。
- [x] 运行真实浏览器验证并截图。
- [x] 运行 `git diff --check`
- [x] 涉及代码图后运行 `codegraph sync .`
- [x] 完成后将本 checklist 移动到 `done/` 或标记为 `done`
- 状态已标记为 `done`2026-06-01 已迁入 `design/07-ai/done/`
## 8. 验收口径
@@ -0,0 +1,407 @@
# 7-42 Page AI mindmap skill and resource generation v1
> 创建时间:2026-05-30
> 状态:`done`
> owner`07-ai`
> 上位参考:
> - `design/07-ai/reference/7-28-resource-ai-tool-contract-v1.md`
> - `design/07-ai/done/7-3-page-ai-hermes-panel-and-mnote-plugin-v1.md`
> - `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md`
> - `design/04-tree-domain/done/4-24-resource-tree-filetree-pagetree-source-contract-checklist-v1.md`
## 1. 背景
当前 Page AI 的 MNote 内置技能只有:
- `mnote-current-page`
- `mnote-local-file`
- `mnote-chat-only`
但 Rust Hermes tool manifest 已经暴露了资源工具雏形:
- `mnote.mindmap.fetch`
- `mnote.mindmap.apply_ops`
这说明底层 resource tool 方向已经存在,缺口不在“完全没有工具”,而在 Page AI 缺少一个明确的 `mnote-mindmap` 内置 skill 来指导 agent 何时读取、何时写入、如何生成新的思维导图资源,以及如何把 PDF / Office / Markdown 等材料整理成新的 `.mindmap.json`
用户明确的长期目标是:
> AI 能总结 PDF,并整理新建出思维导图。
因此本设计不能只覆盖“编辑已有导图节点”,还必须覆盖“从外部材料生成新导图资源”的闭环。
## 2. 原型观察
### 2.1 KMind plugin
`reference-code/kmind-plugin` 体现的关键点:
-`simple-mind-map` 风格树结构为核心。
- 支持多根、MOC、文档树导图、节点超链接、TODO、主题、布局、导入导出。
- 节点可以携带思源块/文档引用,说明 mindmap 节点不只是纯文本,还可能是资源引用容器。
- 导入导出会处理 markdown、Freemind、XMind 等格式,但最终仍要落回 mindmap runtime 可消费的数据结构。
对 MNote 的启发:
- AI 不能把思维导图降级为普通 Markdown 大纲后直接覆盖文件。
- AI 写入必须尽量保留未知扩展字段,如节点样式、引用、视图状态、主题配置。
- 从 PDF 生成导图时,应先生成结构化 outline,再转换成 mindmap tree,而不是让模型手写完整 runtime JSON。
### 2.2 lx-doc mind-map
`reference-code/lx-doc/mind-map` 体现的关键点:
- 思维导图项目独立部署,工作台只负责文件/资源管理。
- 文件内容是完整对象,典型形态为:
- `root`
- `theme`
- `layout`
- `config`
- `view`
- runtime 使用 `setFullData` 恢复完整文件,用 `getData(true)` 保存全量配置。
- `root``simple-mind-map` 树:`{ data: { text, uid, ... }, children: [...] }`
对 MNote 的启发:
- mindmap 是 Resource Tree 对象,不是 Markdown 正文的一部分。
- Markdown 页面只保留占位、链接或嵌入引用。
- AI 读写 mindmap 时应围绕 resource 文件、object identity 和 resource capability 工作。
## 3. 当前 MNote 数据合同
当前默认 `.mindmap.json` 不是裸树,而是 envelope
```json
{
"data": {
"children": [],
"data": {
"expand": true,
"isActive": false,
"text": "KMIND",
"uid": "root"
}
},
"view": {
"state": {
"scale": 1,
"sx": 0,
"sy": 0,
"x": -44.99991989135742,
"y": -15.500006675720217
},
"transform": {
"a": 1,
"b": 0,
"c": 0,
"d": 1,
"e": -44.99991989135742,
"f": -15.500006675720217,
"originX": 0,
"originY": 0,
"rotate": 0,
"scaleX": 1,
"scaleY": 1,
"shear": 0,
"translateX": -44.99991989135742,
"translateY": -15.500006675720217
}
}
}
```
设计约束:
- 最终写入文件必须保持 envelope。
- 根节点位于 `data.data`
- 子节点位于 `data.children`
- 新建导图默认 `data.data.uid``root`
- 新建导图默认 `data.data.text` 可由用户材料标题覆盖;没有标题时使用 `KMIND`
- `view` 默认使用上述稳定模板;AI 不应自行发明缩放和位移。
- 修改已有导图时必须保留未知字段,包括 `view`、节点样式、节点引用、主题和未来扩展字段。
## 4. 目标
- 新增 Page AI MNote 内置 skill`mnote-mindmap`
- 让 Hermes / Reasonix 在 Page AI 中知道如何读写当前页面或当前资源 tab 的 mindmap。
- 让 AI 能从 PDF、Office、Markdown、当前页内容或用户粘贴文本生成层级 outline,再创建新的 mindmap resource。
- 让新建导图进入 Resource Tree / File Tree / Page Tree 的正确边界,而不是写进 Markdown 正文。
- 保留 local-first 主线:本地 `.mindmap.json` 是本地导图资源真相;Rust control-plane 负责授权、审计和 resource identity。
## 5. 非目标
- 不在本批实现 PDF OCR / MinerU / Office 文本抽取本身。
- 不把 mindmap 正文存进 Markdown 页面。
- 不让 Page AI 前端私有拼第二套 mindmap 真相。
- 不让 agent 直接大段重写完整 JSON 作为默认写入方式。
- 不引入轮询刷新链路;写入后的 UI 同步应走已有 watcher、resource session 或命令结果驱动刷新。
## 6. Skill 设计
新增内置 skill
```text
id: mnote-mindmap
title: MNote mindmap editing
description: Read, update, summarize, or create MNote mindmap resources, including generating a new mindmap from PDF or document outlines.
agentIds: hermes, reasonix
readOnly: false
requiresContextRefs: current_page, file, folder, resource
toolNames:
- mnote.context.snapshot
- mnote.context.resolve_target
- mnote.mindmap.fetch
- mnote.mindmap.apply_ops
- mnote.mindmap.create_from_outline
```
Skill 正文规则:
- 只有用户明确要求“思维导图 / mindmap / KMind / 脑图 / 从材料生成导图”时启用。
- 先解析目标资源,再读取内容。
- 若当前 Page AI target 是 mindmap resource tab,优先使用该资源。
- 若当前页面包含 mindmap embed,占位中的 `mindmapId` / `sourcePath` 是候选资源。
- 若用户要求“从 PDF 生成导图”,先读取或请求 PDF 摘要/outline,再调用 mindmap 创建工具。
- 写前必须确认 `permissionLevel=read_write`,并确认 `allowedResourceIds` 覆盖目标资源。
- 写后必须回读,并报告新建或变更的 `.mindmap.json`
- 生成 outline 时必须使用“中心主题 + 一级分类 + 短语化节点”的导图结构;禁止把 PDF/文档摘要以长段落塞进节点文本。
- 节点文本优先使用关键词或短语,一个节点只表达一个概念;层级必须用 `children` 表达,引用/页码/来源信息优先放入 `sourceRefs` 或 metadata。
## 7. Tool 面设计
### 7.1 `mnote.mindmap.fetch`
保留现有命名,增强返回结构。
输入:
- `workspaceId`
- `documentId`
- `mindmapId`
- `rootUri`
- `resourcePath`
- `scope`: `tree | subtree | markdown_summary | full_envelope`
- `nodeId`
- `aiAccessScope`
输出:
- `objectIdentity`
- `resourceKind=mindmap`
- `mindmapId`
- `resourcePath`
- `revision`
- `envelope`,仅 `full_envelope` 返回
- `root`
- `nodes`
- `edges`
- `markdownSummary`
- `source=local_folder`
约束:
- `tree` / `markdown_summary` 默认不返回完整 envelope,避免 prompt 被视图和样式噪音污染。
- `full_envelope` 只在需要保留或精确 patch 文件时使用。
### 7.2 `mnote.mindmap.apply_ops`
当前该工具已存在,但 local resource 非 dry-run 仍提示 agent 用原生 patch。后续应收口为真正结构化写入工具。
输入:
- `workspaceId`
- `documentId`
- `mindmapId`
- `rootUri`
- `resourcePath`
- `expectedRevision`
- `ops`
- `aiAccessScope`
建议 ops
- `updateText`
- `insertChild`
- `insertSiblingAfter`
- `deleteNode`
- `setHyperlink`
- `setRefs`
- `appendNote`
- `patchView`
- `setLayout`
- `setTheme`
约束:
- 默认只改 `data` 树和指定 metadata。
- 未知字段必须透传。
- `expectedRevision` 不匹配时拒写或返回 conflict。
- 写入后返回 `revision``changedFiles``markdownSummary`
### 7.3 `mnote.mindmap.create_from_outline`
新增工具,用于“从材料生成新导图”。
输入:
- `workspaceId`
- `documentId`
- `rootUri`
- `targetPageId``targetDocumentId`
- `resourcePath`,可选;缺省由服务端生成唯一 `.mindmap.json`
- `title`
- `outline`
- `sourceRefs`
- `aiAccessScope`
- `embedIntoPage`,默认 `true`
`outline` 建议格式:
```json
[
{
"text": "一级主题",
"children": [
{
"text": "二级主题",
"children": []
}
]
}
]
```
输出:
- `objectIdentity`
- `mindmapId`
- `resourcePath`
- `envelope`
- `revision`
- `changedFiles`
- `embedResult`
- `markdownSummary`
写入规则:
- 服务端负责把 outline 转换为默认 envelope。
- 根节点文本使用 `title`,没有标题时使用 `KMIND`
- 子节点 uid 由服务端稳定生成,避免模型生成重复 uid。
- 默认 view 使用当前稳定模板。
-`embedIntoPage=true`,通过 Resource Tree / Markdown embed 合同把资源绑定到当前页面。
## 8. PDF 到导图闭环
长期目标链路:
```text
PDF resource
-> 文本/OCR/章节提取
-> AI 生成层级 outline
-> mnote.mindmap.create_from_outline
-> 写入 .mindmap.json
-> Resource Tree 绑定当前页面
-> 打开 mindmap resource tab
```
本设计只冻结后半段合同:
- PDF 摘要工具输出应是 `title + outline + sourceRefs`
- `sourceRefs` 可以记录 PDF 文件、页码、章节、引用片段。
- mindmap 节点可把 `sourceRefs` 保存到节点 `refs``note`,但第一版只要求保留到工具审计和 root metadata。
后续可接入:
- `mnote.office.fetch_summary`
- `mnote.pdf.extract_outline`
- MinerU markdown 识别结果
- 本地 Markdown / 当前页正文
## 9. 权限与审计
读写 mindmap resource 必须同时满足:
- 当前 actor 拥有 root 的 read 或 write grant。
- `aiAccessScope.permissionLevel` 覆盖目标操作。
- `allowedResourceIds` 包含 `mindmapId``resourcePath``objectIdentity`
- 写入只能落在授权 root 内。
审计必须记录:
- tool name
- sessionId / runId / traceId
- actorId
- documentId
- mindmapId
- resourcePath
- sourceRefs
- changedFiles
- before / after revision
## 10. Page AI 上下文
Page AI run payload 应能携带当前 mindmap target
```json
{
"contextRefs": [
{
"kind": "resource",
"resourceKind": "mindmap",
"objectIdentity": "resource:mindmap:{documentId}:{mindmapId}",
"documentId": "{documentId}",
"mindmapId": "{mindmapId}",
"resourcePath": "{relativePath}",
"rootUri": "{rootUri}"
}
]
}
```
来源优先级:
1. 当前打开的 mindmap resource tab。
2. 当前页面选中的 mindmap block/embed。
3. 当前页面中唯一 mindmap embed。
4. 用户明确给出的资源文件路径。
5. 创建新导图时,当前页面作为目标页面。
## 11. 验收清单
- [x] MNote 内置技能面板出现 `MNote mindmap editing`
- [x] `mnote-mindmap` 开关能进入 `skillPreferences.mnote`
- [x] `mnote.skill.read` 能读取 `mnote-mindmap` 正文。
- [x] `mnote-mindmap` 正文说明导图 outline 格式:中心主题、一级分类、短语化节点、避免长段落。
- [x] `mnote.mindmap.fetch` 可读取默认 envelope 并返回 root/nodes/summary。
- [x] `mnote.mindmap.create_from_outline` 可生成符合默认 envelope 的 `.mindmap.json`
- [x] `mnote.mindmap.create_from_outline` 在显式 `embedIntoPage=true` 时可把新导图链接写入当前 local-md 页面。
- [x] `task503-mindmap-skill-capability-smoke` 可直接读取 skill、创建能力导图、fetch 回读、绑定当前页面,并用真实登录浏览器截图验证。
- [x] 新建导图能绑定当前页面并在 File Tree / Resource Tab 可见。
- [x] `mnote.mindmap.apply_ops` 写入后保留 `view` 和未知字段。
- [x] shared/read-only scope 下写工具拒绝。
- [x] revision 不匹配时拒写或返回 conflict。
- [x] PDF outline fixture 可生成 mindmap resource。
- [x] 真实 mindmap resource tab 能注入 Page AI `active_editor` contextRef / `targetPackage`
当前第一批已完成 Page AI skill 发现、skill 正文读取、默认 envelope fetch、`create_from_outline` 写入、显式 `embedIntoPage=true` 页面链接绑定、`task502` Page AI skill payload smoke,以及 `task503` 直接 tool 创建导图 + 页面绑定 + 浏览器截图 smoke。第二批已完成 `apply_ops` 最小结构化写入、`view` / 未知字段保留、revision conflict 和 shared/read-only 拒写单测;2026-06-01 已修复 `task455` 暴露的 local-folder embedded mindmap 刷新后 id 退化,根因是 `blockDocument.blocks[].attrs` 未保留 `mindmapId/sourcePath/rootNodeId` 且浏览器 conversion 未读取 blockDocument attrs;新增 `task525` 覆盖真实 mindmap resource tab 的 Page AI `active_editor` contextRef / `targetPackage`
2026-06-01 复核:`mnote.mindmap.apply_ops` 已从 native patch 提示收口到最小结构化写入,当前支持 `updateText` / `updateNode``insertChild` / `addChild``deleteNode`,并由 `hermes_tools_mindmap_apply_ops_writes_and_preserves_envelope_fields``hermes_tools_mindmap_apply_ops_rejects_stale_revision``hermes_tools_mindmap_apply_ops_shared_read_is_forbidden` 覆盖。resource tab / File Tree 可见性和 Page AI mindmap contextRefs 已由 `task455``task524``task525` 验收。后续若继续扩展 `moveNode``insertSiblingAfter``setHyperlink``setRefs``appendNote``patchView`,应另拆 P2 follow-up,不阻塞本轮最小闭环。
## 12. 第一批执行建议
第一批只做可控闭环:
1. 新增 `skills/mnote-mindmap/SKILL.md`
2.`hermes_tools::skill` 注册 `mnote-mindmap`
3. 补 Page AI skill 面板 smoke。
4.`mnote.skill.read` 单测。
5. 增强 `mnote.mindmap.fetch` 输出,识别当前默认 envelope。
6. 新增 `mnote.mindmap.create_from_outline` dry-run 与真实写入。
7. 用 fixture 模拟 PDF 摘要,不接真实 PDF OCR。
第二批再做:
1. 补浏览器 smoke,验证新建或修改后的 mindmap 能在 File Tree / Resource Tab 可见。
2. 接入真实 PDF / Office / MinerU 结果。
3. 在资源 tab 中把当前 mindmap 自动注入 Page AI contextRefs。
4. 按真实编辑需求继续扩展 `apply_ops`,例如 `moveNode``insertSiblingAfter``setHyperlink``setRefs``appendNote``patchView`
@@ -0,0 +1,292 @@
# 7-44 ChatOnly Doubao session binding v1
> 创建时间:2026-05-31
>
> 状态:`done`
>
> OwnerPage AI ChatOnly / Hermes ACP runtime / SQLite control-plane / OpenClaw Doubao Web provider
>
> 上位依据:
> - `design/07-ai/done/7-25-acp-session-runtime-enhancement-plan-v1.md`
> - `design/07-ai/done/7-30-acp-session-load-resume-checklist-v1.md`
> - `design/07-ai/done/7-33-acp-session-info-plan-ui-checklist-v1.md`
> - `design/07-ai/done/7-39-page-ai-agent-selector-context-authorization-settings-v1.md`
> - `design/07-ai/done/7-41-page-ai-hermes-reasonix-user-profile-isolation-v2.md`
## 1. 背景
MNote Page AI 的 `Chat-only / 豆包` 当前已经能通过 Hermes ACP -> OpenClaw `doubao-web` provider 调用豆包网页,但 MNote 会话与豆包网页会话还没有稳定统一:
- MNote 的会话主记录在 SQLite `ai_runtime_runs` / `ai_runtime_events`,按 `user_id + workspace_id + session_id` 查询和软删除。
- Hermes / ACP 层有 `acpSessionId`MNote 会通过 `session.info.updated` 持久化它。
- OpenClaw `doubao-web` provider 会从豆包 SSE 中捕获 `conversation_id`,并在进程内 `sessionMap` 中用 ACP `sessionId` 复用豆包会话。
-`sessionMap` 没有落 SQLite。MNote 重启、OpenClaw 重启、跨用户、删除会话时,都无法可靠知道某个 MNote ChatOnly session 对应哪条豆包网页 conversation。
2026-05-31 已验证 ChatOnly 豆包重复会话的直接根因是 Hermes `title_generation` 复用了豆包主模型;当前已在 `openclaw-doubao-chat` profile 禁用标题生成辅助调用。但这只解决“一条消息变两条豆包会话”的触发点,不解决会话生命周期统一。
## 2. 目标
本阶段目标是让 MNote 成为 ChatOnly 豆包会话的本地控制面:
- MNote ChatOnly session 与豆包 `conversation_id` 建立持久绑定。
- 后续同一个 MNote session 继续发送时,复用同一个豆包 conversation。
- 删除 MNote session 时,对豆包远端 conversation 做最佳努力删除。
- 多用户会话隔离以 SQLite `user_id` 为准,不能只依赖 OpenClaw 全局内存。
- 删除失败不能阻塞 MNote 本地删除,但必须可审计、可重试。
非目标:
- 不把豆包网页作为 MNote 会话真源。
- 不同步豆包网页中用户手工创建的所有历史会话。
- 不承诺豆包接口稳定可用;远端删除属于 provider-specific best effort。
- 不在 ChatOnly 下申请 MNote 文件写权限。
## 3. 豆包侧接口取证
### 3.1 发送 / 继续会话
当前 OpenClaw `DoubaoWebClientBrowser` 发送消息使用:
```text
POST /samantha/chat/completion
```
请求体关键字段:
```json
{
"completion_option": {
"need_create_conversation": false,
"is_delete": false
},
"conversation_id": "38428454119180290"
}
```
`conversation_id` 为空或 `"0"` 时,豆包创建新 conversation。响应 SSE 中会出现 `conversation_id`OpenClaw 已能解析并打印:
```text
[Doubao Web Browser] Captured conversation_id: ...
```
### 3.2 删除会话
从豆包当前 Web UI 已加载脚本和 CDP 请求监听确认,删除会话优先走 IM cmd 链路:
```text
POST /im/conversation/batch_del_user_conv
```
请求头关键字段:
```text
content-type: application/json; encoding=utf-8
accept: application/json, text/plain, */*
agw-js-conv: str
```
请求体结构:
```json
{
"cmd": 4171,
"uplink_body": {
"batch_delete_user_conversation_uplink_body": {
"conversation_id": ["38428454119180290"],
"delete_all": false,
"conversation_type": 3
}
},
"sequence_id": "uuid",
"channel": 2,
"version": "1"
}
```
其中 `conversation_type: 3` 对应 `ONE_TO_BOT_CHAT`
脚本中还存在旧 wrapper
```text
POST /samantha/im/conversation/batch_delete
```
但当前删除弹窗路径使用 `/im/conversation/batch_del_user_conv`,实现优先采用该路径。
## 4. 数据合同
新增 SQLite 控制面表:
```text
ai_external_conversation_bindings
- id
- user_id # 当前 MNote 用户,必填
- workspace_id # 可空,但列表/删除必须按上下文过滤
- mnote_session_id # MNote ChatOnly session_id
- acp_session_id # Hermes/OpenClaw ACP sessionId,可空,收到后补齐
- agent_id # chat_only
- profile # openclaw-doubao-chat 等
- provider # doubao-web
- remote_conversation_id # 豆包 conversation_id
- remote_url # https://www.doubao.com/chat/{remote_conversation_id}
- status # active | local_deleted | remote_deleted | remote_delete_failed
- metadata_json # 捕获来源、失败原因、最后响应摘要
- created_at
- updated_at
- deleted_at
```
约束:
- `user_id + provider + remote_conversation_id` 唯一,避免同一豆包会话绑定给多个 MNote 用户。
- `user_id + mnote_session_id + provider` 唯一,避免一个 MNote session 绑定多条豆包远端会话。
- 查询、恢复、删除都必须带当前 `user_id`;不能只按 `mnote_session_id` 查。
## 5. 数据流
### 5.1 创建 MNote ChatOnly session
1. 前端 `pageAiEnsureHermesSession(forceCreate=true)``/api/hermes/client/sessions`
2. mnote-web 写入 SQLite session index run`status=session.created`
3. 不立即创建豆包远端 conversation。
4. 绑定表暂不写,或写入一行 `remote_conversation_id=NULL` 的 pending 记录。
### 5.2 第一次发送
1. mnote-web 创建 runpayload 携带 `sessionId``agentId=chat_only``profile=openclaw-doubao-chat`
2. mnote-web 查询绑定表;若无 `remote_conversation_id`,不传远端 ID。
3. OpenClaw 发送给豆包,豆包创建新 conversation。
4. OpenClaw 从 SSE 捕获 `conversation_id`
5. OpenClaw 需要把 `conversation_id` 作为结构化事件回传给 MNote。建议事件:
```json
{
"event": "provider.conversation.bound",
"data": {
"provider": "doubao-web",
"remoteConversationId": "38428454119180290",
"remoteUrl": "https://www.doubao.com/chat/38428454119180290"
}
}
```
6. mnote-web 在 `persist_acp_runtime_event` 中识别该事件,upsert `ai_external_conversation_bindings`
### 5.3 继续发送
1. mnote-web 根据 `user_id + mnote_session_id + provider=doubao-web` 查询绑定。
2. 若找到 active `remote_conversation_id`,在传给 ACP/OpenClaw 的 payload 中加入:
```json
{
"providerConversation": {
"provider": "doubao-web",
"remoteConversationId": "38428454119180290"
}
}
```
3. OpenClaw Doubao provider 使用该 ID 调 `/samantha/chat/completion`,设置 `need_create_conversation=false`
4. 若豆包返回会话不存在或被删除,OpenClaw 发 `provider.conversation.missing`;MNote 标记绑定异常,由用户决定是否新建远端会话。
### 5.4 删除 MNote session
1. 用户在 MNote 删除 ChatOnly 会话。
2. mnote-web 先对 `ai_runtime_runs` 做本地软删除。
3. mnote-web 将绑定表标记为 `local_deleted`
4. 若存在 `remote_conversation_id`,调用 OpenClaw / provider adapter 执行远端删除:
```text
POST /im/conversation/batch_del_user_conv
```
5. 成功后标记 `remote_deleted`
6. 失败时保持本地删除已完成,绑定标记 `remote_delete_failed``metadata_json` 记录错误码、响应摘要和时间。
7. UI 提示:`MNote 会话已删除,豆包远端删除失败,可稍后重试`
## 6. API 边界
### 6.1 mnote-web 内部 API
建议新增 provider conversation helper,不把豆包细节散落在 `hermes_client.rs`
```text
rust/crates/mnote-web/src/provider_conversations.rs
```
职责:
- 解析 run payload 中的 ChatOnly provider。
- 读写 `ai_external_conversation_bindings`
- 将 binding 注入 ACP run payload。
- 处理 `provider.conversation.bound/missing/deleted/delete_failed` 事件。
### 6.2 OpenClaw Doubao provider
需要在 OpenClaw `doubao-web` provider 增加三个能力:
- 从 ACP/context payload 读取 `providerConversation.remoteConversationId`
- 捕获新 `conversation_id` 后发结构化事件,不能只写 console log。
- 暴露 `deleteConversation(remoteConversationId)`,内部走 `/im/conversation/batch_del_user_conv`
第一阶段如果 ACP 不支持 provider 自定义 RPC,可先由 mnote-web 调一个 OpenClaw 本地 HTTP helper;但长期应收口到 provider adapter。
## 7. 错误处理
| 场景 | 行为 |
| --- | --- |
| 豆包创建成功但未捕获 `conversation_id` | run 仍完成;绑定缺失;下一轮可能新建远端会话;UI 标记未绑定 |
| 绑定表有 ID,但豆包返回不存在 | 标记 `remote_missing`,提示用户重新绑定或新建 |
| 删除 MNote 本地成功,豆包远端失败 | 不回滚本地删除;标记 `remote_delete_failed` |
| 多用户尝试绑定同一远端 ID | 拒绝后写 audit,避免跨用户串会话 |
| OpenClaw 重启 | SQLite 绑定仍在;下一轮从 MNote 注入远端 ID |
| 豆包接口变更 | 本地会话不受影响;远端能力降级为不可用 |
## 8. 验收
### 8.1 单元 / 集成
- `control-plane`binding upsert / lookup / local delete / remote delete status transition。
- `mnote-web`ChatOnly run payload 能注入已有 `remoteConversationId`
- `mnote-web``provider.conversation.bound` 事件能写入 SQLite。
- `mnote-web`:删除 session 时先软删除本地,再 best-effort 调 provider delete。
### 8.2 真实浏览器 smoke
1. 使用测试账号登录 `http://localhost:3000`
2. 创建 ChatOnly / 豆包新会话,发送 marker A。
3. 复查:
- MNote SQLite 有一个 `mnote_session_id -> remote_conversation_id` 绑定。
- 豆包日志 `Captured conversation_id` 一次。
4. 在同一 MNote 会话发送 marker B。
5. 复查:
- 豆包日志第二次 `Conversation ID` 等于第一次捕获值。
- 豆包网页同一 conversation 中出现 A 与 B。
6. 删除 MNote 会话。
7. 复查:
- MNote 会话列表不再显示该 session。
- SQLite binding status 为 `remote_deleted``remote_delete_failed`
- 若远端删除成功,豆包网页侧该 conversation 从列表移除或打开后显示已删除。
2026-06-01 验证记录:
- `node scripts/task512-chatonly-doubao-sync-smoke.js` 通过:豆包远端 conversation 绑定、同会话回复、MNote session 删除、provider delete 和 SQLite binding `remote_deleted` 均通过。
- `node scripts/task513-chatonly-provider-sync-smoke.js deepseek` 通过:DeepSeek `remoteConversationId` 绑定、provider delete 和 SQLite binding `remote_deleted` 均通过。
- `node scripts/task513-chatonly-provider-sync-smoke.js gemini` 通过:Gemini conversation URL 绑定、provider delete 和 SQLite binding `remote_deleted` 均通过。
- `cargo test --manifest-path rust/Cargo.toml -p control-plane external_conversation -- --test-threads=1` 通过:binding user scope 与状态迁移。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib provider_conversation -- --test-threads=1` 通过:provider conversation 注入与 bound event 持久化。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib chatonly_doubao_session_delete_calls_provider_and_marks_remote_deleted -- --test-threads=1` 通过:删除 session 后 provider delete 与 `remote_deleted` 状态。
## 9. 实施清单
- [x]`control-plane` 增加 `ai_external_conversation_bindings` schema、store trait 和 SQLite 实现。
- [x]`mnote-web` 增加 provider conversation helper,避免继续扩大 `hermes_client.rs`
- [x] 在 ACP run 创建时为 ChatOnly / 豆包注入已有 `remoteConversationId`
- [x] 在 ACP event 持久化时处理 `provider.conversation.bound`
- [x] 在 session delete 路径中加入远端删除 best-effort 状态机。
- [x] 在 OpenClaw Doubao provider 中加入结构化 conversation bound 事件。
- [x] 在 OpenClaw Doubao provider 中加入 `deleteConversation`,走 `/im/conversation/batch_del_user_conv`
- [x] 补真实浏览器 smoke`node scripts/task512-chatonly-doubao-sync-smoke.js` 覆盖远端 conversation id 绑定、豆包同会话 marker、session 删除、provider delete 和 SQLite binding `remote_deleted`
- [x] 补跨 provider 真实浏览器 smoke`node scripts/task513-chatonly-provider-sync-smoke.js deepseek``node scripts/task513-chatonly-provider-sync-smoke.js gemini`
- [x] 补 MNote Rust 单测:control-plane binding、mnote-web provider conversation 注入、bound event 持久化和 session delete provider 状态。
- [x] OpenClaw provider 源码不在本仓库,provider 单测缺口已转入 `bugs/07-ai/process/7-49-chatonly-openclaw-provider-unit-test-gap-v1.md` 跟踪,不阻塞 MNote 侧设计归档。
@@ -6,7 +6,7 @@
>
> 上位依据:
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
> - `/mnt/Data1T/mnote/CURRENT_ARCHITECTURE.md`
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-8-mvp-post-process-execution-order-v1.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/done/7-15-page-ai-acp-agent-runtime-unified-layer-v1.md`
@@ -276,10 +276,10 @@ Rust SQLite control-plane 是默认控制面,负责:
### Phase A0target selection 与 UI
- [ ] Page AI composer 可见 target chip,显示 workspace、target title、resourceKind、scope、readonly / dirty 状态。
- [ ] 点击 target chip 打开 target picker,可在当前焦点、打开 tabs、树选中项之间选择。
- [ ] 默认 target 来自 last focused editor / resource tab,不来自第一个 tab、URL documentId 或树选中项。
- [ ] mindmap / office / raw file target 显示真实 resourceKind,不伪装成 markdown page。
- [x] Page AI composer 可见 target chip,显示 workspace、target title、resourceKind、scope、readonly / dirty 状态。
- [x] 点击 target chip 打开 target picker,可在当前焦点、打开 tabs、树选中项之间选择。
- [x] 默认 target 来自 last focused editor / resource tab,不来自第一个 tab、URL documentId 或树选中项。
- [x] mindmap / office / raw file target 显示真实 resourceKind,不伪装成 markdown page。
- [ ] 多 tab / 跨 workspace / dirty write 时发送前需要显式确认。
- [ ] 发送后消息记录冻结 target 摘要,切换 tab 不影响已启动 run。
@@ -291,6 +291,8 @@ Rust SQLite control-plane 是默认控制面,负责:
- [ ] allowed roots 外文件写入被 agent runtime 或 MNote 审计层拒绝。
- [ ] readonly 页面不会启动可写 run,或只启动只读问答 run。
2026-06-01 进展:已补第一版 `mnote.agent_target_package.v1` 运行时输入。前端 Page AI run body 会从当前 `editorTarget` / `WorkspacePath` 生成 `targetPackage`;后端会 sanitize 该包,并把 local-first `aiAccessScope.allowedFiles` / `allowedFilePaths``agentRunEnvelope.allowedFiles` 从该包派生。`task502-page-ai-agent-selector-context-smoke.js` 已覆盖 page / mindmap / OnlyOffice target picker 与 payload 冻结;`task520-page-ai-raw-resource-target-smoke.js` 已覆盖真实 raw local resource tab,并断言 `objectIdentity` 不退化为 `[object Object]`。当前仍未完成跨 workspace 多选确认和真实 agent 写入回收 smoke,因此本文继续保持 `process`
### Phase B:写入回收与前台同步
- [ ] agent 修改当前 `.md` 后,MNote 能回收 changed files / diff。
@@ -1,35 +0,0 @@
# 7-38 Page AI sidebar runtime owner split v1
> 创建时间:2026-05-25
> 状态:`process`
> 来源:`design/03-rust-web/done/3-22-sidebar-tree-runtime-second-stage-split-v1.md` Batch A。
> 2026-05-26 更新:`design/10-review/done/16-mnote-web-runtime-module-maintainability-checklist-v1.md` 已把 Page AI 主体迁入 `browser/sidebar-page-ai-runtime.js`;本文件继续作为后续 AI owner 收口 checklist。
## 1. 背景
`rust/crates/mnote-web/browser/sidebar-tree-runtime.js` 曾包含大量 Page AI panel 代码:会话、消息、profile、skills、gateway health、ACP runtime、permission dialog、plan/status event、session search/resume 等。2026-05-26 的 runtime 可维护性批次已将主体迁入 `browser/sidebar-page-ai-runtime.js`tree runtime 目前只保留 sidebar click/change/input 委托、少量 `pageUiState.pageAi*` 状态壳和触发器代理。
这些代码不是 tree/filetree runtime 的长期 owner。继续把 Page AI 面板放在 `03-rust-web` 的 sidebar runtime 拆分里,会让 tree shell、local folder、AI session 三条边界继续混在一起。
## 2. Owner 判断
- owner`07-ai`
- 运行位置:当前仍可挂在 sidebar UI,但 runtime 模块应独立于 tree/filetree runtime。
- 当前目标模块:`rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js`
- 主壳职责:只加载 asset、提供当前 document/workspace/root/bootstrap。
## 3. 后续执行建议
- [x] 只读审计 Page AI panel 的状态入口:`pageUiState.pageAi*`、storage、session 列表、profile/skills、permission dialog。
- [x] 建立独立 Page AI sidebar owner runtime`sidebar-page-ai-runtime.js`
- [ ]`sidebar-tree-runtime.js` 继续迁出 Page AI click/change/input/keydown 委托,tree runtime 只保留 `data-mnote-action="open-page-ai"` 入口。
- [ ]`pageUiState.pageAi*` 从通用 sidebar state 中下沉到 Page AI owner runtime,并保留兼容 getter/setter 或明确 bootstrap contract。
- [ ]`sidebar-page-ai-runtime.js` 继续超过 2,500 行,再按 `session/profile-skill/permission/conversation` 拆成子模块。
- [ ] 浏览器验证使用页面 AI / ACP smoke,而不是 tree/filetree smoke 替代。
## 4. 非目标
- 不在 03-rust-web 的 sidebar runtime 二阶段里继续扩大 Page AI 实现。
- 不在 10-review runtime 可维护性收口里继续重构 ACP/Hermes session 语义;这里只记录 owner 边界。
- 不改变 Hermes / Reasonix ACP runtime 协议。
- 不把 local-first agent 文件编辑链路改回粗粒度 `mnote.page.save`
@@ -0,0 +1,273 @@
# 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。
@@ -2,7 +2,7 @@
## 状态
- 状态:process
- 状态:reference
- OwnerAI runtime / browser verification
- 背景:Reasonix 已能作为独立辅助 agent 执行只读审计,但浏览器验收若只输出自然语言结论,主控仍需重测,无法稳定节省 token。
@@ -144,3 +144,7 @@ Codex/Hermes 只有在以下条件满足时,才能把 Reasonix 浏览器结果
- [ ] 使用 `bugs/0524.md` 中至少 1 个 bug 进行 Reasonix 浏览器试跑。
- [ ] 试跑后记录 accepted findings / false leads / retest required。
- [ ] 根据试跑结果更新本合同或 Reasonix skill。
## 2026-06-01 降级说明
本合同对应的全局 `reasonix-browser-tester` skill 已存在,并已包含 `result.json`、截图、console/network、isolated context、`modified_files` 等 artifact 要求。本文不再作为 MNote 产品 runtime 的 active process 项,降级为协作参考;后续若要继续试跑 `bugs/0524.md`,应在 Reasonix skill 维护流程或独立复盘文档中跟踪。
@@ -2,7 +2,7 @@
## 状态
- 状态:process
- 状态:reference
- OwnerAI runtime / multi-agent collaboration
- 背景:Hindsight 解决了 Reasonix 过程可追溯问题,但不能自动让 Reasonix 下一次更好。需要一个主控驱动的最小自进化机制,由 Codex/Hermes 根据真实 run 修改 Reasonix skill、任务模板和 handoff 合同。
@@ -144,3 +144,7 @@ Reasonix 协同复盘:
- [ ] 用 `bugs/0524.md` 至少跑一轮真实 Reasonix 协同测试。
- [ ] 根据真实结果更新 skill 或明确“不需要修改”。
- [ ] 在 Hindsight / MemPalace 中保留协作复盘摘要。
## 2026-06-01 降级说明
全局 `reasonix-skill-maintainer` skill 已存在,并已要求读取 `result.json``process-handoff`、Hindsight recall,评估 `accepted_findings``false_leads``missing_evidence``retest_required``token_saving_estimate`。本文不再作为 MNote 产品 runtime 的 active process 项,降级为协作参考;后续真实 run 复盘直接走全局 skill。
@@ -2,7 +2,7 @@
> 更新时间:2026-05-11
>
> 当前状态:`reference`。本文只保留思源参考边界与可借鉴能力,不覆盖 `01-05` 当前优先级,也不作为当前执行 checklist。
> 当前状态:`reference`。本文只保留思源参考边界与可借鉴能力,不覆盖 `CURRENT_ARCHITECTURE.md` / `1-8` 当前口径,也不作为当前执行 checklist。
>
> 上位依据:
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
@@ -15,6 +15,8 @@
> - `/mnt/Data1T/mnote/design/old/07-ai/process/7-14-local-first-ai-markdown-editing-convergence-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
>
> 2026-06-01 口径补充:本文是 2026-05-18 的历史 review / checklist 快照。文中把 `mnote.doc.markdown_edit` 描述为简单正文编辑主路径的结论已被后续 local-first agent 文件编辑控制面覆盖;当前 local-first 普通 Markdown 编辑主路径以 `AGENTS.md`、`ARCHITECTURE.md` 与 `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md` 为准。`mnote.doc.markdown_edit` 只保留为 remote/cloud/compat fallback 或结构校验辅助。
---
@@ -14,7 +14,9 @@
## 0. 归档说明
本文件是 2026-05-16 的页面 AI fast block edit 历史审查快照。其记录的 `local_rule` / `doc_apply_block_ops` 快路径被后续 `mnote.doc.markdown_edit` 主路径替代,不再作为当前 runtime 口径。
本文件是 2026-05-16 的页面 AI fast block edit 历史审查快照。其记录的 `local_rule` / `doc_apply_block_ops` 快路径被后续 `mnote.doc.markdown_edit` 阶段替代,但该阶段也已被 local-first agent 文件编辑控制面覆盖;本文不再作为当前 runtime 口径。
2026-06-01 当前有效口径:local-first 普通 Markdown 编辑默认走授权文件引用 + allowed roots/files + agent 原生 patch/diff + watcher/BufferStore/Page Aggregate 同步;`mnote.doc.markdown_edit` 只保留为 remote/cloud/compat fallback 或结构校验辅助。
当前有效口径见:
@@ -23,7 +25,7 @@
- [7-18 AI markdown_edit 阶段状态合同漂移](../../../bugs/07-ai/done/7-18-ai-markdown-edit-phase-state-contract-drift-v1.md)
- [7-20 page_ai_workflow 绕过 Hermes tool executor / audit / toggle](../../../bugs/07-ai/done/7-20-page-ai-workflow-bypasses-hermes-tool-executor-v1.md)
归档后的结论:`page-ai/block-edit-workflow` 仍是简单编辑 fast-path 入口,但当实现应通过模型生成 markdown search/replace / full_content 后调用 `mnote.doc.markdown_edit`,并复用统一 mnote tool executor`mnote.doc.apply_block_ops` / `mnote.block.*` 只作为结构性块操作辅助。
归档时的历史结论:`page-ai/block-edit-workflow` 仍是简单编辑 fast-path 入口,但当实现应通过模型生成 markdown search/replace / full_content 后调用 `mnote.doc.markdown_edit`,并复用统一 mnote tool executor`mnote.doc.apply_block_ops` / `mnote.block.*` 只作为结构性块操作辅助。该结论不指导当前 local-first 新实现。
## 1. 本轮结论
@@ -5,6 +5,8 @@
> 执行状态:`done`
>
> 范围:当前主线中 Page Aggregate 单一真源、页面 AI 快速编辑、`mnote.doc.markdown_edit` 与 ACP / Hermes runtime 的源码级定向审查。
>
> 2026-06-01 口径补充:本文是 2026-05-17 的历史 review 快照。文中“`mnote.doc.markdown_edit` 是简单正文编辑主路径”的结论已被后续 local-first agent 文件编辑控制面覆盖;当前 local-first 普通 Markdown 编辑主路径以 `AGENTS.md``ARCHITECTURE.md``design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md` 为准,即授权文件引用 + allowed roots/files + agent 原生 patch/diff + watcher/BufferStore/Page Aggregate 同步。`mnote.doc.markdown_edit` 只保留为 remote/cloud/compat fallback 或结构校验辅助。
## 1. 本轮结论
@@ -8,6 +8,8 @@
## 1. 结论
> 2026-05-31 口径补充:本文是 2026-05-17 的阶段性 review 快照。文中关于 `mnote.doc.markdown_edit` “已是简单正文编辑主路径”的表述已被后续 local-first agent 文件编辑控制面覆盖;当前 local-first 普通 Markdown 编辑主路径以 `AGENTS.md``CURRENT_ARCHITECTURE.md``ARCHITECTURE.md``design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md` 为准,即“授权文件引用 + allowed roots + agent 原生 patch/diff + watcher/BufferStore/Page Aggregate 同步”。`mnote.doc.markdown_edit` 只作为 cloud / remote agent / compat fallback 或复杂结构辅助边界。
当前 mnote 的主线架构已经成形。本轮 review 识别出的 P0 / 内核优先缺陷已完成代码修复、文档迁移和定向验证。
已修复的四个关键冲突:
@@ -0,0 +1,99 @@
# 18 当前 design 口径与 bug 寻找 Review v1
> 创建时间:2026-05-31
>
> 状态:`done`
>
> 范围:当前仓库、历史 design 口径、active `process/`、现有 `bugs/process`、近期未提交 OnlyOffice / Page AI / control-plane 改动。
## 1. 结论
当前上位口径仍一致:
- `tree-first graph kernel` 是长期对象真相层。
- local-first workspace 是默认产品形态,本地 `.md` 是页面正文真相。
- Rust `mnote-web` 是 3000 主执行面。
- `leptos-tiptap` 是默认 Markdown 前端编辑器。
- Page Aggregate 是 Rust-first 投影收口方向,但仍需要 compat 瘦身。
- Page AI 的 local-first 主路径是“授权文件引用 + allowed roots + Hermes/Reasonix 原生 patch/diff + watcher/BufferStore 同步”,不是继续扩 `mnote.doc.markdown_edit``/api/page-ai/block-edit-workflow`
本轮发现的主要问题不是架构方向动摇,而是 `design/process` 状态与真实完成态、索引入口和 bug 记录之间出现漂移。
## 2. 已更新口径
- `design/README.md` 的当前入口已从不存在的 `design/01-05-current-priority-overview.md` 改为 `CURRENT_ARCHITECTURE.md``1-8`
- `1-8` 已补充 2026-05-31 口径复核:历史 `01-05` 已迁入 `old/`;已归档的 `3-3` / `3-18` 不再作为 active process 入口。
- 本 review 作为本轮设计治理和 bug 寻找事实源,后续 worker 不应凭旧路径恢复过时优先级。
## 3. design 漂移
已确认的漂移:
- `design/README.md``1-8` 曾引用已不存在的 `design/01-05-current-priority-overview.md`
- `design/03-rust-web/process/3-24-global-content-type-page-width-settings-v1.md` checklist 全部完成并有实施记录;2026-05-31 已迁入 `design/03-rust-web/done/3-24-global-content-type-page-width-settings-v1.md`
- `design/05-editor-mainline/done/5-31-page-settings-sqlite-preference-convergence-v1.md` 已迁入 `done/`;剩余风险另按后续 bug / follow-up 跟踪。
- `design/05-editor-mainline/done/5-32-filetree-lazy-loading-sidex-alignment-v1.md` 已迁入 `done/`tree live cache 后续入口改看 `3-23` 与相关 bug。
- `design/05-editor-mainline/process/5-33-navigation-page-route-guard-checklist-v1.md` 顶部落地状态和正文 checklist 状态不一致,需要拆成已完成核心与剩余 follow-up。
- `design/07-ai/done/7-41-page-ai-hermes-reasonix-user-profile-isolation-v2.md` 文件头状态为 `done`2026-06-01 已迁入 `done/`
- `design/10-review/process/17-sidex-mnote-workbench-gap-execution-checklist-v1.md` 文件头状态为 `done`2026-05-31 已迁入 `design/10-review/done/17-sidex-mnote-workbench-gap-execution-checklist-v1.md`
- `design/10-review/done/11-current-full-architecture-review-v1.md` 中“`mnote.doc.markdown_edit` 已是简单正文编辑主路径”的历史判断已被 `7-18` 和当前 AGENTS/ARCHITECTURE 覆盖;只能作为当时 review 快照,不应指导 local-first 新实现。
## 4. 新增 bug 条目
本轮已写入:
- `bugs/10-review/done/10-18-design-process-status-and-stale-entry-v1.md`
- `bugs/07-ai/process/7-45-onlyoffice-live-bridge-session-isolation-v1.md`
- `bugs/07-ai/process/7-46-onlyoffice-live-tool-resource-scope-bypass-v1.md`
- `bugs/05-editor-mainline/process/5-40-onlyoffice-bridge-plugin-noise-regression-v1.md`
- `bugs/07-ai/process/7-47-local-first-ai-markdown-edit-mouth-drift-v1.md`
- `bugs/10-review/done/10-19-agents-md-broken-design-references-v1.md`
- `bugs/03-rust-web/done/3-26-sidebar-dev-hot-reload-setinterval-v1.md`
- `bugs/10-review/done/10-20-testing-reference-missing-new-smokes-v1.md`
其中 `10-18``10-19``10-20` 已完成治理收口并迁入 `done/`OnlyOffice / Page AI / Sidebar 等运行时缺陷仍按各自 bug 条目继续跟踪验收。
## 5. 下一步建议
P0
- [x] 修复 OnlyOffice live bridge session identity 与 resource scope 两个风险,优先补 Rust 单测,随后做 clean browser 多 tab smoke。
- [x] 继续补 OnlyOffice 真实浏览器集成证据:真实 Office iframe 双 tab 不串台,以及 Page AI target=A 时不能读写 Office resource=B。
- [x] `AGENTS.md` 断裂引用、Sidebar dev hot reload guard、`5-31` / `5-32` / `7-41` 归档已经收口。
- [~] `5-33` breadcrumb / fetch guard follow-up 仍保留在 `process/`
- [x] 更新 `10-review/done/11` 或追加覆盖说明,防止 worker 继续把 `mnote.doc.markdown_edit` 当 local-first 主写入口。
P1
- [~] 继续推进 `7-18` Agent Target Resolvertarget chip/picker、dirty guard、allowed roots/files 与写后 diff 回收。
- [x] 推进 `WorkspacePath/ObjectIdentity + BufferStore + Page Aggregate compat 瘦身`,让 FileTree、Open Editors、resource tab 与 AI target resolver 消费同一身份与 buffer 状态。
- [x] 保留 `3-23``7-38` 作为 runtime owner follow-up,不重新打开 raw string 大拆分议题。
P2
- [x] `7-42` mindmap 第二批:结构化 `apply_ops`、resource tab contextRefs、revision conflict。
- [~] `3-25` MinerU OCR:后端真实 MinerU runtime 已补;任务状态事件、全局任务栏和 UI 入口仍待做。
- [x] `7-43` OnlyOffice Plugin BridgeP0/P1 安全主路径与 P2 recipe 矩阵已收口,后续新增 recipe 按矩阵逐项验证。
- [x] `7-44` ChatOnly Doubao bindingMNote 侧 binding 与 Doubao provider 单测已完成。
- [ ] `7-49` ChatOnly OpenClaw providerDeepSeek / Gemini provider 级 delete helper 与单测仍在外部仓库阻塞,不能在 MNote 内归档。
本轮已将 `task503``task504``task512``task513``task514``task515``task516` 补入 `scripts/TESTING_REFERENCE.md`;对应 smoke 已实跑,运行结果由 `bugs/10-review/done/10-20-*` 记录。
## 6. 验证记录
原始 review 是设计/bug 审查和文档更新,未修业务代码,未跑浏览器 smoke。2026-06-01 后续收口已补跑 `task514``task517`,并登记 `task517` 到 smoke referenceOnlyOffice 真实 iframe / DocumentServer 验收仍未完成。
已执行只读检查:
- `git status --short`
- `find design -maxdepth 3 -type f`
- `find bugs -maxdepth 3 -type f`
- `codegraph status .`
- 多组 `rg` / `sed` / `nl` 证据核对
已使用:
- 2 个只读 subagent:架构口径审查、bug 候选审查。
- 2 个 Reasonix 只读 workerdesign governance audit、bug hunt candidate audit。
注意:当前工作区有大量未提交改动,且 CodeGraph 显示新增文件 pending。本轮只对本轮新增/修改的文档负责,不回滚、不移动用户已有改动。
+6 -5
View File
@@ -23,12 +23,13 @@
## 当前优先级入口
- `01-05` 当前有效主线与优先级,请先看:
`/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
- Runtime 模块级拆分完成态,请看:
`/mnt/Data1T/mnote/design/10-review/done/16-mnote-web-runtime-module-maintainability-checklist-v1.md`
- 当前有效架构口径,请先看:
`/mnt/Data1T/mnote/CURRENT_ARCHITECTURE.md`
- MVP 后阶段剩余 `process/` 的执行顺序,请看:
`/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-8-mvp-post-process-execution-order-v1.md`
- Runtime 模块级拆分完成态,请看:
`/mnt/Data1T/mnote/design/10-review/done/16-mnote-web-runtime-module-maintainability-checklist-v1.md`
- 历史 `01-05-current-priority-overview` 已迁入 `design/old/`,只能作为归档快照,不再作为当前入口。
## 主线顺序
@@ -82,7 +83,7 @@
- `90-reference/`
- 跨域生态参考资料,不参与 `[done]/[process]/[draft]/[reference]/[recycle]` 状态判断
- 引用时只能作为生态资料或背景材料,不能覆盖 `ARCHITECTURE.md``AGENTS.md``01-05` 当前优先级或对应主线 `process/` / `done/` 设计稿
- 引用时只能作为生态资料或背景材料,不能覆盖 `CURRENT_ARCHITECTURE.md``ARCHITECTURE.md``AGENTS.md``1-8` 当前执行顺序或对应主线 `process/` / `done/` 设计稿
- `old/`
- 已废弃或被替代的历史稿件,标题统一标记 `[recycle]`
- 每个大类继续按 `process/``done/` 分层