chore: land tree view-state, vault, Pi module split, and repo hygiene
Persist PageTree expand state via control-plane view-state and align chevron/DOM with restored expansion; keep Sidex-style shallow page-tree scan and drop the unused recursive scanner that only added cargo noise. Add password vault workbench routes/runtime/skill/CLI, split page_ai_pi into a module package, and retire Hermes/ACP/OpenHub recycle + root harness evidence from the index while gitignoring recycle and local diag dumps. Archive superseded design/bugs docs under old/, point architecture at ARCHITECTURE.md, and refresh smokes for Pi S1–S7, vault, and editor regressions so the working tree can stay clean.
This commit is contained in:
@@ -0,0 +1,430 @@
|
||||
# [recycle] 3-25 Local Folder MinerU OCR Sidecar Checklist v1
|
||||
|
||||
> 创建时间:2026-05-29
|
||||
> 状态:`process`
|
||||
> Owner:03-rust-web / local-folder resource runtime
|
||||
>
|
||||
> 2026-06-01 复核:后端 mock 闭环已落地 `local_ocr.rs` 与 OCR route;真实 MinerU HTTP client 已补本地 HTTP mock 成功路径测试;resource tab 图片/PDF OCR 工具条、附件/File Tree 资源菜单入口和 `task526` UI smoke 已补。active job store、`local_ocr.job.updated` realtime event、全局任务抽屉和 AI 资源上下文读取 OCR sidecar 尚未落地。本文继续保持 `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
|
||||
[OCR:photo.png](./Page.ocr/photo.png.ocr.md)
|
||||
```
|
||||
|
||||
可选模式 `inlineSection` 插入正文段落:
|
||||
|
||||
```markdown
|
||||
### OCR:photo.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 模式)
|
||||
- [x] 后端:新增 OCR active job store,记录 queued/uploading/mineru_processing/downloading/writing_sidecar/done/failed/interrupted/stale。
|
||||
- [x] 后端: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。
|
||||
- [x] UI:在附件右键菜单、File Tree 资源菜单和资源 tab 增加 OCR 识别入口。
|
||||
- [x] UI:展示 OCR 状态、stale、失败和重新识别动作。
|
||||
- [x] UI:新增全局 OCR 任务栏 / 任务抽屉,显示任务阶段、所属页面、打开 OCR、重试。
|
||||
- [x] UI/API:增加“插入 OCR 链接到正文”动作。(可选“插入 OCR 正文段落”不作为本阶段验收项,后续需要时另拆增强稿)
|
||||
- [x] AI:让 image/PDF 资源上下文优先读取已有 OCR sidecar。
|
||||
- [x] 测试:Rust 单测覆盖路径规划、frontmatter、索引读写、stale、越界拒绝。
|
||||
- [x] 测试:Rust route 测试覆盖无 token、真实 MinerU HTTP mock 成功、模拟失败和脱敏错误。
|
||||
- [x] 测试:local search 测试覆盖 OCR 命中返回 owner page,OCR sidecar 不作为普通页面。
|
||||
- [x] Smoke:浏览器覆盖图片/PDF 手动 OCR、全局任务栏状态、打开 OCR resource tab、搜索 OCR 命中、插入 OCR 链接。
|
||||
- [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`
|
||||
|
||||
2026-06-01 任务链与 AI context 收口:
|
||||
|
||||
- 后端在 `AppState` 增加 OCR active job store 和 `local_ocr_job_tx` broadcast;`POST /api/local-folder/ocr/jobs` 会在 queued / uploading / mineru_processing / downloading / writing_sidecar / done / failed 阶段更新 `.mnote/ocr-index.json`,并通过 `/api/local-folder/events` 发出 `local_ocr.job.updated`。
|
||||
- 资源 tab 增加全局 OCR 任务 dock / drawer;任务抽屉从 jobs API 恢复历史状态,并通过 `local_ocr.job.updated` 事件更新,支持打开 OCR 和失败/stale 重试。
|
||||
- Page AI active image/PDF resource target 会在发 run 前读取已有 OCR sidecar,把 `mnote.local_ocr_context.v1` 放入 active_editor contextRef、targetPackage、currentFile 和 primary target。
|
||||
- 已通过:
|
||||
- `node --check rust/crates/mnote-web/browser/document-resource-tab-runtime.js`
|
||||
- `node --check rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js`
|
||||
- `node --check rust/crates/mnote-web/browser/sidebar-page-ai-target-runtime.js`
|
||||
- `node --check scripts/task526-local-folder-ocr-api-smoke.js`
|
||||
- `node --check scripts/task520-page-ai-raw-resource-target-smoke.js`
|
||||
- `cargo fmt --check --all`
|
||||
- `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`
|
||||
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3000 node scripts/task526-local-folder-ocr-api-smoke.js`
|
||||
- `TASK520_OCR_CONTEXT=1 MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_WEB_SMOKE_BASE_URL=http://127.0.0.1:3000 node scripts/task520-page-ai-raw-resource-target-smoke.js`
|
||||
@@ -0,0 +1,452 @@
|
||||
# [recycle] 3-7 [done] Rust Web 3000 Wolai UI Parity Implementation Plan v1
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.
|
||||
|
||||
**Goal:** 让 `mnote-web` 持有的 `3000` 公共入口恢复到可用的 Wolai 工作区产品壳,并继续向 `/mnt/Data1T/mnote/design/design/html/image.png` 的目标 UI 收敛。
|
||||
|
||||
**Architecture:** 保持 `3000` 的 owner 是 `mnote-web`,不把 Next App Router 恢复为主入口。Rust Web 输出 workspace/document server-first shell、Page Aggregate snapshot、Sidebar projection 与 topbar/floating action 的稳定 HTML 合同;React/Next 仅保留交互 island、legacy 对照链和显式 debug 边界。
|
||||
|
||||
**Tech Stack:** Rust workspace、`mnote-web`、`axum`、Leptos SSR、Page Aggregate、Convex substrate、Rust tree projection、Playwright/Node smoke、Cargo tests、Vitest。
|
||||
|
||||
---
|
||||
|
||||
## 1. 当前事实基线
|
||||
|
||||
本计划基于 2026-04-29 本机检查结果建立:
|
||||
|
||||
- `127.0.0.1:3000` 当前由 `target/debug/mnote-web` 监听,`GET /` 返回 `x-mnote-web-owner: mnote-web`。
|
||||
- `127.0.0.1:3100` 当前由 `node scripts/dev-server.js -p 3100` 监听,根路径返回 `307 /auth`,仍可作为 legacy Next 对照链。
|
||||
- 当前 `3000` 截图 `/mnt/Data1T/mnote/tmp/image copy 30.png` 是极简 MNOTE 首页,只有“首页 / 搜索 / 文档”和欢迎文案。
|
||||
- 当前 legacy/Next 对照截图 `/mnt/Data1T/mnote/tmp/image copy 31.png` 已有 workspace/document 左栏、页面树和文档内容,但仍不是目标 UI。
|
||||
- 目标 UI 参考是 `/mnt/Data1T/mnote/design/design/html/image.png` 与 `/mnt/Data1T/mnote/design/design/html/个人.html`,它包含完整 Wolai 桌面壳:账号区、顶栏、快捷图标、星标置顶、我的页面、页面树、底部入口、右下悬浮按钮和居中页面内容。
|
||||
- `harness-tasks.json` 目前只有 `task-001` 到 `task-026`,全部是 `completed`,没有待办任务。
|
||||
- `task-019` 到 `task-026` 已证明主 Web owner cutover,但没有证明产品 UI parity。
|
||||
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task114-rust-web-gateway-entry-smoke.js` 当前失败,原因是脚本仍断言根页包含 `Rust Web gateway` 文案,而当前根页已经是极简 `MNOTE` 首页。
|
||||
- `MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task115-rust-web-document-shell-smoke.js` 当前失败,原因是 `/documents/doc_1?workspaceId=ws_demo` 经过 Convex 查询返回 `未登录`,HTTP 状态为 `502`。
|
||||
- `rust/crates/mnote-web/src/routes/gateway.rs::root_entry` 当前直接渲染 `ssr/pages/home.rs::HomePage`,不会把 `/` 代理到 3100 的 workspace/document 壳。
|
||||
- `rust/crates/mnote-web/src/ssr/pages/layout.rs::PageLayout` 当前只实现极简侧栏,不具备目标 UI 的 workspace shell 合同。
|
||||
|
||||
结论:当前问题不是“3000 owner 还没切到 Rust Web”,而是“owner 已切到 Rust Web 后,产品工作区 UI、真实会话、真实数据壳和视觉/交互验收没有接上”。
|
||||
|
||||
## 2. 边界原则
|
||||
|
||||
- `3000` 保持唯一公开入口,默认 owner 继续是 `mnote-web`。
|
||||
- 不恢复 `3104` 作为公开入口。
|
||||
- 不把 `3100` 恢复成主入口;`3100` 只作为 legacy/reference/debug upstream。
|
||||
- 不在前端重新创造树真相;Sidebar / 页面树继续消费 Rust kernel projection 或 Page Aggregate 中的稳定 projection。
|
||||
- 不把“截图样式补丁”当成完成;必须同时修复当前 3000 的真实会话、真实路由和 smoke 验收。
|
||||
- 不修改或回滚当前未提交改动;执行本计划时按任务最小写集落地。
|
||||
|
||||
## 3. 文件结构
|
||||
|
||||
### Rust Web SSR shell
|
||||
|
||||
- `rust/crates/mnote-web/src/routes/gateway.rs`
|
||||
- 把 `/` 从极简欢迎页改成 workspace entry,负责选择 active workspace / active page / fallback route。
|
||||
- `rust/crates/mnote-web/src/routes/web_shell.rs`
|
||||
- 继续承接 document shell 与 Page Aggregate;新增 workspace shell 所需的共享加载器。
|
||||
- `rust/crates/mnote-web/src/ssr/pages/layout.rs`
|
||||
- 从极简 `PageLayout` 演进为 `WolaiWorkspaceLayout` 或等价组件,输出目标 UI 的稳定 DOM 合同。
|
||||
- `rust/crates/mnote-web/src/ssr/pages/home.rs`
|
||||
- 从营销/欢迎页改为 workspace home/page shell,不再作为默认产品首屏。
|
||||
- `rust/crates/mnote-web/src/ssr/pages/document.rs`
|
||||
- 对齐目标页面内容区:breadcrumb、顶部操作区、页面 icon/title、子页面列表、编辑 island mount。
|
||||
- `rust/crates/mnote-web/src/ssr/styles.rs`
|
||||
- 收口目标 UI 样式变量、sidebar/topbar/content/floating action 样式。
|
||||
|
||||
### Rust Web 数据合同
|
||||
|
||||
- `rust/crates/mnote-web/src/workspace_shell.rs`
|
||||
- 新建 workspace shell projection 聚合器,组合 workspace summary、sidebar dataset、active page、starred/pinned 区域和 bottom entries。
|
||||
- `rust/crates/mnote-web/src/routes/session.rs`
|
||||
- 修复 3000 当前真实请求的 session/auth handoff,避免文档页在登录态/开发态下返回 `未登录` 502。
|
||||
- `rust/crates/mnote-web/src/transport/convex.rs`
|
||||
- 确认 Rust Web 到 Convex 的 cookie/header/token 透传合同;必要时补只读 helper,不绕开用户鉴权。
|
||||
|
||||
### 前端 legacy/island
|
||||
|
||||
- `wolai-frontend/src/lib/rust-web-main-execution-boundary.ts`
|
||||
- 补充 UI parity guard,明确 Next 只能是 legacy/reference/island source。
|
||||
- `wolai-frontend/src/components/sidebar/**`
|
||||
- 只在需要复用现有 projection/DOM 合同时修改,不重新把 Next Sidebar 设为主产品壳。
|
||||
- `wolai-frontend/src/components/editor/**`
|
||||
- 只保留编辑器 island runtime 和必要 mount contract。
|
||||
|
||||
### Smoke / 验收
|
||||
|
||||
- `scripts/task114-rust-web-gateway-entry-smoke.js`
|
||||
- 从“根页包含调试文案”升级为“根页是 Rust-owned Wolai workspace shell”。
|
||||
- `scripts/task115-rust-web-document-shell-smoke.js`
|
||||
- 从 fixture-only 文档壳验证升级为“当前 3000 可打开真实/开发态文档壳且不返回未登录 502”。
|
||||
- `scripts/task118-rust-web-wolai-ui-parity-smoke.js`
|
||||
- 新建目标 UI parity smoke,覆盖 sidebar、topbar、content、floating actions、legacy 禁用组合。
|
||||
- `scripts/task119-rust-web-wolai-visual-regression-smoke.js`
|
||||
- 新建可选 Playwright 截图 smoke,对比 3000、3100 legacy 和 `design/design/html/image.png` 的关键布局指标。
|
||||
|
||||
## 4. 实施任务
|
||||
|
||||
### Task U-0:冻结 UI parity 失败用例
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `scripts/task118-rust-web-wolai-ui-parity-smoke.js`
|
||||
- Modify: `scripts/task114-rust-web-gateway-entry-smoke.js`
|
||||
- Modify: `scripts/task115-rust-web-document-shell-smoke.js`
|
||||
|
||||
- [x] **Step 1: 写 3000 UI parity smoke**
|
||||
|
||||
创建 `scripts/task118-rust-web-wolai-ui-parity-smoke.js`,固定当前必须失败的产品壳断言:
|
||||
|
||||
```js
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert");
|
||||
|
||||
const BASE_URL = (process.env.MNOTE_UI_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
|
||||
const REQUEST_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_TIMEOUT_MS || 20_000);
|
||||
|
||||
async function fetchWithTimeout(path) {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(new Error(`请求超时: ${path}`)), REQUEST_TIMEOUT_MS);
|
||||
try {
|
||||
return await fetch(`${BASE_URL}${path}`, {
|
||||
redirect: "manual",
|
||||
cache: "no-store",
|
||||
signal: controller.signal,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
async function readHtml(path) {
|
||||
const response = await fetchWithTimeout(path);
|
||||
const html = await response.text();
|
||||
assert.equal(response.status, 200, `${path} 状态码应为 200`);
|
||||
assert.equal(response.headers.get("x-mnote-web-owner"), "mnote-web", `${path} 必须由 mnote-web 拥有`);
|
||||
assert.match(response.headers.get("content-type") || "", /text\/html/, `${path} 必须返回 HTML`);
|
||||
return html;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const rootHtml = await readHtml("/");
|
||||
|
||||
assert.match(rootHtml, /data-mnote-shell="workspace"/, "根页必须是 workspace shell,而不是欢迎页");
|
||||
assert.match(rootHtml, /liaibo的个人空间|个人空间|开发用户 的空间/, "侧栏必须显示工作区身份");
|
||||
assert.match(rootHtml, /星标置顶/, "侧栏必须包含星标置顶区域");
|
||||
assert.match(rootHtml, /我的页面/, "侧栏必须包含我的页面区域");
|
||||
assert.match(rootHtml, /垃圾箱/, "侧栏底部必须包含垃圾箱入口");
|
||||
assert.match(rootHtml, /模板中心/, "侧栏底部必须包含模板中心入口");
|
||||
assert.match(rootHtml, /data-testid="wolai-topbar"/, "主内容区必须包含 Wolai 顶栏");
|
||||
assert.match(rootHtml, /data-testid="wolai-floating-ai"/, "页面必须包含右下 AI 悬浮入口");
|
||||
assert.doesNotMatch(rootHtml, /欢迎使用 MNOTE 知识管理平台/, "根页不能停留在极简欢迎页");
|
||||
assert.doesNotMatch(rootHtml, /<a href="\/documents">文档<\/a>/, "根页不能停留在三项导航壳");
|
||||
|
||||
console.log(JSON.stringify({ ok: true, baseUrl: BASE_URL, shell: "workspace", owner: "mnote-web" }, null, 2));
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.stack || error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
```
|
||||
|
||||
- [x] **Step 2: 运行 smoke,确认当前失败**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task118-rust-web-wolai-ui-parity-smoke.js
|
||||
```
|
||||
|
||||
Expected before implementation: FAIL,第一处失败应指向 `data-mnote-shell="workspace"` 缺失或极简欢迎页仍存在。
|
||||
|
||||
- [x] **Step 3: 更新 task114 的根页断言**
|
||||
|
||||
把 `scripts/task114-rust-web-gateway-entry-smoke.js` 中对 `/Rust Web gateway/` 的断言改成:
|
||||
|
||||
```js
|
||||
assert.match(rootText, /data-mnote-shell="workspace"/);
|
||||
assert.doesNotMatch(rootText, /Rust Web gateway|欢迎使用 MNOTE 知识管理平台/);
|
||||
```
|
||||
|
||||
- [x] **Step 4: 更新 task115 的失败信息**
|
||||
|
||||
在 `scripts/task115-rust-web-document-shell-smoke.js` 中保留 `x-mnote-web-owner` / `x-mnote-web-shell` 断言,并在 502 时输出 body,确保“未登录”不被隐藏成普通 shell 失败。
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task115-rust-web-document-shell-smoke.js
|
||||
```
|
||||
|
||||
Expected before implementation: FAIL,错误信息包含 `未登录` 或 `convex_upstream_error`。
|
||||
|
||||
### Task U-1:修复 3000 当前真实 session / document shell 基线
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `rust/crates/mnote-web/src/routes/session.rs`
|
||||
- Modify: `rust/crates/mnote-web/src/transport/convex.rs`
|
||||
- Modify: `rust/crates/mnote-web/src/routes/web_shell.rs`
|
||||
- Test: `rust/crates/mnote-web/src/routes/web_shell.rs`
|
||||
- Test: `scripts/task115-rust-web-document-shell-smoke.js`
|
||||
|
||||
- [x] **Step 1: 写 Rust 测试覆盖 session handoff**
|
||||
|
||||
在 `web_shell.rs` 的测试模块中增加测试,构造带 `mnote_web_convex_token` cookie 的请求,断言 Rust Web 会把 token 传给 Convex transport plan,不会落入未登录错误路径。
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/rust
|
||||
cargo test -p mnote-web document_shell session
|
||||
```
|
||||
|
||||
Expected before implementation: FAIL,失败点是缺少 cookie/header 透传或测试 helper 不存在。
|
||||
|
||||
- [x] **Step 2: 修复 Rust Web 到 Convex 的认证透传**
|
||||
|
||||
保持 `DEV_USER_ID` 只作为开发辅助,不绕过真实用户鉴权。实际请求优先级固定为:浏览器 cookie / session handoff token > `mnote_web_convex_token` > 开发态显式 DEV_USER fallback。
|
||||
|
||||
- [x] **Step 3: 验证当前 3000 文档页不再 502**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task115-rust-web-document-shell-smoke.js
|
||||
```
|
||||
|
||||
Expected after implementation: PASS,输出 JSON 中 `owner` 为 `mnote-web`,`shell` 为 `document`。
|
||||
|
||||
### Task U-2:建立 workspace shell projection 聚合器
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `rust/crates/mnote-web/src/workspace_shell.rs`
|
||||
- Modify: `rust/crates/mnote-web/src/lib.rs`
|
||||
- Modify: `rust/crates/mnote-web/src/routes/gateway.rs`
|
||||
- Modify: `rust/crates/mnote-web/src/routes/web_shell.rs`
|
||||
- Test: `rust/crates/mnote-web/src/workspace_shell.rs`
|
||||
|
||||
- [x] **Step 1: 定义 workspace shell 最小数据结构**
|
||||
|
||||
新增结构:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WorkspaceShellProjection {
|
||||
pub schema: String,
|
||||
pub workspace_id: String,
|
||||
pub workspace_name: String,
|
||||
pub active_page_id: Option<String>,
|
||||
pub active_page_title: Option<String>,
|
||||
pub starred_items: Vec<WorkspaceShellItem>,
|
||||
pub my_page_items: Vec<WorkspaceShellItem>,
|
||||
pub bottom_entries: Vec<WorkspaceShellEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WorkspaceShellItem {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub icon: Option<String>,
|
||||
pub href: String,
|
||||
pub depth: u32,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WorkspaceShellEntry {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
pub href: String,
|
||||
pub icon: String,
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **Step 2: 写聚合器测试**
|
||||
|
||||
测试输入使用 `sidebar.dataset.list` 形状,断言输出包含:`星标置顶` 数据、`我的页面` 数据、`垃圾箱`、`模板中心`,且 active page 能从请求参数或 projection 中确定。
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/rust
|
||||
cargo test -p mnote-web workspace_shell
|
||||
```
|
||||
|
||||
Expected before implementation: FAIL,模块或类型不存在。
|
||||
|
||||
- [x] **Step 3: 接入 gateway root**
|
||||
|
||||
`GET /` 不再直接输出 `HomePage` 欢迎页,而是加载 `WorkspaceShellProjection` 后输出 workspace shell。无 active page 时,选择第一个可见 my page;没有页面时输出空 workspace shell,不输出营销欢迎页。
|
||||
|
||||
### Task U-3:把 `PageLayout` 替换为 Wolai workspace shell 合同
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `rust/crates/mnote-web/src/ssr/pages/layout.rs`
|
||||
- Modify: `rust/crates/mnote-web/src/ssr/pages/home.rs`
|
||||
- Modify: `rust/crates/mnote-web/src/ssr/pages/document.rs`
|
||||
- Modify: `rust/crates/mnote-web/src/ssr/styles.rs`
|
||||
- Test: `rust/crates/mnote-web/src/ssr/pages/layout.rs`
|
||||
|
||||
- [x] **Step 1: 写 SSR DOM 合同测试**
|
||||
|
||||
测试 `WolaiWorkspaceLayout` 渲染结果必须包含:
|
||||
|
||||
```text
|
||||
data-mnote-shell="workspace"
|
||||
data-testid="wolai-sidebar"
|
||||
data-testid="wolai-topbar"
|
||||
星标置顶
|
||||
我的页面
|
||||
垃圾箱
|
||||
模板中心
|
||||
data-testid="wolai-floating-ai"
|
||||
data-testid="wolai-floating-help"
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/rust
|
||||
cargo test -p mnote-web wolai_workspace_layout
|
||||
```
|
||||
|
||||
Expected before implementation: FAIL,当前只有 `mnote-shell` / `mnote-sidebar-nav`。
|
||||
|
||||
- [x] **Step 2: 实现 workspace shell DOM**
|
||||
|
||||
把极简三项导航替换为目标 UI 合同:顶部账号区、快捷操作区、星标置顶、我的页面、底部入口、主内容 topbar 和右下浮动入口。
|
||||
|
||||
- [x] **Step 3: 样式对齐目标 UI 的关键布局指标**
|
||||
|
||||
关键指标:左栏宽度约 `280px` 到 `360px` 区间;侧栏背景为浅灰;选中行浅红背景;主内容首屏留白大;页面中心内容宽度约 `720px`;顶栏高度约 `44px`;右下两个圆形浮动按钮不遮挡正文。
|
||||
|
||||
### Task U-4:恢复 3000 根页到 workspace/document 产品首屏
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `rust/crates/mnote-web/src/routes/gateway.rs`
|
||||
- Modify: `rust/crates/mnote-web/src/ssr/pages/home.rs`
|
||||
- Modify: `rust/crates/mnote-web/src/routes/web_shell.rs`
|
||||
- Test: `scripts/task118-rust-web-wolai-ui-parity-smoke.js`
|
||||
|
||||
- [x] **Step 1: 根路径选择 active page**
|
||||
|
||||
选择顺序固定为:URL `pageId` 参数 > session 最近页面 > projection active page > 第一个 `my_page_items` > 空 workspace。
|
||||
|
||||
- [x] **Step 2: 根路径输出 workspace shell**
|
||||
|
||||
`GET /` 的 body 必须包含 `data-mnote-shell="workspace"`,不再包含 `欢迎使用 MNOTE 知识管理平台`。
|
||||
|
||||
- [x] **Step 3: 验证 3000 smoke**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task118-rust-web-wolai-ui-parity-smoke.js
|
||||
```
|
||||
|
||||
Expected after implementation: PASS。
|
||||
|
||||
### Task U-5:文档页内容区对齐 legacy 3100 与目标 UI
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `rust/crates/mnote-web/src/ssr/pages/document.rs`
|
||||
- Modify: `rust/crates/mnote-web/src/routes/web_shell.rs`
|
||||
- Modify: `rust/crates/mnote-web/src/ssr/styles.rs`
|
||||
- Test: `scripts/task115-rust-web-document-shell-smoke.js`
|
||||
|
||||
- [x] **Step 1: 文档 shell 输出 topbar / breadcrumb / action slots**
|
||||
|
||||
文档页必须输出 `data-testid="wolai-topbar"`、breadcrumb 当前页标题、收藏/更多/AI action slot。
|
||||
|
||||
- [x] **Step 2: 文档 shell 继续嵌入 Page Aggregate snapshot**
|
||||
|
||||
保留现有:`id="__MNOTE_PAGE_AGGREGATE__"`、`data-page-aggregate-snapshot="mnote.page_aggregate.v1"`、`data-editor-host="leptos_tiptap_island"`。
|
||||
|
||||
- [x] **Step 3: 文档内容区不退回 BlockNote-first**
|
||||
|
||||
默认编辑 host 仍是 `leptos_tiptap_island`;BlockNote 只作为 fallback/debug。
|
||||
|
||||
### Task U-6:补视觉回归 smoke
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `scripts/task119-rust-web-wolai-visual-regression-smoke.js`
|
||||
- Modify: `package.json` 或既有 smoke 调度脚本,仅在仓库已有集中 smoke 入口时接入。
|
||||
|
||||
- [x] **Step 1: 写 Playwright 截图与布局指标检查**
|
||||
|
||||
检查项固定为:左栏存在且宽度稳定、topbar 存在、主内容不被侧栏遮挡、右下浮动按钮不遮挡正文、根页没有极简欢迎页。
|
||||
|
||||
- [x] **Step 2: 运行可视 smoke**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task119-rust-web-wolai-visual-regression-smoke.js
|
||||
```
|
||||
|
||||
Expected after implementation: PASS,并输出截图路径到 `/mnt/Data1T/mnote/test-results/`。
|
||||
|
||||
### Task U-7:文档和 harness 收口
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `design/03-rust-web/done/3-8-rust-web-3000-wolai-ui-parity-checklist-v1.md`
|
||||
- Modify: `harness-tasks.json` only when explicitly scheduling execution.
|
||||
- Modify: `harness-progress.txt` only when execution starts.
|
||||
|
||||
- [x] **Step 1: 更新 checklist 状态**
|
||||
|
||||
每完成一个 U 阶段,只更新对应 checklist 项,不把 owner cutover 结果重复写成 UI parity 完成。
|
||||
|
||||
- [x] **Step 2: 如需进入 harness,追加 task-027 起的新任务**
|
||||
|
||||
建议映射:
|
||||
|
||||
```text
|
||||
task-027 -> U-0 UI parity smoke
|
||||
task-028 -> U-1 session/document shell baseline
|
||||
task-029 -> U-2 workspace shell projection
|
||||
task-030 -> U-3 Wolai workspace layout
|
||||
task-031 -> U-4 root workspace entry
|
||||
task-032 -> U-5 document UI parity
|
||||
task-033 -> U-6 visual regression smoke
|
||||
```
|
||||
|
||||
只有在明确开始执行时修改 `harness-tasks.json`,避免把分析计划误写成已排队任务。
|
||||
|
||||
## 5. 完成判定
|
||||
|
||||
全部完成时必须同时成立:
|
||||
|
||||
- `GET http://127.0.0.1:3000/` 返回 `x-mnote-web-owner: mnote-web`。
|
||||
- 根页 body 标记为 `data-mnote-shell="workspace"`。
|
||||
- 根页不再出现 `欢迎使用 MNOTE 知识管理平台` 这类极简欢迎页文案。
|
||||
- 根页包含目标 UI 的结构区:账号区、快捷入口、星标置顶、我的页面、底部入口、topbar、右下浮动按钮。
|
||||
- `/documents/:id` 不因当前登录态或开发态缺失而返回 `未登录` 502。
|
||||
- 文档页仍消费 Page Aggregate snapshot,并保留 `leptos_tiptap_island` 默认编辑 host。
|
||||
- `MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT=0` 或等价 skip legacy 场景下,核心 workspace/document shell 仍可打开。
|
||||
- `3100` 仅作为 legacy/reference/debug,不是主入口 owner。
|
||||
- 新增 smoke 能在当前 3000 上证明产品壳,而不只是证明 owner header。
|
||||
|
||||
## 6. 自检
|
||||
|
||||
- Spec coverage:覆盖当前 3000 截图、3100 对照截图、目标 UI 参考、git/harness 现状、session 502、root shell 极简化、owner cutover 与 UI parity 的口径拆分。
|
||||
- Placeholder scan:本计划不使用 `TBD`、`TODO`、`implement later` 作为执行步骤。
|
||||
- Type consistency:统一使用 `WorkspaceShellProjection`、`WolaiWorkspaceLayout`、`data-mnote-shell="workspace"`、`mnote-web`、`next-app-router legacy compat`。
|
||||
@@ -0,0 +1,222 @@
|
||||
# [recycle] 3-8 [done] Rust Web 3000 Wolai UI Parity Checklist v1
|
||||
|
||||
> 更新时间:2026-04-29
|
||||
>
|
||||
> 对应计划:
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-7-rust-web-3000-wolai-ui-parity-plan-v1.md`
|
||||
>
|
||||
> 参考输入:
|
||||
> - 当前 3000 截图:`/mnt/Data1T/mnote/tmp/image copy 30.png`
|
||||
> - 当前 3100 legacy/Next 对照截图:`/mnt/Data1T/mnote/tmp/image copy 31.png`
|
||||
> - 目标 UI:`/mnt/Data1T/mnote/design/design/html/image.png`
|
||||
> - 目标 HTML:`/mnt/Data1T/mnote/design/design/html/个人.html`
|
||||
|
||||
## 1. 当前结论
|
||||
|
||||
当前已经完成的是:
|
||||
|
||||
- [x] `3000` 默认由 `mnote-web` 监听。
|
||||
- [x] `GET /` 返回 `x-mnote-web-owner: mnote-web`。
|
||||
- [x] `3100` 已降为 legacy/Next 对照链,不是公开主入口 owner。
|
||||
- [x] `harness-tasks.json` 中 `task-001` 到 `task-026` 全部为 `completed`。
|
||||
|
||||
当前没有完成的是:
|
||||
|
||||
- [x] `3000` 首屏恢复到 3100 的 workspace/document 产品壳。
|
||||
- [x] `3000` 首屏达到目标 Wolai UI 的结构与视觉效果。
|
||||
- [x] `3000` 文档页在当前真实运行态下稳定打开,不返回 `未登录` 502。
|
||||
- [x] smoke 验收从“owner header 通过”升级为“产品壳 UI parity 通过”。
|
||||
|
||||
一句话判断:
|
||||
|
||||
> **Rust Web owner cutover 已完成;本轮已把 3000 恢复为 Rust Web 持有的 Wolai workspace/document 产品壳,并用 task114/task115/task118/task119 固化验收。**
|
||||
|
||||
## 2. 不可误判为完成的情况
|
||||
|
||||
- [x] 只看到 `x-mnote-web-owner: mnote-web`,不算 UI parity 完成。
|
||||
- [x] 只看到极简 `MNOTE` 欢迎页,不算工作区 shell 完成。
|
||||
- [x] 只在 `MNOTE_WEB_ALLOW_DEV_FIXTURES=1` 下通过,不算当前真实 3000 完成。
|
||||
- [x] 只把 3000 proxy 回 3100,不算 Rust Web 产品壳完成。
|
||||
- [x] 只恢复 3100 legacy UI,不算 Rust Web 3000 UI 完成。
|
||||
- [x] 只补 CSS 像截图,但 session/document shell 仍 502,不算完成。
|
||||
|
||||
## 3. Phase U-0:冻结 UI parity smoke
|
||||
|
||||
目标:先让当前差距可自动失败,避免继续靠人工看截图判断。
|
||||
|
||||
- [x] 新增 `scripts/task118-rust-web-wolai-ui-parity-smoke.js`。
|
||||
- [x] smoke 断言 `GET /` 是 `data-mnote-shell="workspace"`。
|
||||
- [x] smoke 断言根页包含账号/工作区身份。
|
||||
- [x] smoke 断言根页包含 `星标置顶`。
|
||||
- [x] smoke 断言根页包含 `我的页面`。
|
||||
- [x] smoke 断言根页包含 `垃圾箱`。
|
||||
- [x] smoke 断言根页包含 `模板中心`。
|
||||
- [x] smoke 断言根页包含 `data-testid="wolai-topbar"`。
|
||||
- [x] smoke 断言根页包含 `data-testid="wolai-floating-ai"`。
|
||||
- [x] smoke 负向断言根页不再包含 `欢迎使用 MNOTE 知识管理平台`。
|
||||
- [x] `task114-rust-web-gateway-entry-smoke.js` 不再检查 `Rust Web gateway` 调试文案。
|
||||
- [x] `task115-rust-web-document-shell-smoke.js` 在 502 时输出 body,能看见 `未登录` 根因。
|
||||
|
||||
验证命令:
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task118-rust-web-wolai-ui-parity-smoke.js
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task114-rust-web-gateway-entry-smoke.js
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task115-rust-web-document-shell-smoke.js
|
||||
```
|
||||
|
||||
## 4. Phase U-1:修复真实 session / document shell 基线
|
||||
|
||||
目标:当前 3000 文档页不能继续因为 Rust Web 到 Convex 的 session/handoff 问题返回 `未登录` 502。
|
||||
|
||||
- [x] 确认当前 3000 进程环境和浏览器请求中的 auth/session 来源。
|
||||
- [x] 固定 Rust Web session handoff 优先级:浏览器 cookie / session handoff token > `mnote_web_convex_token` > 显式开发态 fallback。
|
||||
- [x] `transport/convex.rs` 能把必要 cookie/header/token 传到 Convex。
|
||||
- [x] `/documents/:id` 在当前登录态或开发态下不返回 `convex_upstream_error: 未登录`。
|
||||
- [x] 文档页仍返回 `x-mnote-web-owner: mnote-web`。
|
||||
- [x] 文档页仍返回 `x-mnote-web-shell: document`。
|
||||
- [x] 文档页仍嵌入 `__MNOTE_PAGE_AGGREGATE__`。
|
||||
- [x] 文档页仍标记默认 `data-editor-host="leptos_tiptap_island"`。
|
||||
|
||||
验证命令:
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/rust
|
||||
cargo test -p mnote-web document_shell session
|
||||
cd /mnt/Data1T/mnote
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task115-rust-web-document-shell-smoke.js
|
||||
```
|
||||
|
||||
## 5. Phase U-2:建立 workspace shell projection
|
||||
|
||||
目标:Rust Web 根页不再手写欢迎页,而是消费稳定 workspace/sidebar/page projection。
|
||||
|
||||
- [x] 新增 `WorkspaceShellProjection` 或等价 Rust 类型。
|
||||
- [x] projection 包含 `workspaceId`。
|
||||
- [x] projection 包含 `workspaceName`。
|
||||
- [x] projection 包含 `activePageId` / `activePageTitle`。
|
||||
- [x] projection 包含 `starredItems`。
|
||||
- [x] projection 包含 `myPageItems`。
|
||||
- [x] projection 包含 `bottomEntries`,至少覆盖 `垃圾箱` 与 `模板中心`。
|
||||
- [x] projection 从 `sidebar.dataset.list` 或 Rust kernel sidebar projection 构建,不在 UI 层另造树真相。
|
||||
- [x] 空数据时输出空 workspace shell,不回到极简营销欢迎页。
|
||||
|
||||
验证命令:
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/rust
|
||||
cargo test -p mnote-web workspace_shell
|
||||
```
|
||||
|
||||
## 6. Phase U-3:替换极简 PageLayout 为 Wolai workspace shell
|
||||
|
||||
目标:`mnote-web` SSR 输出与目标 UI 同级的产品壳结构。
|
||||
|
||||
- [x] SSR layout 根节点标记 `data-mnote-shell="workspace"`。
|
||||
- [x] 左侧栏标记 `data-testid="wolai-sidebar"`。
|
||||
- [x] 左侧栏顶部包含头像/首字母块与工作区名称。
|
||||
- [x] 左侧栏快捷操作区包含搜索、图谱/关系、快捷动作、收纳/文件、更多等 slot。
|
||||
- [x] 左侧栏包含 `星标置顶` section。
|
||||
- [x] 左侧栏包含 `我的页面` section。
|
||||
- [x] 左侧栏 page row 支持 active/selected 视觉状态。
|
||||
- [x] 左侧栏底部包含 `垃圾箱` 与 `模板中心`。
|
||||
- [x] 主区顶部包含 `data-testid="wolai-topbar"`。
|
||||
- [x] 主区右上包含收藏、演示/评论/关系/成员/历史/更多等 action slot。
|
||||
- [x] 右下包含 `data-testid="wolai-floating-ai"`。
|
||||
- [x] 右下包含 `data-testid="wolai-floating-help"`。
|
||||
- [x] CSS 左栏宽度、浅灰背景、选中行浅红背景、主内容居中宽度与目标图接近。
|
||||
|
||||
验证命令:
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/rust
|
||||
cargo test -p mnote-web wolai_workspace_layout
|
||||
```
|
||||
|
||||
## 7. Phase U-4:恢复 3000 根路径产品首屏
|
||||
|
||||
目标:`GET /` 是真实 workspace/document 入口,而不是极简欢迎页。
|
||||
|
||||
- [x] `gateway.rs::root_entry` 不再直接渲染 `HomePage` 欢迎页。
|
||||
- [x] 根路径选择 active page 的顺序固定:URL `pageId` > session 最近页面 > projection active page > 第一个我的页面 > 空 workspace。
|
||||
- [x] 根路径返回 HTML 包含 `data-mnote-shell="workspace"`。
|
||||
- [x] 根路径返回 HTML 不包含 `欢迎使用 MNOTE 知识管理平台`。
|
||||
- [x] 根路径返回 HTML 不包含旧三项导航壳 `首页 / 搜索 / 文档` 作为主 UI。
|
||||
- [x] `MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT=0` 时根路径仍可打开核心 shell。
|
||||
|
||||
验证命令:
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task118-rust-web-wolai-ui-parity-smoke.js
|
||||
```
|
||||
|
||||
## 8. Phase U-5:文档页内容区对齐
|
||||
|
||||
目标:`/documents/:id` 恢复 workspace/document 产品壳,同时继续保持 Page Aggregate 与 `leptos-tiptap` 主链。
|
||||
|
||||
- [x] 文档页包含 workspace sidebar。
|
||||
- [x] 文档页包含 topbar / breadcrumb。
|
||||
- [x] 文档页包含收藏/更多/AI action slot。
|
||||
- [x] 文档页标题来自 Page Aggregate。
|
||||
- [x] 文档页正文 mount 保持 `leptos_tiptap_island`。
|
||||
- [x] 文档页不重新把 BlockNote 设为默认主编辑器。
|
||||
- [x] 文档页在真实 3000 下通过 smoke,不返回 502。
|
||||
|
||||
验证命令:
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote/rust
|
||||
cargo test -p mnote-web document_shell page_aggregate
|
||||
cd /mnt/Data1T/mnote
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task115-rust-web-document-shell-smoke.js
|
||||
```
|
||||
|
||||
## 9. Phase U-6:补视觉回归 smoke
|
||||
|
||||
目标:用浏览器自动化把“看起来像目标 UI”变成可回归指标。
|
||||
|
||||
- [x] 新增 `scripts/task119-rust-web-wolai-visual-regression-smoke.js`。
|
||||
- [x] 检查左栏存在且宽度稳定。
|
||||
- [x] 检查 topbar 存在且不遮挡正文。
|
||||
- [x] 检查主内容区未被侧栏覆盖。
|
||||
- [x] 检查右下浮动按钮存在且不遮挡正文。
|
||||
- [x] 检查根页没有极简欢迎页。
|
||||
- [x] 输出截图到 `/mnt/Data1T/mnote/test-results/`。
|
||||
|
||||
验证命令:
|
||||
|
||||
```bash
|
||||
cd /mnt/Data1T/mnote
|
||||
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task119-rust-web-wolai-visual-regression-smoke.js
|
||||
```
|
||||
|
||||
## 10. Harness 映射建议
|
||||
|
||||
当前 `harness-tasks.json` 中 `task-001` 到 `task-026` 已全部 completed。若进入执行,建议追加新任务,不修改旧任务状态:
|
||||
|
||||
| 新任务 | Phase | 标题 | 优先级 |
|
||||
| --- | --- | --- | --- |
|
||||
| `task-027` | U-0 | 冻结 Rust Web 3000 Wolai UI parity smoke | P0 |
|
||||
| `task-028` | U-1 | 修复 Rust Web session/document shell 当前 502 基线 | P0 |
|
||||
| `task-029` | U-2 | 建立 workspace shell projection 聚合器 | P0 |
|
||||
| `task-030` | U-3 | 替换极简 PageLayout 为 Wolai workspace shell | P0 |
|
||||
| `task-031` | U-4 | 恢复 3000 根路径 workspace/document 产品首屏 | P0 |
|
||||
| `task-032` | U-5 | 对齐文档页 topbar/content/Page Aggregate shell | P1 |
|
||||
| `task-033` | U-6 | 补 Wolai UI visual regression smoke | P1 |
|
||||
|
||||
说明:本 checklist 建立阶段不直接修改 `harness-tasks.json`,避免把分析结果误登记为已排队执行任务。
|
||||
|
||||
## 11. 完成判定
|
||||
|
||||
- [x] `GET http://127.0.0.1:3000/` 返回 `x-mnote-web-owner: mnote-web`。
|
||||
- [x] 根页 body 标记 `data-mnote-shell="workspace"`。
|
||||
- [x] 根页不再出现极简欢迎页文案。
|
||||
- [x] 根页包含目标 UI 的核心结构区。
|
||||
- [x] `/documents/:id` 不返回 `未登录` 502。
|
||||
- [x] 文档页继续消费 Page Aggregate snapshot。
|
||||
- [x] 文档页默认编辑 host 仍是 `leptos_tiptap_island`。
|
||||
- [x] `3100` 只作为 legacy/reference/debug。
|
||||
- [x] `task114`、`task115`、`task118` 和可选 `task119` 在当前 3000 上通过。
|
||||
- [x] 本 checklist 才能移动到 `design/03-rust-web/done/`。
|
||||
Reference in New Issue
Block a user