chore: align mvp design governance

- 统一 local-first MVP 后阶段架构口径,补充 process 执行总序和 Reasonix 协作记录

- 归档已完成的 design checklist,标注参考型 process,更新 AGENTS/REASONIX/架构文档

- 补充文件树/主编辑器下载与上下文菜单相关实现、bug 记录和 smoke 脚本

验证:git diff --check;codegraph sync .;cargo test -p mnote-web;node --check scripts/task476-filetree-editor-context-menu-download-smoke.js
This commit is contained in:
lix-2026
2026-05-21 09:04:13 +08:00
parent fd94792ea6
commit 1956a8a21a
69 changed files with 3390 additions and 111 deletions
+16
View File
@@ -0,0 +1,16 @@
# CodeGraph data files
# These are local to each machine and should not be committed
# Database
*.db
*.db-wal
*.db-shm
# Cache
cache/
# Logs
*.log
# Hook markers
.dirty
+161
View File
@@ -0,0 +1,161 @@
{
"version": 1,
"include": [
"**/*.ts",
"**/*.tsx",
"**/*.js",
"**/*.jsx",
"**/*.py",
"**/*.go",
"**/*.rs",
"**/*.java",
"**/*.c",
"**/*.h",
"**/*.cpp",
"**/*.hpp",
"**/*.cc",
"**/*.cxx",
"**/*.cs",
"**/*.php",
"**/*.rb",
"**/*.swift",
"**/*.kt",
"**/*.kts",
"**/*.dart",
"**/*.svelte",
"**/*.vue",
"**/*.liquid",
"**/*.pas",
"**/*.dpr",
"**/*.dpk",
"**/*.lpr",
"**/*.dfm",
"**/*.fmx",
"**/*.scala",
"**/*.sc"
],
"exclude": [
"**/.git/**",
"**/node_modules/**",
"**/vendor/**",
"**/Pods/**",
"**/dist/**",
"**/build/**",
"**/out/**",
"**/bin/**",
"**/obj/**",
"**/target/**",
"**/*.min.js",
"**/*.bundle.js",
"**/.next/**",
"**/.nuxt/**",
"**/.svelte-kit/**",
"**/.output/**",
"**/.turbo/**",
"**/.cache/**",
"**/.parcel-cache/**",
"**/.vite/**",
"**/.astro/**",
"**/.docusaurus/**",
"**/.gatsby/**",
"**/.webpack/**",
"**/.nx/**",
"**/.yarn/cache/**",
"**/.pnpm-store/**",
"**/storybook-static/**",
"**/.expo/**",
"**/web-build/**",
"**/ios/Pods/**",
"**/ios/build/**",
"**/android/build/**",
"**/android/.gradle/**",
"**/__pycache__/**",
"**/.venv/**",
"**/venv/**",
"**/site-packages/**",
"**/dist-packages/**",
"**/.pytest_cache/**",
"**/.mypy_cache/**",
"**/.ruff_cache/**",
"**/.tox/**",
"**/.nox/**",
"**/*.egg-info/**",
"**/.eggs/**",
"**/go/pkg/mod/**",
"**/target/debug/**",
"**/target/release/**",
"**/.gradle/**",
"**/.m2/**",
"**/generated-sources/**",
"**/.kotlin/**",
"**/.dart_tool/**",
"**/.vs/**",
"**/.nuget/**",
"**/artifacts/**",
"**/publish/**",
"**/cmake-build-*/**",
"**/CMakeFiles/**",
"**/bazel-*/**",
"**/vcpkg_installed/**",
"**/.conan/**",
"**/Debug/**",
"**/Release/**",
"**/x64/**",
"**/.pio/**",
"**/release/**",
"**/*.app/**",
"**/*.asar",
"**/DerivedData/**",
"**/.build/**",
"**/.swiftpm/**",
"**/xcuserdata/**",
"**/Carthage/Build/**",
"**/SourcePackages/**",
"**/__history/**",
"**/__recovery/**",
"**/*.dcu",
"**/.composer/**",
"**/storage/framework/**",
"**/bootstrap/cache/**",
"**/.bundle/**",
"**/tmp/cache/**",
"**/public/assets/**",
"**/public/packs/**",
"**/.yardoc/**",
"**/coverage/**",
"**/htmlcov/**",
"**/.nyc_output/**",
"**/test-results/**",
"**/.coverage/**",
"**/.playwright-mcp/**",
"**/.idea/**",
"**/logs/**",
"**/tmp/**",
"**/temp/**",
"**/_build/**",
"**/docs/_build/**",
"**/site/**",
"recycle/**",
"tmp/**",
"test/results/**",
"test-results/**",
"references/**",
"LightRAG/**",
"home/lix/.hermes/skills/note-taking/mnote-block-ai/**",
"home/**",
".sandbox-home/**",
".sandbox-tmp/**",
".convex-tmp/**",
"graphify-out/**",
"design/old/**",
"design/**/reference-code/**",
"reference-code/**",
"rust/spikes/**/generated/**",
"rust/spikes/**/dist/**"
],
"languages": [],
"frameworks": [],
"maxFileSize": 1048576,
"extractDocstrings": true,
"trackCallSites": true
}
@@ -0,0 +1,79 @@
你是浏览器测试员,不是代码修改员。
项目根目录:`/mnt/Data1T/mnote`
被测入口:`http://127.0.0.1:3000`
输出目录:`/mnt/Data1T/mnote/tmp/reasonix-filetree-editor-context-menu-2026-05-21`
## 硬性约束
- 禁止修改源码、禁止 git commit、禁止回滚文件。
- 使用真实浏览器或 Playwright 验证。
- 使用全新的 isolated browser context / userDataDir;测试开始前清空 cookies、localStorage、sessionStorage、IndexedDB 和 Cache Storage。
- 所有截图和 `result.json` 写入输出目录。
- 失败时保留 console/network 摘要。
- 最终只汇报测试证据,不提出未经验证的代码结论。
## 登录
- 打开 `/auth`
- 优先点击“测试账号快速登录”。
- 默认测试账号:`mnote.e2e@example.com` / `MnoteE2E123!`
## 测试目标
验证当前 MNote 的:
1. 文件树右键菜单实际显示哪些动作,尤其是文件/asset 行是否有“下载”。
2. 文件树右键菜单点击后是否有可观测状态变化:
- `data-mnote-filetree-last-action`
- `data-mnote-filetree-last-action-status`
- `data-mnote-filetree-readonly-blocked`
- 是否发生新 tab / 下载 / network 请求。
3. 主编辑器附件 hover 菜单与更多菜单:
- hover 附件后是否出现 `[data-testid="mnote-attachment-actions"]`
- 点击更多菜单后是否出现 `[data-testid="mnote-tree-context-menu"][data-kind="attachment"]`
- attachment 菜单是否有“下载 / 弹窗预览 / 弹窗编辑 / 新窗口编辑”等动作。
## 推荐操作步骤
1. 打开主页面或任意 `/documents/...` 页面,确保侧边栏和文件树可见。
2. 如果当前没有明显文件/asset 行,可上传或使用页面内已有附件;也可以创建一个临时 `.txt` / `.md` / `.docx` 资源,但必须记录文件名。
3. 右键文件树中的 page/document 行,截图菜单并记录所有 menu item 文案与 `data-action`
4. 右键文件树中的 asset/resource 文件行,截图菜单并记录所有 menu item 文案与 `data-action`;重点确认是否有 `download`
5. 如菜单里有“下载”,点击并记录 download 事件或 network;如没有,标记为 `FAIL:filetree_asset_download_action_missing`
6. 在主编辑器找到附件块或上传一个小附件,hover 后截图附件 hover actions。
7. 点击附件 hover actions 的更多按钮,截图 attachment context menu 并记录 actions。
## result.json 格式
```json
{
"ok": false,
"url": "",
"assertions": [
{"name": "filetree_doc_context_menu_visible", "status": "pass|fail|blocked", "details": ""},
{"name": "filetree_asset_context_menu_visible", "status": "pass|fail|blocked", "details": ""},
{"name": "filetree_asset_download_action", "status": "pass|fail|blocked", "details": ""},
{"name": "editor_attachment_hover_actions", "status": "pass|fail|blocked", "details": ""},
{"name": "editor_attachment_context_menu", "status": "pass|fail|blocked", "details": ""}
],
"menus": {
"filetreeDocument": [],
"filetreeAsset": [],
"editorAttachment": []
},
"screenshots": [],
"consoleErrors": [],
"networkFailures": [],
"downloads": []
}
```
## 最终回复
请只汇报:
- `result.json` 路径
- 截图路径
- 每个断言的 pass/fail/blocked
- 关键失败证据
@@ -0,0 +1,49 @@
# Reasonix Worker Adesign 01/02/03 审查
Project root: `/mnt/Data1T/mnote`
## 目标
审查以下范围内的 `process/` 文档,判断哪些应继续执行、哪些应标为参考、哪些应归档到 `done/``old/process/`
- `design/01-tree-first-graph-kernel/process/`
- `design/02-convex-rust-long-term-architecture/process/`
- `design/03-rust-web/process/`
## 当前统一口径
MNote 已进入 local-first MVP 后阶段。本地 workspace folder 是默认数据真相,本地 `.md` 是页面正文真相,Rust kernel / projection / command 持有语义,`mnote-web` 是 3000 主入口,Convex 只是 auth/share/sync/ACP/cloud/compat 控制面。旧 Next / BlockNote / Convex 默认主存储口径只能作为 legacy / compat 背景。
## 只读要求
- 不要移动、修改或删除 `design/` 文件。
- 只创建或更新你的报告文件:`.codex/reasonix-tasks/results/design-governance-worker-a-01-03.md`
- 不要提交 git,不要回滚任何文件。
## 审查输出格式
对每个 process 文档给出:
```text
文件:
建议状态:keep-process | mark-process-reference | archive-done | archive-old-process | needs-human-confirmation
证据:
归档/保留原因:
建议补充说明:
```
重点检查:
- 是否仍用旧 Convex 默认存储 / Next 主入口 / BlockNote 默认编辑器口径。
- 是否已被 `design/01-05-current-priority-overview.md``ARCHITECTURE.md``CURRENT_ARCHITECTURE.md` 或同域 done 文档覆盖。
- 若建议 `archive-done`,必须指出文档内或仓库中可见的完成证据。
- 若建议 `archive-old-process`,必须指出覆盖它的新文档。
## 验证命令
可运行只读命令:
- `find design/01-tree-first-graph-kernel/process design/02-convex-rust-long-term-architecture/process design/03-rust-web/process -maxdepth 1 -type f -name '*.md' | sort`
- `rg -n "Convex|Next|BlockNote|MVP|当前状态|状态|待完成|未完成|已完成|smoke|done|process" <files>`
最终回复只说明报告路径和最高风险项。
@@ -0,0 +1,45 @@
# Reasonix Worker Bdesign 04 tree-domain 审查
Project root: `/mnt/Data1T/mnote`
## 目标
审查 `design/04-tree-domain/process/` 下所有 process 文档,判断哪些仍需继续执行,哪些已经可归档到 `done/`,哪些应降级到 `old/process/`
## 当前统一口径
MNote 已进入 local-first MVP 后阶段。树域长期口径是 Resource Tree -> File Tree -> Page TreeRust kernel / projection / command 是语义 owner。新增能力优先走 `tree.*` / `tree.resource.*`,文件树不能重新制造第二份资源真相。旧 Convex 主存储口径只作为 compat/cloud 背景。
## 只读要求
- 不要移动、修改或删除 `design/` 文件。
- 只创建或更新你的报告文件:`.codex/reasonix-tasks/results/design-governance-worker-b-04-tree.md`
- 不要提交 git,不要回滚任何文件。
## 审查输出格式
对每个 process 文档给出:
```text
文件:
建议状态:keep-process | mark-process-reference | archive-done | archive-old-process | needs-human-confirmation
证据:
归档/保留原因:
建议补充说明:
```
重点检查:
- `4-27``4-45` 是否已被近期 bug done、smoke 或代码改动覆盖。
- open target、trash、resource lifecycle、bulk delete、download、drop upload、open editors lifecycle 是否仍有未完成项。
- 若建议 `archive-done`,必须指出对应 done bug/smoke 或文件内完成证据。
- 若证据只来自当前未提交文件,标明“依赖未提交变更”,不要直接判定已稳定。
## 验证命令
可运行只读命令:
- `find design/04-tree-domain/process -maxdepth 1 -type f -name '*.md' | sort`
- `rg -n "状态|待完成|未完成|已完成|smoke|task|bugs/04|tree.resource|download|drop|open target|trash|bulk" design/04-tree-domain/process bugs/04-tree-domain scripts`
最终回复只说明报告路径和最高风险项。
@@ -0,0 +1,52 @@
# Reasonix Worker Cdesign 05/06/08/09 审查
Project root: `/mnt/Data1T/mnote`
## 目标
审查以下范围内的 process 文档:
- `design/05-editor-mainline/process/`
- `design/06-mindmap/process/`
- `design/08-wolai-aline-test-flow/process/`
- `design/09-siyuan-reference/process/`
判断哪些仍需继续执行,哪些应归档到 `done/`,哪些应降级到 `old/process/`,哪些只应标为参考流程。
## 当前统一口径
MNote 已进入 local-first MVP 后阶段。文档页默认主编辑器是 `leptos-tiptap` island,但本地 `.md` 才是正文真相。主编辑器 tab / resource open / OnlyOffice / mindmap 都应服从 Resource Tree 和 Page Aggregate,不恢复 BlockNote 或 Next App Router 主链。
## 只读要求
- 不要移动、修改或删除 `design/` 文件。
- 只创建或更新你的报告文件:`.codex/reasonix-tasks/results/design-governance-worker-c-05-06-08-09.md`
- 不要提交 git,不要回滚任何文件。
## 审查输出格式
对每个 process 文档给出:
```text
文件:
建议状态:keep-process | mark-process-reference | archive-done | archive-old-process | needs-human-confirmation
证据:
归档/保留原因:
建议补充说明:
```
重点检查:
- `5-15``5-26` 是否仍是当前 tab/resource/OnlyOffice/mindmap 主执行清单,还是已被近期代码和 smoke 覆盖。
- `5-2 / 5-7 / 5-9` 是否仍应保留为 Wolai/tiptap 体验持续参考,而非当前执行 checklist。
- `06-mindmap` 是否有已完成项应进入 done,是否仍有内核化/adapter 未完成。
- `08-wolai``09-siyuan` 是否应保持 process 作为流程/参考,还是需要改为 reference/done。
## 验证命令
可运行只读命令:
- `find design/05-editor-mainline/process design/06-mindmap/process design/08-wolai-aline-test-flow/process design/09-siyuan-reference/process -maxdepth 1 -type f -name '*.md' | sort`
- `rg -n "BlockNote|Next|leptos|tiptap|OnlyOffice|mindmap|tab|resource|状态|待完成|未完成|已完成|smoke|task" design/05-editor-mainline/process design/06-mindmap/process design/08-wolai-aline-test-flow/process design/09-siyuan-reference/process bugs scripts`
最终回复只说明报告路径和最高风险项。
@@ -0,0 +1,50 @@
# Reasonix Worker Ddesign 07/10 审查
Project root: `/mnt/Data1T/mnote`
## 目标
审查以下范围内的 process 文档:
- `design/07-ai/process/`
- `design/10-review/process/`
判断哪些仍需继续执行,哪些应归档到 `done/`,哪些应降级到 `old/process/`,哪些只应保留为参考审查。
## 当前统一口径
MNote 已进入 local-first MVP 后阶段。local-first 普通 Markdown 编辑主路径是 `currentFile + selection + allowedRoots / aiAccessScope + agent 原生 patch/diff + watcher 同步``mnote.doc.*` / `mnote.block.*` 是 cloud、remote、compat 或复杂结构辅助。Hermes/Reasonix 是 agent runtime;不要恢复 page-ai fast-path 或 BlockNote AI 主链作为默认方向。
## 只读要求
- 不要移动、修改或删除 `design/` 文件。
- 只创建或更新你的报告文件:`.codex/reasonix-tasks/results/design-governance-worker-d-07-10.md`
- 不要提交 git,不要回滚任何文件。
## 审查输出格式
对每个 process 文档给出:
```text
文件:
建议状态:keep-process | mark-process-reference | archive-done | archive-old-process | needs-human-confirmation
证据:
归档/保留原因:
建议补充说明:
```
重点检查:
- `7-10 / 7-12 / 7-14 / 7-15 / 7-16 / 7-17 / 7-27` 是否仍代表活跃 AI 主线,或已被 local-first 文件编辑与 ACP runtime 完成态覆盖。
- `7-30``7-33` 是否是当前 ACP session 后续产品化 checklist。
- `10-review/process/12-14` 是否已被后续执行/bug 修复覆盖,应归档到 done 或保留为 gap review。
- 标出任何仍把 `mnote.doc.markdown_edit``page-ai/block-edit-workflow`、BlockNote AI 或 Convex session 全文当默认主路径的段落。
## 验证命令
可运行只读命令:
- `find design/07-ai/process design/10-review/process -maxdepth 1 -type f -name '*.md' | sort`
- `rg -n "mnote\\.doc|mnote\\.block|page-ai|block-edit-workflow|BlockNote|Hermes|Reasonix|ACP|Convex|状态|待完成|未完成|已完成|smoke|task" design/07-ai/process design/10-review/process bugs scripts`
最终回复只说明报告路径和最高风险项。
@@ -0,0 +1,271 @@
# Design Governance Report — Worker A
审查范围:`design/01-tree-first-graph-kernel/process/``design/02-convex-rust-long-term-architecture/process/``design/03-rust-web/process/`
审查时间:2026-06-17
上位依据:`design/01-05-current-priority-overview.md`2026-05-21 口径)
---
## 总览
| 域 | 文件 | 建议状态 | 高风险项 |
|----|------|---------|---------|
| 01 | 1-tree-first-graph-kernel-v1.md | **keep-process** | 无 |
| 01 | 1-1-tree-first-graph-kernel-checklist-v2.md | **keep-process** | 无 |
| 01 | 1-2-tree-first-graph-product-vision-progress-v1.md | **keep-process** | 无 |
| 01 | 1-3-current-priority-execution-checklist-v1.md | **keep-process** | 无 |
| 01 | 1-4-next-phase-execution-roadmap-v1.md | **keep-process** | 无 |
| 01 | 1-5-next-phase-sequential-execution-checklist-v1.md | **keep-process** | ⚠ 多数 items 未勾选 |
| 01 | 1-6-next-phase-gap-closure-checklist-v1.md | **keep-process** | ⚠ browser smokes 未复跑 |
| 01 | 1-7-local-workspace-root-page-bundle-checklist-v1.md | **archive-done** | 无 |
| 02 | 2-3-local-workspace-access-control-productization-v1.md | **archive-done** | 无 |
| 03 | 3-rust-web-long-term-architecture-v1.md | **keep-process** | 无 |
| 03 | 3-1-rust-web-long-term-checklist-v2.md | **keep-process** | 无 |
| 03 | 3-3-rust-web-tree-realtime-event-stream-v1.md | **keep-process** | 无 |
| 03 | 3-13-rust-web-local-markdown-gfm-ast-parser-migration-v1.md | **keep-process** | ⚠ 3 unsub items |
| 03 | 3-15-local-markdown-asset-upload-relative-path-v1.md | **keep-process** | ⚠ browser smoke 待补 |
| 03 | 3-17-convex-export-web-entry-v1.md | **needs-human-confirmation** | ⚡ 设计稿但未落码 |
---
## 逐文件审查
---
### 1. `01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
**建议状态:keep-process**
**证据:** `01-05-current-priority-overview.md` §1 明确列为当前仍然有效的 3 份上位主线之一。文档更新于 2026-04-22,全文固定 "tree-first graph kernel" 长期方向,不使用旧 Convex 默认存储 / Next 主入口 / BlockNote 默认编辑器口径。
**归档/保留原因:** 仍是 tree-first kernel 方向的权威定义文档,所有后续设计以此为基础。
---
### 2. `01-tree-first-graph-kernel/process/1-1-tree-first-graph-kernel-checklist-v2.md`
**建议状态:keep-process**
**证据:** `01-05-current-priority-overview.md` §3 明确列为"仍然有效但不在第一线"的 process 文档。文档头部已标注优先级转移说明,使用 `DONE / PARTIAL / NOT_STARTED` 状态体系,反映真实代码状态而非乐观口径。
**归档/保留原因:** 仍是 Kernel Phase 全景参考,Phase 0-3 已 DONEPhase 4-9 进度清晰。
---
### 3. `01-tree-first-graph-kernel/process/1-2-tree-first-graph-product-vision-progress-v1.md`
**建议状态:keep-process**
**证据:** 更新于 2026-05-19,引用 `01-05-current-priority-overview.md` 作为上位依据。使用 local-first MVP 后阶段口径。包含对照 Wolai 帮助中心的详细块类型覆盖率、AI 操作能力缺口分析,以及三条架构收口主线的进度百分比。
**归档/保留原因:** 唯一一份产品愿景 vs 实现进展的对照文档,提供 gap analysisAI 操作思维导图 / Office 文件),对产品规划仍有参考价值。
---
### 4. `01-tree-first-graph-kernel/process/1-3-current-priority-execution-checklist-v1.md`
**建议状态:keep-process**
**证据:** `01-05-current-priority-overview.md` §6 明确引用为"持续推进 checklist"。文档 P0-P6 全部勾选且有验证证据(单测命令 + smoke 脚本),P7 已完成阶段性瘦身。迁移到 done/ 的标准清晰列出并已满足。
**归档/保留原因:** 当前最活跃的执行清单,`1-3` 本身作为持续执行清单保留在 process/,已完成的上位设计各自在 done/。
---
### 5. `01-tree-first-graph-kernel/process/1-4-next-phase-execution-roadmap-v1.md`
**建议状态:keep-process**
**证据:** 更新于 2026-05-19,引用 `01-05``1-3`。全文核心论点是"架构收口为主,产品化闭环为辅",与当前 local-first MVP 后阶段口径完全一致。
**归档/保留原因:** 定义了下一阶段路线图(WorkspacePath → BufferStore → Page Aggregate kernel-native → command context → live cache),仍有效。
---
### 6. `01-tree-first-graph-kernel/process/1-5-next-phase-sequential-execution-checklist-v1.md`
**建议状态:keep-process**
**证据:** 更新于 2026-05-19,引用 `1-4``1-3`。所有执行原则和 Phase A0-A7 定义仍符合当前架构方向。
**归档/保留原因:** 提供了顺序执行的详细清单,且多数 items 尚未勾选,说明它是未来的工作计划而非已完成的文档。
**⚠ 风险:** 全场 687 行,大多数 checkbox 未勾选。如果团队当前执行顺序已不按此 checklist 推进(例如优先做 BufferStore 而非 smokes),应降级为 `mark-process-reference` 或更新本文件。当前保留但需关注执行偏差。
---
### 7. `01-tree-first-graph-kernel/process/1-6-next-phase-gap-closure-checklist-v1.md`
**建议状态:keep-process**
**证据:** 创建于 2026-05-19,状态 `PARTIALLY_DONE`。引用 `1-5``1-4`。基于 Reasonix 执行汇总的缺口补齐文档。
**归档/保留原因:** 记录了哪些缺口已由 Reasonix 执行填补(BufferStore、CommandContext、export/rollback),哪些仍未完成(browser smokes、conflict UI)。仍有跟踪价值。
**⚠ 风险:** §6.2 列出 6 个未复跑的 browser smokes。这些 smokes 如果长期不跑,本文件的剩余项跟踪价值下降。
---
### 8. `01-tree-first-graph-kernel/process/1-7-local-workspace-root-page-bundle-checklist-v1.md`
**建议状态:archive-done**
**证据:**
- 所有 checklist items 全部勾选并附验证命令
- 8 个 Rust 单测通过(`create_default_local_workspace_uses_managed_data_root``local_tree_command_create_page_creates_timestamped_markdown_and_sibling_folder` 等)
- HTTP smoke 通过:`node scripts/task443-local-markdown-asset-upload-smoke.js`
- 浏览器验收完成(截图保存到 `tmp/page-bundle-*`
- 磁盘结构验证通过:无默认 `pages/``assets/``mindmaps/` 顶层目录,新建页面带 `HHMMSS` 时间戳
**代码证据:**
- `create_default_local_workspace_for_actor_at_base``local_folder_source.rs:1494`
- 同名目录作为页面子容器 — 单测 `local_page_tree_uses_sibling_directory_as_page_children_container`
- 文件树不合并 `.md` 和同名目录 — 单测 `local_file_tree_keeps_markdown_and_sibling_folder_as_separate_rows`
**归档/保留原因:** 所有目标已实现且有验收证据。设计稿的收口方向是"默认工作区只保留用户内容根目录和 `.mnote/` 系统目录"——已落地。
---
### 9. `02-convex-rust-long-term-architecture/process/2-3-local-workspace-access-control-productization-v1.md`
**建议状态:archive-done**
**证据:**
- A1-A5 (P0 管理员控制面 API):全部完成,`GET/POST/DELETE /api/admin/access-policy/*` 已实现并有单测覆盖
- B1-B5 (P1 权限覆盖审计):全部完成,覆盖 local folder open、page body write、tree command、Hermes/Reasonix、shared AI session
- `01-05-current-priority-overview.md` §5 已将"管理员目录授权 UI/API"列为"已有最小实现与验证证据,不再作为当前第一优先级"
**代码证据:**
- `rust/crates/mnote-web/src/routes/local_folder_source.rs``add_local_access_grant_for_context``create_local_access_grant``ensure_local_workspace_access_for_actor`
- `rust/crates/mnote-web/src/ssr/pages/admin.rs` — 管理员 UI
- `rust/crates/mnote-web/src/routes/mod.rs` — route registration
**归档/保留原因:** 所有 checklist items 已勾选并有单测/代码证据。文档中定义的产品化目标(管理员可管理、agent 可复用、全入口不绕过)已实现。后续 access control 扩展可在新文档中推进。
---
### 10. `03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
**建议状态:keep-process**
**证据:** `01-05-current-priority-overview.md` §1 明确列为当前仍然有效的 3 份上位主线之一。更新于 2026-05-09,定义了 Rust Web 分层方案(axum + Leptos Islands + Hermes)。
**归档/保留原因:** 仍是 Rust Web 架构的权威定义文档。
---
### 11. `03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md`
**建议状态:keep-process**
**证据:** `01-05-current-priority-overview.md` §3 明确列为"仍然有效但不在第一线"。文档头部已标注优先级转移说明(2026-04-22 + 2026-04-29 复核补充)。Phase 0-8 状态清晰(DONE / PARTIAL / NOT_STARTED),反映真实代码状态。
**归档/保留原因:** 仍是 Rust Web 实施全景参考。Phase 1 接近 DONE、Phase 8 接近 DONE、Phase 3/4/5/7 为 PARTIAL。
---
### 12. `03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
**建议状态:keep-process**
**证据:** `01-05-current-priority-overview.md` §2.3 引用为 Tree Realtime 当前状态文档。2026-05-18 补充 local-first 口径,明确 Convex realtime substrate 仅适用于 convex_workspace。有明确 §9.2 "仍未完成" 说明不能移到 done/。
**归档/保留原因:** 有明确 §9.2 未完成项(Sidebar/page subtree/filetree 未全部统一到同一条 live stream cacheWS 尚未成为主实时链路)。README 中明确了 done/ 迁移前必须完成的验收标准。无风险。
---
### 13. `03-rust-web/process/3-13-rust-web-local-markdown-gfm-ast-parser-migration-v1.md`
**建议状态:keep-process**
**证据:** 文档自身 §11.9 明确标注 "[ ] 迁移完成后把设计稿状态从 process 移到 done" 未勾选。剩余 3 项未完成:
1. "[ ] 新增 web shell 单测,验证 legacy <-> Tiptap 转换不丢 marks"
2. "[ ] 把手写 parser 标记为过渡实现"
3. "[ ] 移除 web_shell.rs 中只为补丁存在的临时适配分支"
**归档/保留原因:** 迁移工作未完成。当前读侧 AST 已就位,写侧回 Markdown 仍部分依赖手写规则。无口径冲突。
**⚠ 风险:** 3 个未完成项如果长期无人推进,应降级为 `mark-process-reference`。但当前准确。
---
### 14. `03-rust-web/process/3-15-local-markdown-asset-upload-relative-path-v1.md`
**建议状态:keep-process**
**证据:** 文档状态头明确标注"(代码、单测和 HTTP smoke 已完成;真实浏览器 smoke 待补)"。§9 checklist 中除浏览器 smoke 外所有项目已勾选。实现与缺陷闭环在 `bugs/03-rust-web/done/3-16`
**代码证据:**
- `local_markdown_asset_upload_copies_next_to_markdown_with_relative_path` 单测通过
- HTTP smoke 覆盖 `/api/local-folder/assets/upload` 真实 multipart 上传
**归档/保留原因:** 代码实现已完成,仅缺浏览器 smoke 这一最终验收。应该保持 process 直到 browser smoke 通过。
**⚠ 风险:** 缺少浏览器 smoke 意味着编辑器上传面板的实际用户体验未验证。低风险。
---
### 15. `03-rust-web/process/3-17-convex-export-web-entry-v1.md`
**建议状态:needs-human-confirmation → 候选人:mark-process-reference**
**证据:** 状态标记为 `process`,但文档是一份 Web 控制台 UI 的设计提案("后续落码顺序"列出 5 步尚未实现的落码步骤)。CLI 工具已存在:
- `scripts/export-convex-workspace-to-local.js` 支持 `--dry-run``--manifest``--conflict-report``--rollback`
- `task444``task455` smoke 通过
`01-05-current-priority-overview.md` §5 已将 P6 (Convex 导出) 列为"已有最小实现与验证证据,不再作为当前第一优先级"。
**建议补充说明:** 文档作为 Web 入口设计稿保留在 process/ 会产生"这是当前待办"的误导。建议:
- **选项 A (推荐)mark-process-reference** — 保持文档但标记为参考设计,不误认为待办。保留 CLI 设计供后续 Web 入口实现时参考。
- **选项 Bkeep-process** — 如果团队明确要在短期内实现 Web 入口。
- **选项 Carchive-old-process** — 如果决定 CLI 能力已足够,不优先做 Web UI。
**⚠ 风险:** 如果标记为 process,用户可能误以为 Web 入口已进入实施状态。实际上 5 个落码步骤均未开始。
---
## 汇总统计
| 建议状态 | 数量 | 文件 |
|---------|------|------|
| keep-process | 11 | 1, 1-1, 1-2, 1-3, 1-4, 1-5, 1-6, 3, 3-1, 3-3, 3-13, 3-15 |
| archive-done | 2 | 1-7, 2-3 |
| needs-human-confirmation | 1 | 3-17 |
> 注:3-15 虽推荐 keep-process,但其代码实现已完成,仅缺浏览器 smoke;在 browser smoke 通过后应自动进入 archive-done。
---
## 最高风险项
1. **`3-17-convex-export-web-entry-v1.md` (needs-human-confirmation)** — 被标记为 process 但实质是未落码的设计稿,可能误导读者以为 Web 入口在实施中。需人工确认是降级为参考、归档、还是推进落地。
2. **`1-5-next-phase-sequential-execution-checklist-v1.md` (keep-process, ⚠)** — 全场 687 行,大多数 checkbox 未勾选。如果执行计划已偏离此 checklist,内容可能过时。
3. **`1-6-next-phase-gap-closure-checklist-v1.md` (keep-process, ⚠)** — 6 个 browser smokes 长期未复跑降低剩余项跟踪价值。
4. **`3-13-rust-web-local-markdown-gfm-ast-parser-migration-v1.md` (keep-process, ⚠)** — 3 个未完成项如果无人认领,迁移工作将停滞在 partial 状态。
---
## 验证命令
```bash
# 确认文件存在
find design/01-tree-first-graph-kernel/process design/02-convex-rust-long-term-architecture/process design/03-rust-web/process -maxdepth 1 -type f -name '*.md' | sort
# 确认归档建议的文件已落地代码
cargo test -p mnote-web create_default_local_workspace_uses_managed_data_root -- --nocapture
cargo test -p mnote-web local_tree_command_create_page_creates_timestamped_markdown_and_sibling_folder -- --nocapture
cargo test -p mnote-web local_workspace_access -- --nocapture
```
---
## 未完成项
- 本报告仅为只读审查,未移动任何 `design/` 文件。
- 如果审核通过,`1-7``2-3` 应移至各自域的 `done/`
- `3-17` 需人工确认最终状态。
@@ -0,0 +1,230 @@
# Design Governance Report — Worker B: 04-tree-domain
> 生成时间: 2026-05-21
> 范围: `design/04-tree-domain/process/` 全部 12 个 process 文档
> 状态: 只读审查,未修改任何 `design/` 文件
---
## Executive Summary
审查 12 个 process 文档,建议分布:
| 状态 | 数量 | 文档 |
|------|------|------|
| archive-done | 6 | 4-27, 4-39, 4-40, 4-41, 4-42, 4-43 |
| keep-process | 5 | 4-28, 4-34, 4-38, 4-44, 4-45 |
| mark-process-reference | 1 | 4-23 |
**最高风险项**:
1. **4-45 checklist 完全未执行** — 4 项全部未勾选,Worker D 停留在计划阶段未产出任何变更
2. **4-34 local folder 子项仍无 no-refresh 证据** — Convex 侧已全部通过但 local folder 侧仍标 reload,这是 dual-browser 矩阵的唯一缺口
3. **4-44 仍有 3 个 browser smoke 未勾选** — Rust 代码改动已合入但 task430/task471 及新增 smoke 未通过
4. **4-28 React DOM focus 仍缺浏览器证据** — 核心 restore location 已实现但前端焦点行为未验证
---
## 逐文档审查
### 4-23 — Local / Cloud 显式桥接 P3 候选合同 v1
**建议状态:mark-process-reference**
- **证据**: 这是 P3 候选设计合同,定义了 `tree.bridge.import/export/publish/sync` 的 preview/execute 契约。文档无 checklist、无 code change、无 execution record。
- **归档/保留原因**: 这是一个**候选策略文档**,不是执行 checklist。P3 未排入当前 sprint,文档本身定义了清晰的 done gateP0/P1/P2 smoke 全绿+preview UI 组件复用+rollback plan+浏览器 smoke)。作为长期架构参考保留比归档更有价值。
- **补充说明**: 文档质量高,边界定义清晰。建议在文档头部加注 "P3 Candidate — 未进入实现,参考保留"。
### 4-27 — Resource Lifecycle Command Cutover v1
**建议状态:archive-done**
- **证据**:
- Section 2 "已落地范围" 列出 7 个已实现条目:`bridge-runtime` 新增 4 个命令计划、`storage-convex-bridge` 映射、`wolai-frontend` `/api/media/batch``/api/media/purge` 迁移、`mnote-web` Rust 3000 兼容 alias。
- Section 3 的 "未完成边界"(双浏览器 no-refresh、reveal/focus、projection delta)已由 4-34 和 4-28 覆盖。
- Section 7 "剩余边界"rename 仅覆盖附件、empty-trash 仍为批量合同、前端 Sidebar 仍走旧 URL)是有意保留的边界,非未完成任务。
- Section 8 sub-stagelocal_folder file_path resource lifecycle)已全部合入并通过:`tree.resource.archive/restore/purge` 进入 `.mnote/trash`, `trash-index.json`, `task437` smoke 通过。
- 2026-05-15 验收测试全部:4 项 `cargo test` 通过,2 项 wolai-frontend 测试通过。
- **归档原因**: 核心命令切换已完成归档。未完成项已明确转移至后续文档(4-34/4-28)或是有意保留的边界(empty-trash 批量合同、rename 范围限制)。
- **补充说明**: 归档前建议在文件头部加注引用 `4-34`(dual browser)、`4-28`(reveal/focus) 和 `4-37`(trash modal) 作为后续入口。
### 4-28 — Trash Restore Location Reveal Focus v1
**建议状态:keep-process**
- **证据**:
- **已落地**`restore_parent_id`/`restore_sort_order` schema`documents.softDelete` 快照写入,`documents.restore` 位置恢复与 fallback 逻辑,Sidebar 恢复后展开 focus。
- **已验证**:4 个测试文件(32 items)通过,`task429` browser smoke 通过覆盖 restore_location/fallback/sort_order 无重复。
- Section 5 "待补" 仍有 2 项未完成:React Sidebar Drawer 的恢复后 DOM focus 缺浏览器证据;双浏览器 no-refresh 归入阶段 8。
- **留存原因**: React Sidebar Drawer DOM focus 的浏览器证据未补。当前 3000 主壳 smoke 已覆盖 "恢复后无需刷新可见/reveal",但 "DOM focus" 断言是 React Drawer 代码路径的独立验证。
- **补充说明**: React DOM focus 是一个较小的缺口,可考虑在下一轮补充一个 focused smoke 后归档。
### 4-34 — Filetree Trash Dual Browser No Refresh v1
**建议状态:keep-process**
- **证据**:
- **Convex 侧全部通过**task432(页面) + task433(附件) + task434(mindmap/table) 双浏览器 no-refresh smoke 全部通过且记录了事件来源。
- task435(empty-trash 汇总) 作为 3.4 提及但**无独立执行记录**,不过其实已被 task432-434 的 empty-trash 子步骤覆盖。
- **local folder 侧未完成**3.5 task436 无执行记录。文档明确标注 "非 md 本地资源文件 watcher / trash / restore / purge 仍单列为待补"。
- 完成标准要求 local folder 子项也通过 task436 或等价 smoke;当前状态标 reload,不能标 no-refresh。
- **留存原因**: local folder 双浏览器 no-refresh 仍然是明确缺口。同时 `resync_required` delta 事件合同需在后续新增资源 route 时补回归。
- **补充说明**: 建议将 Convex 侧已完成的部分在文档内打上标记(如 `[Convex ✓]`),只保留 local folder 暴露项作为未完成。
### 4-38 — FileTree Resource Lifecycle 与 Open Target 收口 Checklist v1
**建议状态:keep-process**
- **证据**:
- 已勾选:`local-file:<relativePath>``resolve_local_raw_file_id``local-file` restore/purge 测试、local mindmap 单独删除入 trash index、local move `sortOrder` 返回 `_unsupportedFields`、Rust `cargo test` 通过。
- **未勾选**:扩展 File Tree open target、保留 `local:asset:/local:node:` 兼容、前端 delete/bulk delete 优先走 `/api/tree/commands`、browser smoke 覆盖全链、task437 继续通过。
- 执行记录确认 "未完成:File Tree open target 合同扩展、前端 delete/bulk delete 全面 cutover、浏览器垃圾箱资源行全链 smoke"。
- **留存原因**: 虽然核心 Rust 变更已合入,但 open target 扩展已有 4-39 承接,前端 cutover 和 browser smoke 尚未完成。需待这些子项完成后归档。
- **补充说明**: 建议在文档头部加注 "open target 合同由 4-39/4-42 覆盖;bulk delete 由 4-41 覆盖"。本文件的剩余项(前端 cutover + browser smoke)可浓缩后移至更轻量文档。
### 4-39 — FileTree Open Target 与 Resource Identity Checklist v1
**建议状态:archive-done**
- **证据**:
- **全部 5 项 checklist 已勾选**:扩展 `FileTreeOpenTarget``LocalFile`/`Directory` variant + `resourceKind`)、open target 包含 canonical identity、`parse_open_target_from_row` 兼容旧格式、camelCase 序列化、Rust 单测通过。
- **验收全部通过**`cargo test -p mnote-web filetree_runtime``runtime_api``tree_shell_filetree_mode_embeds_asset_state` 全部通过。
- **归档原因**: Checklist 全部完成。唯一剩余项 "上游 row dataset 补齐 rootUri" 已由 4-42 承接。
- **补充说明**: 归档时建议在头部引用 4-42 作为 rootUri 补齐的后续文档。
### 4-40 — Resource Lifecycle Command Cutover 与垃圾箱 Smoke Checklist v1
**建议状态:archive-done**
- **证据**:
- 从 7 项 checklist 中,6 项已勾选:旧 route 保留 fallback、command response 保留 artifact、trash workbench 显示 local resource、新增 browser smoke (task437 + task464)。
- **唯一未勾选项**"前端 local resource delete/bulk delete 优先调用 `/api/tree/commands` archive/purge/restore" — 这已被 4-41 覆盖。
- **验收全部通过**`task437``task464` smoke 通过,`cargo test resource_trash``cargo test tree_command_local_folder` 通过。
- **归档原因**: 文档定义的 smoke 覆盖率目标已完成。前端 cutover 的剩余项已由 4-41 清晰承接。
### 4-41 — FileTree Bulk Resource Command Cutover Checklist v1
**建议状态:archive-done**
- **证据**:
- **全部 6 项 checklist 已勾选**local file asset/mindmap/table bulk delete 使用 tree command、成功后刷新 snapshot/trash、失败保留错误、smoke 覆盖不同 resource kind。
- **验收全部通过**`task464``task471` smoke 通过,`cargo test sidebar_filetree_local_table_bulk_delete_uses_tree_command``tree_command_local_folder` 通过。
- **归档原因**: Checklist 已全部完成。云端 table 保留旧 fallback 是有意保留的边界(已勾选禁止事项)。
### 4-42 — FileTree Open Target RootUri Contract Checklist v1
**建议状态:archive-done**
- **证据**:
- **全部 5 项 checklist 已勾选**`FileTreeRuntimeEnvironment.rootUri``resolve_open_target` 使用 rootUri、SSR state 传入 rootUri、反序列化兼容缺失 rootUri、Rust 单测覆盖。
- **验收全部通过**3 项 `cargo test` 全部通过。
- **归档原因**: 文档定义的 rootUri 合同已全部实现。这是 P0-4 的收尾文档,已经完成其使命。
### 4-43 — Local Folder Sort Order Persistence Checklist v1
**建议状态:archive-done**
- **证据**:
- **全部 6 项 checklist 已勾选**`.mnote/file-order.json` 持久化、move sortOrder 写入、projection 消费排序记录、rename/archive/restore 后清理悬挂项、Rust 合同测试、browser smoke。
- **验收全部通过**`cargo test local_tree_command_move_with_sort_order``tree_command_local_folder` 通过,`task472` smoke 通过。
- **归档原因**: 完整的端到端实现。`_unsupportedFields.sortOrder` 退役记录清晰。
### 4-44 — FileTree Action Layer Paste / Drop / Delete Checklist v1
**建议状态:keep-process**
- **证据**:
- **Rust 代码已合入**`recordFileTreeAction`/`recordFileTreeActionStatus`、右键 `paste-into` 使用菜单触发 row、`layout.rs` readonly 阻断(data 属性标记 + alert fallback)、paste/drop preflight。
- **验收 2/5 已勾选**`cargo test -p mnote-web layout``tree_command_local_folder` 通过。
- **3 项验收未勾选**`node scripts/task430-vscode-explorer-stage7-smoke.js``node scripts/task471-local-folder-bulk-resource-trash-smoke.js`、新增 `task47*-filetree-action-layer-smoke.js`
- **留存原因**: Rust 侧变更已合入但 browser smoke 验证未通过。task471 的语义断言依赖 environment preparation4-45 也依赖同一个环境)。无法在不验证 smoke 的情况下确认动作层行为正确。
- **补充说明**: task430 的 workspace bootstrap 问题(4-45 提及)是前置阻塞项。建议先修复 task430 前置条件,再补全 4-44 的 smoke 验收。
### 4-45 — FileTree / Open Editors Lifecycle Link Checklist v1
**建议状态:keep-process**
- **证据**:
- **全部 4 项 checklist 未勾选**
1. "跑或修复 task430 / task471 的可复现前置条件" — 未完成
2. "明确哪些 smoke 已过、哪些因环境阻塞" — 未完成
3. "补充 open editor / filetree active row / lifecycle 状态断言建议" — 未完成
4. "更新 4-44 checklist 执行记录" — 未完成
- 执行记录仅包含 Worker D 的讨论和计划,没有实际变更。
- **留存原因**: 这是一份非常新的文档(2026-05-21),所有项都未验证。它不是高复杂度文档,但需要解决 task430 workspace bootstrap 前置问题后才能推进。如果 task430 环境问题没有阻碍,可在 1-2 小时内完成所有 4 项。
- **补充说明**: 这是一份"接通型"文档 — 它的主要目标是让 4-44 的 smoke 验证可复现,以及让 open editor state 可观测。建议优先修 task430 workspace bootstrap,这同时解除 4-44 和 4-45 的阻塞。
---
## 交叉依赖分析
```text
4-23 ──→ P3 候选,不依赖其他文档
4-27 ──→ 核心 cutover4-34/4-28 承接其尾项
4-28 ──→ 依赖 4-27 的 restore 命令,React DOM focus 单列为待补
4-34 ──→ Convex 侧依赖 4-27 的命令,local folder 侧为独立待补
4-38 ──→ open target 子项由 4-39/4-42 完成;bulk delete 由 4-41 完成
4-39 ──→ rootUri 子项由 4-42 完成 [已归档]
4-40 ──→ bulk delete 子项由 4-41 完成 [已归档]
4-41 ──→ 独立完成 [已归档]
4-42 ──→ 独立完成 [已归档]
4-43 ──→ 独立完成 [已归档]
4-44 ──→ 依赖 task430 workspace bootstrap(同 4-45 阻塞项)
4-45 ──→ 依赖 task430 workspace bootstrap
```
## 全局风险排序
| 风险 | 文档 | 描述 | 影响 |
|------|------|------|------|
| 🔴 高 | 4-45 | 4 项 checklist 全部未勾选,Worker D 无产出 | open editor lifecycle 状态不可观测 |
| 🔴 高 | 4-34 | local folder 双浏览器 no-refresh 未解决 | 唯一未完成的 dual-browser 缺口 |
| 🟡 中 | 4-44 | 3 个 browser smoke 未通过 | action layer 代码合入但未经验证 |
| 🟡 中 | 4-28 | React DOM focus 无浏览器证据 | location restore 已实现但焦点行为未确认 |
| 🟢 低 | 4-38 | 前端 cutover 代码未完成但已有 4-39/4-41 覆盖 | 信息过期风险,非功能缺口 |
| 🟢 低 | 4-23 | P3 候选,从未进入实现 | 无实际风险 |
---
## 建议归档路径(非执行)
以下仅作为人工决策参考,本次报告**未移动任何文件**:
| 文档 | 建议目标 | 前提条件 |
|------|----------|----------|
| 4-27 | `done/` | 添加引用 4-34/4-28 的头部注释 |
| 4-39 | `done/` | 添加引用 4-42 的头部注释 |
| 4-40 | `done/` | 无 |
| 4-41 | `done/` | 无 |
| 4-42 | `done/` | 无 |
| 4-43 | `done/` | 无 |
| 4-23 | `process/` 保留 | 添加 "P3 Candidate" 头注 |
| 4-28 | `process/` 保留 | React DOM focus smoke 通过后归档 |
| 4-34 | `process/` 保留 | task436 或等价 local folder no-refresh 证据通过后归档 |
| 4-38 | `process/` 保留 | 剩余前端 cutover + UI smoke 通过后归档 |
| 4-44 | `process/` 保留 | 3 个 smoke 全部通过后归档 |
| 4-45 | `process/` 保留 | 所有 4 项 checklist 完成后归档 |
---
## 报告说明
**路径**: `.codex/reasonix-tasks/results/design-governance-worker-b-04-tree.md`
**只读约束**: 本文只创建了报告文件,未移动、修改或删除任何 `design/` 文件。上述归档/移动建议需由人工或后续 Worker 执行。
**验证命令**:
```bash
# 确认报告文件存在
find .codex/reasonix-tasks/results/ -name '*worker-b*'
# 确认未修改任何设计文件
cd /mnt/Data1T/mnote && git diff --stat design/
# 复现 Rust 验收(抽样)
cargo test --manifest-path rust/Cargo.toml -p mnote-web filetree_runtime -- --test-threads=1
cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_command_local_folder -- --test-threads=1
# 检查 smoke 脚本语法
node --check scripts/task430-vscode-explorer-stage7-smoke.js
node --check scripts/task471-local-folder-bulk-resource-trash-smoke.js
```
@@ -0,0 +1,254 @@
# Design Governance Report — Worker C: 05/06/08/09 Review
> Generated: 2026-05-21
> Scope: `design/05-editor-mainline/process/`, `design/06-mindmap/process/`, `design/08-wolai-aline-test-flow/process/`, `design/09-siyuan-reference/process/`
---
## Executive Summary
Of 22 process documents reviewed:
| Status | Count | Documents |
|--------|-------|-----------|
| archive-done | 10 | 5-15, 5-17, 5-18, 5-19, 5-20, 5-21, 5-22, 5-23, 5-24, 5-25 |
| keep-process | 4 | 5-26, 5-9, 5-6 (partial), 6-mindmap-phase6-leptos-ui-shell-reuse-checklist |
| mark-process-reference | 7 | 5-5, 5-2, 5-7, 5-14, 6-mindmap-leptos-adapter-reference-notes, wolai-aline-test-flow-v1, 9-siyuan-reference |
| needs-human-confirmation | 1 | 5-16 (minor open items, superseded by later docs) |
**Highest risk**: 5-26 remains unexecuted — convergence plan proposed but never landed. 5-9 Phase F (Page Aggregate items) still shows TODO but the work was done in 5-6 checklists (mild stale reference risk). Mindmap `leptos-mindmap` checklist is large and has visual parity items that haven't been systematically closed.
---
## 1. `design/05-editor-mainline/process/` — 18 files
### 5-15 — local-markdown-path-identity-unification-checklist-v1
**建议状态:archive-done**
- **证据**: 所有 P0 (3 items) 和 P1 (1 item) checklist 均已勾选完成。文档末尾 `执行状态(2026-05-19` 列出了所有已完成的验收命令和补充清理(删除 `ParsedLocalMarkdownPage.mnote_id` 字段)。
- **归档原因**: 这是一个单次执行的 checklist,所有目标均已验证通过。后续身份逻辑若有回归需求,可查阅 `done/`
- **补充说明**: 验收命令完整可复现,是存档质量良好的 checklist。
### 5-16 — editor-group-resource-tab-safety-checklist-v1
**建议状态:needs-human-confirmation**
- **证据**: 核心项已完成:resource tab MRU、关闭 guard、`aria-selected`/`tabindex`、task460 smoke。剩余未完成项:正式 `MainEditorTabEntry` 模型(后被 5-18 的状态模型覆盖)、打开失败 placeholder tab(后被 5-18 完整实现)。
- **留存原因**: 未完成的 2 项已被后续 checklists (5-18) 解决,文档本身已无独立未完成工作。
- **建议**: 若确认 5-18 已完全覆盖 placeholder tab 需求,可归档。需人工确认。
### 5-17 — resource-editor-kind-and-smoke-matrix-checklist-v1
**建议状态:archive-done**
- **证据**: 核心项已完成:PDF 独立 badge kind、file fallback badge、smoke 覆盖 markdown/text/code/image/pdf 未知类型。`scripts/task461-resource-kind-smoke.js` 已通过。
- **归档原因**: 未完成项(正式 resolver 抽象、Office DocumentServer 回源、`active-tab/side/new-window` 统一 resolver)已由 5-19 和 5-26 覆盖。本文件是 5-17→5-19→5-26 递进的第一层,已完成的历史阶段。
### 5-18 — main-editor-tab-state-and-error-placeholder-checklist-v1
**建议状态:archive-done**
- **证据**: 所有 checklist 项已勾选。`createResourceSession` 已补 `lastActiveAt``openResourceInActiveTab` 失败时保留 error placeholder tab。`task462` smoke 通过。
- **归档原因**: 未完成项(`lastActiveAt` 未用于排序、dirty guard 无 UI 确认弹窗)属于后续 5-20 的独立主题。本文件闭环了主状态模型和错误占位。
### 5-19 — resource-open-resolver-and-onlyoffice-checklist-v1
**建议状态:archive-done**
- **证据**: 所有 checklist 项已执行。`resolveResourceOpen` 已定义,`buildOnlyOfficeOpenUrl` 已修正回源 URL`task463` smoke 覆盖 docx/pptx/xlsx badge、iframe、new-window。
- **归档原因**: 唯一未完成项——“未验证真实外部 DocumentServer 协同编辑会话”,这属于外部服务可用性测试而非设计执行,不应阻塞归档。
### 5-20 — resource-tab-close-confirm-checklist-v1
**建议状态:archive-done**
- **证据**: 所有 checklist 项已勾选并验证。`showResourceTabCloseGuardNotice` 对 dirty/saving/hasExternalConflict 输出可见提示,`task460` 断言可见提示不堆叠。
- **归档原因**: 单次执行 checklist,已完成闭环。
### 5-21 — editor-tab-url-active-identity-checklist-v1
**建议状态:archive-done**
- **证据**: 所有项已勾选。resource tab 激活写入 `resourceTab=<canonical identity>` URL statefiletree row 设 `data-active=true``task457` 覆盖全路径。
- **归档原因**: 单次执行 checklist,已验证。
### 5-22 — mindmap-main-editor-tab-kind-v1
**建议状态:archive-done**
- **证据**: 所有项已勾选。`kind=mindmap` resource tab 使用 `resource:mindmap:...` identity`task169` 从旧 object shell 口径切到 main editor resource tab 口径。
- **归档原因**: 已执行完成。旧 `task169` realtime 深测被标记为 `retiredRealtimeTopicEditing`,不含未完成工作。
### 5-23 — editor-side-target-secondary-pane-v1
**建议状态:archive-done**
- **证据**: 所有项已勾选。`openResourceAsSideTarget`,资源侧栏目标显示 unsupported placeholder`task472` smoke 通过。
- **归档原因**: 已完成闭环。
### 5-24 — open-editors-lightweight-view-checklist-v1
**建议状态:archive-done**
- **证据**: 所有项已勾选。`mnote.open_editors_snapshot.v1` 已暴露 snapshot 合同。`cargo test -p mnote-web web_shell` 通过。
- **归档原因**: 执行记录清晰,UI 当前阶段保持为可观测模型(无 sidebar UI 新增)。
### 5-25 — editor-tab-keyboard-and-mru-checklist-v1
**建议状态:archive-done**
- **证据**: 所有项已勾选。`bindMainEditorTabStrip()` keyboard roving focus、Enter/Space 激活、关闭后 MRU focus 迁移已实现。`cargo test` 通过。
- **归档原因**: Reasonix Worker B 输出 proposalCodex 实现最小安全集。闭环。
### 5-26 — resource-open-resolver-convergence-checklist-v1
**建议状态:keep-process**
- **证据**: 执行记录明确记载 "Worker C 进程停留在计划提交阶段,没有生成 final.md"。"审查...重复判断" 和 "给出最小收敛方案" 未完成。Checklist 第 4 节 2 个主项未勾选。
- **留存原因**: 仍有收敛正文附件与 FileTree 资源打开策略的实际工作待做。但 5-17/5-19 已完成了第一轮 resolver 建设,5-26 的收敛优先级较低。
- **补充建议**: 可降级为 Low Priority process,或在 next sprint 以 1-2 小时限时任务完成。
### 5-5 — page-aggregate-single-truth-alignment-v1
**建议状态:mark-process-reference**
- **证据**: 这是战略设计文档,非 checklist。定义了 Page Aggregate 的 Phase F-J 方向和长期架构目标。文档末尾明确结论 `leptos-tiptap 主编辑区已基本可用,但页面聚合仍未收口`
- **保留原因**: 作为长期架构参考仍有效。不应归档为 done(聚合未完全闭环),但也不需要作为 process 继续执行(方向已定)。
- **建议补充说明**: 建议在文档头部加注 `参考: 当前执行进度见 5-6`
### 5-6 — page-aggregate-alignment-checklist-v1
**建议状态:keep-process (partial)**
- **证据**: 大部分 Phase (F/G/H/I/J) 已基本完成。仍有较小缺口:Phase G 退出标准第 2 项(新增字段不散落 props)未勾选;Phase H 页面设置分类第 2 项(哪些项降级/隐藏)未勾选。
- **留存原因**: 文档非常大(34KB+),大部分已完成但仍有小缺口。可将这些微小项转到新的浓缩 checklist,或在本文件内更新状态后归档。
- **建议**: 考虑将已完成的 Phase F/G/H/I/J 子项标记 final,剩余的微量工作移至更轻量文档继续。
### 5-2 — tiptap-notion-like-template-adoption-v1
**建议状态:mark-process-reference**
- **证据**: 这不是 checklist,是策略分析文档。结论是 `以后续主编辑器开发继续以 Tiptap 官方能力模型为体验上限...已完成主编辑器接入和 Rust truth 落地`
- **保留原因**: 作为 tiptap 编辑器的设计参考和 P0.5 边界定义仍有价值。不应继续视为 process 执行清单。
### 5-7 — wolai-page-tree-main-editor-experience-restoration-v1
**建议状态:mark-process-reference**
- **证据**: 文档自身已声明执行口径转交 5-9 checklist 和 `wolai-aline` skill。包含大量 Wolai 参考截图路径和差异矩阵,是重要的取证基线。
- **保留原因**: 宝贵的 Wolai 对标取证基线记录和交互合同定义,作为 reference 保留。不应继续作为执行 checklist。
### 5-9 — wolai-aline-continuous-checklist-v1
**建议状态:keep-process**
- **证据**: 这是活跃的 checklist 文档。Phase A-E 大部分项目已完成(GREEN/PARITY),但存在未完成项:
- **A4** (字体栈与基础 token): 仍为 TODO
- **E18** (Clipboard/Paste): 仍为 TODO
- **F1-F5** (Page Aggregate 相关): 仍为 TODO — 但这些在 5-6 中已大部分完成
- Phase `已退役` 段落记录了一些退役项
- **留存原因**: 仍有少量待办项 (A4, E18, F1-F5)。但 F1-F5 的内容实际已在 5-6 中完成,这里存在陈旧风险。
- **建议**: 审查 F1-F5 是否应追认完成或引用 5-6 的进展;A4 和 E18 可考虑转入低优先级或归档。
### 5-14 — zed-lapce-vscode-reference-adoption-matrix-v1
**建议状态:mark-process-reference**
- **证据**: 这是参考采用矩阵设计文档,非 checklist。定义了 Zed/Lapce/VS Code/SideX 各自的参考职责和 P0-P3 实施建议。
- **保留原因**: 作为代码引用架构的权威参考文档很有价值。不应作为执行 checklist,但推荐作为后续 BufferStore、WorkspacePath 等模块设计时的引用入口。
- **补充说明**: 文档质量高、边界清晰,建议在 `done/` 也做一份副本或在 05-mainline README 中交叉引用。
---
## 2. `design/06-mindmap/process/` — 3 files
### 6-mindmap-kernel-phase6-projection-editor-v1
**建议状态:keep-process**
- **证据**: 这是 Phase 6 mindmap 的主架构设计文档。Phase 6.1 (真实 simple-mind-map runtime cutover) 已于 2026-05-10 完成。但文档定义的长期方向(Rust-native engine 路线)仍是未来可选项。
- **留存原因**: 定义了三路线定位和 Rust 内核+KMind-like 编辑器的当前主线。仍有 `10.1 现实复核差距` 中记录的 Phase 6.1 尾项(`view_data_change` 持久化、toolbar zoom 绑定、RichText 插件)。
- **补充说明**: 有 1 份对应的 done checklist (`6-mindmap-phase6-kmind-parity-detail-checklist-v1.md`),建议在 process 文档头部加注引用。
### 6-mindmap-leptos-adapter-reference-notes-v1
**建议状态:mark-process-reference**
- **证据**: 这是审计参考笔记,非执行 checklist。详细记录了 lx-doc/mind-map 的 Vue 组件到 kernel command 的映射,以及历史 React UI 壳的能力清单(8 个表格)。
- **保留原因**: 作为 adapter 实现和命令映射的详细参考,在后续 rust-native UI shell 开发时仍有价值。不应作为执行清单。
### 6-mindmap-phase6-leptos-ui-shell-reuse-checklist-v1
**建议状态:keep-process**
- **证据**: 这是活跃的执行 checklist。Task 1-7 和 Task 8 (Context Menu) 大部分已完成。Task 9 (KMind visual parity) 的 9 个子项中有未封闭的项(node style panel、base style panel 等详细面板界面仍有差距)。
- **留存原因**: 仍有未完成的 visual parity 细节项。KMind 视觉对齐是一个持续的收口过程(对应 `done/` 下的 detail checklist)。
- **补充说明**: 文档很大(43KB+),建议将已完成 Task 移入 `done/`,只保留未完成的 Task 9+ 视觉差异项。
---
## 3. `design/08-wolai-aline-test-flow/process/` — 1 file
### wolai-aline-test-flow-v1
**建议状态:mark-process-reference**
- **证据**: 这是对标测试流程的方法论文档,非 checklist。定义了标准流程(明确小任务→取 Wolai 基线→写 RED smoke→实现→本地验证→subagent 复测→主线程复核)、安全边界、资源路径和 subagent 模板。
- **保留原因**: 作为 Wolai 对标的标准流程参考仍有效。不应归档(仍在持续使用),也不应作为 process checklist(不是执行清单)。
- **补充说明**: 文档质量高,应保持为 process/ 中唯一的流程定义文件。
---
## 4. `design/09-siyuan-reference/process/` — 1 file
### 9-siyuan-reference-boundary-and-adoption-v1
**建议状态:mark-process-reference**
- **证据**: 这是参考边界与采用策略文档,非 checklist。明确区分了思源值得参考的层级(块引用/属性视图/本地工作区心智/导图插件)和不建议照搬的部分(`.sy` 真相层/SQL 主接口/前端运行时)。
- **保留原因**: 作为块式笔记能力参考的权威边界稿很有价值。当前无 process 待办项。
- **补充说明**: 建议在 process 文档头部或设计根目录 README 中交叉引用,让后续设计者知道“思源参考应当落在哪层”。
---
## 5. Cross-Cutting Observations
### 5.1 主要风险
1. **5-26 计划未落地**: 资源打开 resolver 收敛方案由 Worker C 提出但从未实现。这是本次审查中唯一有"提议但未实施"记录的文件。风险较低(基本功能已由 5-17/5-19 覆盖),但历史记录显示有一轮未闭环的工作流。
2. **5-9 F1-F5 陈旧风险**: 5-9 的 F1-F5 (Page Aggregate 相关项) 标记为 TODO,但实际工作在 5-6 中已大部分完成。存在同一工作两处记录不一致的问题。建议审查 5-9 F1-F5 是否应追认或标注 "参见 5-6"。
3. **6-mindmap 视觉差距无限延伸**: `6-mindmap-phase6-leptos-ui-shell-reuse-checklist-v1` 最后阶段(Task 9+)的 KMind visual parity 项目可能变成无限尾随工程。建议明确一个 "足够好" 的退出标准。
### 5.2 建议立即行动项
- 将 5-15/5-17/5-18/5-19/5-20/5-21/5-22/5-23/5-24/5-25 共 10 个文件移至 `done/`
- 将 5-5/5-2/5-7/5-14 标记或移动为 reference/ 类文档(当前 mnote 无统一 reference/ 目录,可另议)
- 为 5-26 决定是否在下一 sprint 收敛或正式废案
- 解决 5-9 F1-F5 的状态不一致问题
- 将 6-mindmap 的 3 个文件在 phase 里程碑间做一次已完成/进行中/参考的切分
### 5.3 无需变更的文档
- `06-mindmap/done/6-mindmap-phase6-kmind-parity-detail-checklist-v1.md` — 已经是正确位置
- `08-wolai-aline-test-flow/process/wolai-aline-test-flow-v1.md` — 保持在 process/ 作为流程定义
- `09-siyuan-reference/process/9-siyuan-reference-boundary-and-adoption-v1.md` — 保持在 process/ 作为参考边界稿
---
## 报告说明
**路径**: `.codex/reasonix-tasks/results/design-governance-worker-c-05-06-08-09.md`
**只读约束**:本文只创建了报告文件,未移动、修改或删除任何 `design/` 文件。上述归档/移动建议需由人工或后续 Worker 执行。
**验证命令**
```bash
# 确认报告文件存在
find .codex/reasonix-tasks/results/ -name '*worker-c*'
# 确认未修改任何设计文件
git diff --stat design/
```
@@ -0,0 +1,362 @@
# Worker Ddesign 07/10 审查报告
> 执行时间:2026-05-27
> 范围:`design/07-ai/process/` + `design/10-review/process/`
> 口径:local-first MVP 后阶段统一口径
> 约束:只读审查,不移动/修改/删除 design 文件
---
## 综合结论
审查的 16 个文档中:
| 建议状态 | 数量 | 文档 |
|---------|------|------|
| keep-process | 5 | 7-10, 7-12, 7-14, 7-15, 7-27 |
| mark-process-reference | 6 | 7-17, 7-28, 12, 13, 14, 15 |
| archive-done(建议归档) | 5 | 7-16, 7-30, 7-31, 7-32, 7-33 |
**核心发现**design 07 中 2026-05-16 之前的文档(7-10/12/14/15)均已叠加 local-first 口径修正,无遗留的「BlockNote 默认主链」或「mnote.doc.markdown_edit 唯一主路径」问题。07 最近的 checklist7-30~7-33)大部分已完成,仅余次要项。10-review 的四份 doc 均为已消耗的审查参考。
---
## 逐文档审查
### 1. `design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
**建议状态**`keep-process`
**证据**
- Phase 17Page Aggregate → block.move_after)全部已勾,有真实 smoke 证据(tmp JSON + 截图 + 测试通过记录)[phase-1-7-complete]
- Phase 8 全部未勾:UI / Review Mode 的 4 项均为未完成状态 [phase-8-pending]
- DONE 条件明确要求 Phase 8 未完成即不能归档 [done-condition-unmet]
- 已包含 2026-05-16 和 2026-05-19 两轮口径修正,对齐 local-first 文件编辑主线 [section-1.0, 1.0.1]
**归档/保留原因**Phase 8 UI / Review Mode 仍冻结(Phase C 政策),但 Phase 1–7 的实打实执行证据在此文档中集中记录。如果强行归档到 done/,则 Phase 8 的跟踪上下文丢失。
**建议补充说明**:无。
---
### 2. `design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
**建议状态**`keep-process`
**证据**
- Phase A(设计治理)已勾 [phase-a-done]
- Phase BManifest 合同收口)全部未勾 [phase-b-open]
- Phase Cblock-edit-workflow 改造)全部未勾 [phase-c-open]
- Phase DReview session)部分已勾(schema 定义),UI 未勾 [phase-d-partial]
- Phase E(状态统一)全部未勾 [phase-e-open]
- Phase F(验收 smoke)除 selection guard 外全部未勾 [phase-f-open]
- 2026-05-18 已补充 local-first 口径修正 [section-0]
**归档/保留原因**:定义了当前 AI 编辑的完整架构方向——local-first 文件编辑为主、mnote 兼容工具为辅。Phases BF 是后续执行 roadmap。
**建议补充说明**Phase C 与 Phase C policyPhase C 冻结)存在矛盾——本文 Phase C 要求将 block-edit-workflow 改造成 router,但统一口径要求「不要恢复 page-ai fast-path」。建议在本文 Phase C 开头标注「当前仍冻结,待 Phase C 解冻后实施」。
---
### 3. `design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md`
**建议状态**`keep-process`
**证据**
- 当前产品默认形态已切到 local-first workspace [section-0-update]
- 明确两层模型:agent 原生 patch/diff 为主、mnote.doc.markdown_edit 为 compat fallback [section-1-conclusion]
- Phase A(当前立即)有 5 项,Phase BPhase A 后)、Phase A/B 后补充、Phase C(远期)均未完成 [section-8-phases]
- CLI Main 参考分析深入,已转化为采纳清单 [section-2.1]
**归档/保留原因**:本文是最全面定义「两层 AI 编辑模型」的设计文档,是当前本地 vs 在线编辑口径的权威来源。Phase A 的部分实现(scope/detail/markdown 优先)仍待执行。
**建议补充说明**:无。
---
### 4. `design/07-ai/process/7-15-page-ai-acp-agent-runtime-unified-layer-v1.md`
**建议状态**`keep-process`
**证据**
- 17 步执行计划中 step-1 到 step-7 已被后续 checklist7-30 到 7-33)覆盖 [section-9-low-friction]
- step-8 到 step-17 仍待实施(Reasonix wrapper、前端运行时选择器、存量迁移、旧代码退役)[steps-in-progress]
- 不包含勾选状态,是架构设计而非 checklist [no-checkboxes]
- 已包含 2026-05-18 local-first 口径补充 [section-1.2]
**归档/保留原因**:ACP 统一架构的实施蓝图。后续 checklist7-30~7-33)已覆盖前 7 步,但 step-8 之后仍需此文档指导。
**建议补充说明**:建议标注哪些步骤已被 7-30~7-33 覆盖,以便新 reader 快速定位执行状态。
---
### 5. `design/07-ai/process/7-16-page-block-ai-real-smoke-followup-matrix-v1.md`
**建议状态**`archive-done`
**证据**
- 3.1 到 3.7 全部已完成,有真实 JSON 和截图证据 [sections-3.1-to-3.7-done]
- 3.8UI / Review Mode 边界)全部未勾 [section-3.8-open]
- 本文开头声明「不新增 AI 功能面」——3.8 是唯一剩余项,且已被 Phase C 政策冻结 [scope-statement]
- 冻结策略明确写于 7-10 和 7-12:「Phase C 仍冻结,不实施流式 apply 或新的审阅 UI」[phase-c-frozen]
**归档/保留原因**:本文范围内的所有可执行 smoke 项目已完成。剩余的 3.8(4 项)属于 Phase C 范畴,当前已被统一口径冻结。继续留在 process/ 会产生「还有事要做」的误导,但实际上这些项不能在当前阶段执行。
**建议补充说明**:由于本 worker 只读不移动文件,此建议需人工确认后执行 `mv`。若 3.8 解冻,需重新打开此文档或创建新 checklist。
---
### 6. `design/07-ai/process/7-17-acp-session-convex-sharing-contract-v1.md`
**建议状态**`mark-process-reference`
**证据**
- 文档自标「状态:PROCESS / FUTURE」[status-future]
- Phase A(约束固化)已完成「ACP 默认主链可用、账号作用域写入 Convex、旧 HTTP 主链进入 recycle」[phase-a]
- Phase B–E 全部标记为项目基本完成后的远期计划 [phases-b-c-d-e]
- 本文冻结 API 草案形状,不要求当前实施 [section-6-api-draft]
**归档/保留原因**Phase A 已完成。Phase B–E 是项目基本完成后的产品化功能,不改变当前架构方向。保留在 process/ 作为合同参考。
**建议补充说明**:无。
---
### 7. `design/07-ai/process/7-27-online-markdown-writeback-final-content-truth-v2.md`
**建议状态**`keep-process`
**证据**
- 本文是针对 7-24/7-25 已知缺陷的修复设计 [bug-references]
- 详细实施计划:6 个步骤(增强 blocks_to_markdown → 实现 parse_final → 实现 build_page_content → 替换写回路径 → 更新 full_content → 清理死代码)[implementation-plan]
- 新增 5 个测试矩阵 P0/P1 级 [test-matrix]
- 没有代码已完成的证据:本文是设计稿,代码尚未实施 [no-code-evidence]
**归档/保留原因**:是当前活跃的待实施修复。`mnote.doc.markdown_edit` 在线文档路径仍然以「二次推导」而非「最终 markdown」为写回真源,7-27 是改正此问题的方案。
**建议补充说明**:高优先级——如果不实施,7-24/7-25 修复后的在线 markdown_edit 在 full_content 和多 operation 场景仍然受限。
---
### 8. `design/07-ai/process/7-28-resource-ai-tool-contract-v1.md`
**建议状态**`mark-process-reference`
**证据**
- 定义 mindmap / office 工具合同,无勾选状态 [no-checkboxes]
- 无实施记录或代码证据 [no-code-evidence]
- 不属于当前 AI 编辑主路径(主路径是文件编辑 + ACP session[outside-main-path]
**归档/保留原因**:保留在 process/ 作为远期资源工具设计的参考合同。当前不需要实施——mindmap 和 office 的 AI 操作尚未成为产品优先级。
**建议补充说明**:无。
---
### 9. `design/07-ai/process/7-30-acp-session-load-resume-checklist-v1.md`
**建议状态**`archive-done`
**证据**
- 6/7 项已勾:load_session、ensure_session、ACP run 优先尝试 loaded session、mock 测试覆盖 [checklist-checks]
- 仅 1 项未完成:「明确区分 MNote session id 与 ACP adapter session id;需要持久化/返回时字段名不能混淆」[remaining-item]
- 执行记录:2026-05-20 Reasonix 实施 + Codex 复核通过 [exec-record]
- 验收命令全部通过 [verification-commands]
**归档/保留原因**:核心功能(session/load → ensure_session 流程)完整实现并通过 Codex 复核。剩余项是 session id 字段全链路持久化问题,不属于功能性缺陷,适合作为后续产品化工作的一部分跟踪。
**建议补充说明**:由于本 worker 只读不移动文件,此建议需人工 `mv``design/07-ai/done/`
---
### 10. `design/07-ai/process/7-31-acp-permission-decision-loop-checklist-v1.md`
**建议状态**`archive-done`
**证据**
- 文档自标「状态:done」[status-done-header]
- 6/7 项已勾:incoming handler、pending map、SSE permission.requested、HTTP endpoint resolve、超时 deny [checklist-checks]
- 仅 1 项未完成:「补 mock ACP 测试:incoming request 等待决策、allow 返回 result、deny 返回 result/error、timeout deny」[remaining-item]
- 执行记录 + Codex 修正记录详细 [exec-record]
**归档/保留原因**:真实 permission 闭环(前端 allow/deny → 后端 JSON-RPC response)已完整实现。剩余 end-to-end mock 测试是测试覆盖的增强项,不妨碍功能完整性。
**建议补充说明**:虽然文档自标 done,但未完成项可能会导致后续重构时缺少回归保护。建议归档时在备注中标注此测试缺口。
---
### 11. `design/07-ai/process/7-32-acp-tool-location-and-open-action-checklist-v1.md`
**建议状态**`archive-done`
**证据**
- 6/7 项已勾:ToolLocation 结构、locations SSE、tool card 展示、打开按钮 [checklist-checks]
- 代码层已通过 Codex 复核 [exec-record]
- 仅 1 项未完成:「补测试覆盖 locations parse、SSE 数据、前端字符串断言或 smoke」[remaining-item]
**归档/保留原因**ACP tool locations 解析、SSE 桥接、前端 tool card 打开动作完整实现。剩余 browser smoke 是验证性补充,核心功能已就绪。
**建议补充说明**:无。
---
### 12. `design/07-ai/process/7-33-acp-session-info-plan-ui-checklist-v1.md`
**建议状态**`archive-done`
**证据**
- 5/6 项已勾:session.info.updated SSE、plan.updated SSE、前端 title 更新持久化、plan 面板渲染、bridge 单测 [checklist-checks]
- 仅 1 项未完成:「tool raw output 中包含 todo JSON 时,可以复用 plan 区域或 tool card 状态显示」[remaining-item]
- 执行记录 + Codex 复核通过 [exec-record]
**归档/保留原因**ACP session info/plan 的完整桥接+UI 链路已实现。剩余 todo JSON 解析项因格式未确认而暂缓,不影响 session 自动标题和 plan 面板功能。
**建议补充说明**:无。
---
### 13. `design/10-review/process/12-sidex-vscode-workbench-gap-review-v1.md`
**建议状态**`mark-process-reference`
**证据**
- 审查发现的 P0/P1/P2 缺口已拆分为执行 checklist5-16、5-17、4-38、5-18、5-19、4-39、4-40、5-20、4-41、4-42、5-21、4-43、5-22、5-23、4-44、4-45[execution-split]
- 本文本身不再包含待执行项目 [no-remaining-items]
- 「不建议照搬」部分已明确 MNote 的吸收边界 [section-7]
**归档/保留原因**:已完成审查输出——缺口分析已有对应执行文档跟进。本文保留作为 Sidex 对照原始证据和决策记录。
**建议补充说明**:无。
---
### 14. `design/10-review/process/13-hermes-vscode-plugin-gap-review-v1.md`
**建议状态**`mark-process-reference`
**证据**
- 审查发现的 P0 缺口已拆分为 7-30、7-31、7-32、7-33(均已实施完成)[execution-split-7-30-33]
- P1/P2 缺口未拆分,但列为后续执行参考 [p1-p2-open]
- 本文本身无待执行勾选 [no-checkboxes]
**归档/保留原因**:审查产出的 4 份 P0 checklist 均已实施。本文保留作为 Hermes VSCode 插件对照的原始证据和 P1/P2 参考。
**建议补充说明**:P1/P2 如果后续拆分执行,建议更新本文关联或创建新 review。
---
### 15. `design/10-review/process/14-sidex-workbench-gap-followup-review-v1.md`
**建议状态**`mark-process-reference`
**证据**
- 审查发现的缺口已拆分为执行 checklist5-24、5-25、5-26、4-45[execution-split]
- 明确列出「不建议继续照搬」[section-4]
- 本文本身无待执行项 [no-remaining-items]
**归档/保留原因**:同 12——审查已输出到执行 checklist。
**建议补充说明**:无。
---
### 16. `design/10-review/process/15-design-governance-mvp-post-review-v1.md`
**建议状态**`mark-process-reference`
**证据**
- 本文是当前的 governance 框架文档,定义了本次审查的范式 [meta-document]
- Workers A/B/C/D 由此散发 [section-3]
- 不包含待执行代码项 [no-code-items]
**归档/保留原因**:是本次治理审查的元文档和框架定义。如果归档,将失去当前治理周期的 root 文档。
**建议补充说明**:建议在所有 Worker 报告完成后,将本文与各 Worker 报告一同作为 governance 周期快照归档到 `design/10-review/done/``design/10-review/process/` 之内保留。
---
## 最高风险项
按风险从高到低:
### 🔴 H17-27 尚未实施(keep-process
`mnote.doc.markdown_edit` 的在线文档写回路径仍然以「二次推导」而非「最终 markdown」为真源。7-24/7-25 已修复拒绝写入的安全问题,但 full_content 和多 operation 场景的正确性仍未达到设计目标。如不尽快实施,已有 bugfix 的保护措施效果有限,且代码与设计文档的差距会随时间扩大。
**建议**:优先安排 7-27 的 6 步实施计划。
### 🟡 H27-10 Phase 8 冻结与后续解冻风险(keep-process
Phase 17 全部完成,但 Phase 8UI / Review Mode)因 Phase C 政策冻结。如果后续「冻结」解除,接手者需要从零启动——当前 design 文档不承诺 UI 形状、不包含 mockup、不定义用户交互路径。7-16 3.8 的 4 项未勾项同样属此问题。
**建议**:在 Phase C 解冻前标记「Phase 8 实施时需新建设计文档,当前只保留缺口清单」。
### 🟡 H37-15 执行计划分化(keep-process
17 步计划中只有前 7 步被 7-30~7-33 覆盖实施。step-8 之后(Reasonix ACP wrapper、前端运行时选择器、存量迁移、旧代码退役)仍然没有对应的 checklist 或代码证据。若 ACP 架构的目标是「Hermes 与 Reasonix 可互换」,则后续步骤仍需要推动。
**建议**:创建 7-34 ~ 7-36 覆盖 Reasonix wrapper、前端选择器和存量迁移。
### 🟢 H47-30/31/32/33 剩余未完成项(archive-done
这些 checklist 的核心功能已完成,但各有 1 项未完成:
- 7-30session id 全链路持久化
- 7-31end-to-end mock 测试缺口
- 7-32browser smoke 未补
- 7-33todo JSON 格式解析
**建议**:归档后将这些剩余项汇总到统一 tracker(如 bugs/07-ai/process/ 或 project board),避免分散丢失。
---
## 无过期口径声明
检查所有文档后确认:**没有文档仍在将 `mnote.doc.markdown_edit``page-ai/block-edit-workflow` 描述为 local-first 普通 Markdown 编辑的唯一/默认主路径。** 2026-05-16 之后的各轮口径修正已正确覆盖:
- 7-10 §1.0.1:「local-first 普通正文编辑默认给 Hermes / Reasonix 当前文件引用」
- 7-12 §0:「local-first 普通 Markdown 编辑默认给 Hermes / Reasonix 授权文件引用」
- 7-14 §0:「local-first 普通 Markdown 编辑优先给 agent 授权文件引用」
- 7-15 §1.2:「local-first 普通 Markdown 编辑默认不要求 runtime 调 mnote.doc.markdown_edit」
- 7-16 §1:「local-first 普通 Markdown 编辑主路径仍是授权文件引用 + agent 原生 patch/diff + watcher 同步」
- 7-27 §0:「本地 .md 路径的默认 AI 编辑主路径是授权文件引用 + agent 原生 patch/diff + watcher 同步」
- 7-17、7-28、7-30~33 不涉及编辑主路径定义
---
## 归档建议汇总(需人工执行)
以下文件建议由人工移动到 `done/` 目录:
```bash
# 07-ai 已完成 checklist
mv design/07-ai/process/7-16-page-block-ai-real-smoke-followup-matrix-v1.md design/07-ai/done/
mv design/07-ai/process/7-30-acp-session-load-resume-checklist-v1.md design/07-ai/done/
mv design/07-ai/process/7-31-acp-permission-decision-loop-checklist-v1.md design/07-ai/done/
mv design/07-ai/process/7-32-acp-tool-location-and-open-action-checklist-v1.md design/07-ai/done/
mv design/07-ai/process/7-33-acp-session-info-plan-ui-checklist-v1.md design/07-ai/done/
```
10-review 的四份文档保留为参考(mark-process-reference),不归档。
---
## 验证命令执行摘要
```bash
# 1. 文件列表验证
find design/07-ai/process design/10-review/process -maxdepth 1 -type f -name '*.md' | sort
# → 16 files confirmed
# 2. 高危模式搜索
rg -n "mnote\\.doc\\.markdown_edit.*主路径|page-ai.*唯一|BlockNote.*默认" \
design/07-ai/process design/10-review/process
# → 无匹配:所有文档已更新口径
```
---
## 文件范围
本报告写入 `.codex/reasonix-tasks/results/design-governance-worker-d-07-10.md`
未修改 design/ 下任何文件。未提交 git。未运行浏览器 smoke。
+2 -1
View File
@@ -3,6 +3,7 @@
## 当前主线
- 当前长期方向以 `tree-first graph kernel` 为准,不以 `BlockNote-first``Mindmap-first` 为准。
- 当前已经进入 local-first MVP 后阶段:最小可用闭环已建立,后续重点是底座统一、compat 瘦身和产品化闭环,不再把新增能力建立在旧 Convex / Next / BlockNote 主链上。
- 当前产品形态固定为:`VSCode 简化版工作区 + tiptap 的 Markdown 前端编辑器 + Hermes/Reasonix agent + simplemindmap/office 插件 + Wolai 主题 Web 壳 + 鉴权控制面`
- 本地文件夹是早期产品默认数据真相;本地 `.md` 是页面正文真相。`Convex` / 服务端降级为 auth、membership、share grants、sync state、AI policy、cloud source、compat 和 sync replica 控制面,不再作为新增能力的默认正文、附件或 AI 会话全文主存储。
- `Rust kernel` 持有树、子树、边、projection、query、command 的语义主导权;新增树规则不要继续散落到前端、Next route 或临时 compat 层。
@@ -12,7 +13,7 @@
- `3000` 当前主壳已接入 Rust Web WebSocket push 主链(`/api/realtime/ws`+ SSE fallback`/api/tree/events`)的 snapshot / delta / resync consumer,并已有 browser smoke 验证(2026-05-17 WS 迁移 `57ec8322`);后续收口重点是统一 live cache 与减少补偿链,而不是把它描述成“还没接 live stream”。
- 文档页默认主编辑器已切到页面内 `leptos-tiptap` island`BlockNote` 已退出文档页默认主路径,只保留为历史参考实现 / 对照材料。
- Page Aggregate 当前已输出 `blockDocument / blockProjectionVersion / projectionSource``projectionSource=documents.content`),标题/正文/页面设置写入后 smoke 已通过(`task110``task-page-aggregate-body-sync-smoke``task-page-aggregate-options-sync-smoke`);页面/块 AI 最小工具链已通过 Rust Hermes tools 读取和写入块投影;但这仍是从 `documents.content` / local markdown content 投影出来的过渡态,不是 EditorBlockDocument 原生落库完成态。客户端 `PageAggregateClientState` reducer 仍在,页面域单一真源未完全闭环。
- 当前最优先的架构收口不是继续扩编辑器 UI,而是 local-first 工作区产品化`Page Aggregate` 单一真源收口、`tree command cutover` 收尾、`tree realtime event stream` live cache 统一(WS push 主链 2026-05-17 上线,SSE 降级为 fallback)。AI 侧普通 Markdown 编辑主路径已收口为“当前文件引用 + selection + allowed roots + agent 原生 patch/diff + watcher 同步”;`mnote.doc.fetch``mnote.doc.markdown_edit``mnote.doc.apply_block_ops``mnote.block.*` 只作为 cloud / remote agent / compat fallback 或复杂结构辅助;`review session`/流式 apply 属于 Phase C(设计冻结,当前不实施),不扩新 AI 功能,不把粗粒度 `mnote.page.save` 当成精确块编辑主入口。
- 当前最优先的架构收口不是继续扩编辑器 UI,而是 local-first MVP 基线上推进 `WorkspacePath/ObjectIdentity` runtime 消费统一、`DocumentBuffer/BufferStore``Page Aggregate` 单一真源收口、`tree command cutover` 收尾、`tree realtime event stream` live cache 统一(WS push 主链 2026-05-17 上线,SSE 降级为 fallback和 agent diff / 冲突合并 / 本地索引 / 分享同步产品化。AI 侧普通 Markdown 编辑主路径已收口为“当前文件引用 + selection + allowed roots + agent 原生 patch/diff + watcher 同步”;`mnote.doc.fetch``mnote.doc.markdown_edit``mnote.doc.apply_block_ops``mnote.block.*` 只作为 cloud / remote agent / compat fallback 或复杂结构辅助;`review session`/流式 apply 属于 Phase C(设计冻结,当前不实施),不扩新 AI 功能,不把粗粒度 `mnote.page.save` 当成精确块编辑主入口。
## 组件定位
+4 -4
View File
@@ -1,10 +1,10 @@
# MNOTE 当前架构梳理
> 更新时间:2026-05-19
> 更新时间:2026-05-21
>
> 2026-05-19 口径更新:产品形态已切换为 local-first workspace;本地文件夹是默认数据真相,Convex / 服务端降级为账号、分享、同步、协作和 AI 隔离控制面。相关设计已完成并迁入 `design/02-convex-rust-long-term-architecture/done/2-2-local-first-workspace-convex-control-plane-v1.md`。本地 Markdown 图片与附件上传已新增 `/api/local-folder/assets/upload`,页面内上传默认写入 `{mdBase}.assets/` 并保存相对 Markdown 路径,不再走 Convex media asset。
> 2026-05-21 口径更新:产品形态已切换为 local-first workspace,且初步 MVP 已建立;本地文件夹是默认数据真相,Convex / 服务端降级为 auth、membership、share grants、sync state、AI policy、cloud source、compat 和 sync replica 控制面。相关设计已完成并迁入 `design/02-convex-rust-long-term-architecture/done/2-2-local-first-workspace-convex-control-plane-v1.md`。本地 Markdown 图片与附件上传已新增 `/api/local-folder/assets/upload`,页面内上传默认写入 `{mdBase}.assets/` 并保存相对 Markdown 路径,不再走 Convex media asset。
>
> **当前阶段:local-first MVP 骨架已达成。** 3000 下文档页、Sidebar/File Tree/Page Tree 三层树模型、tree command`tree.*` preferred)、tree realtime WS 主链 / SSE fallback、本地 Markdown 上传、AI 会话本地化与 VSCode-like agent 运行口径均已建立。剩余工作集中在管理员目录授权、VSCode-like 冲突合并 UI、agent diff 审计、本地索引 / 分享 / 同步闭环,而不是继续扩张专用 page-ai fast-path 或 Convex 主存储链。
> **当前阶段:local-first MVP 后阶段。** 3000 下文档页、Sidebar/File Tree/Page Tree 三层树模型、tree command`tree.*` preferred)、tree realtime WS 主链 / SSE fallback、本地 Markdown 上传、上传下载、AI 会话本地化与 VSCode-like agent 运行口径均已建立。剩余工作集中在 `WorkspacePath/ObjectIdentity` runtime 消费统一、`DocumentBuffer/BufferStore`、Page Aggregate compat 瘦身、tree command context、live cache 统一、VSCode-like 冲突合并 UI、agent diff 审计、本地索引 / 分享 / 同步闭环,而不是继续扩张专用 page-ai fast-path 或 Convex 主存储链。
本文只描述当前仓库中真实成立的主线结构,以及当前最优先的架构收口点。
@@ -46,7 +46,7 @@
- `/mnt/Data1T/mnote/rust/crates/mnote-web/`
Rust Web route、kernel projection、documents/search/mindmap shell、Hermes bridge、stream transport、compat,以及显式开启时的 debug shell;它是当前主 Web gateway / shell / transport owner。
- `/mnt/Data1T/mnote/recycle/wolai-frontend/convex/`
已随 wolai-frontend 移入 recycle。当前活跃 Convex functions 位于 `infra/convex/` 与 Rust bridge 中
已随 wolai-frontend 移入 recycle,不允许作为当前 Convex deploy fallback。当前 active Convex functions 部署源在仓库根 `convex/`(现阶段为 ACP / Hermes runtime session store`schema.ts` + `aiSessions.ts`);`infra/convex/` 只负责自托管 Convex 基础设施
- `/mnt/Data1T/mnote/infra/convex/`
Convex 自托管部署与基础设施。
- `/mnt/Data1T/mnote/wolai-backend/`
+33 -11
View File
@@ -1,8 +1,8 @@
# 当前完整架构
> 更新时间:2026-05-19
> 更新时间:2026-05-21
>
> 范围:`/mnt/Data1T/mnote` 当前可见实现、主线口径、历史退役边界和后续功能缺口。
> 范围:`/mnt/Data1T/mnote` 当前可见实现、local-first MVP 后阶段主线口径、历史退役边界和后续功能缺口。
## 1. 当前产品形态
@@ -12,6 +12,7 @@
这意味着:
- 初步 MVP 已建立,后续不是继续证明 Rust Web、local-first 或 tiptap island 是否成立,而是收紧底座、减少 compat、补齐产品化闭环。
- 本地 workspace folder 是早期产品默认数据真相。
- 本地 `.md` 文件是页面正文真相;Page Aggregate、tiptap state、AI context 都是投影或工作副本。
- Rust kernel / projection / command 持有树、页面、资源和权限语义。
@@ -61,42 +62,63 @@
- `/api/page-ai/block-edit-workflow` 不再作为 local-first 普通正文编辑主路径。
- AI session 全文在 local source 下默认写入 `ai-sessions/private/*.jsonl``ai-sessions/shared/<share-id>/*.jsonl`Convex 只保存必要 metadata / audit / sync replica。
## 3. 已完成收口
## 3. 已完成 MVP 收口
- Local-first workspace 上位设计和 checklist 已完成,迁入 `design/02-convex-rust-long-term-architecture/done/2-2-*`
- 3000 下文档页、Sidebar/File Tree/Page Tree、tree command preferred path、tree realtime WS 主链 / SSE fallback、本地上传下载、AI 会话本地化与 VSCode-like agent 运行口径已建立。
- `cargo fmt --check --all``local_folder``hermes_tools`、local/shared AI session、local-first Convex guard、Convex export fixture、local asset upload smoke、external-change conflict smoke 均已作为 `2-2` 验收证据记录。
- 本地上传链路已避免 Convex media asset,落盘相对 Markdown 链接。
- 本地 AI 会话已区分 private / shared / cloud,并在 UI 上显示来源。
- Convex guard 已阻止 active 路径重新引入未标注的 Convex documents / media / aiSessions 默认主存储口径。
## 4. 仍在推进的功能缺口
## 4. MVP 后阶段功能缺口
1. **管理员目录授权 UI / API**
1. **WorkspacePath / ObjectIdentity runtime 消费统一**
- 继续把本地路径、资源 identity、页面正文 `.md`、附件、mindmap、office 资源归属统一到 kernel / projection contract。
- 防止 UI、compat route 或临时文件逻辑重新生成第二套资源真相。
2. **DocumentBuffer / BufferStore**
- 让 tiptap、文件 watcher、agent 写入、冲突检测和页面 tab 共用打开态与文件版本仲裁。
- 避免 dirty editor、资源 tab、文件树点击之间各自维护状态。
3. **Page Aggregate compat 瘦身 / kernel-native projection**
- 继续退役 legacy `body.content` / compat join 边界。
- local-first 正文真相保持为 `.md` 文件,Page Aggregate 只输出稳定投影。
4. **tree command context / context key**
- 菜单、快捷键、按钮、AI 写入能力的 enablement 判断应收口到同一套 command context。
- 文件树和主编辑器右键菜单不能继续各自硬编码。
5. **tree live cache 统一**
- 把 local-folder watcher、projection refresh、WS/SSE snapshot/delta/resync 收进同一 live cache。
- 减少 polling 或重复补偿链。
6. **管理员目录授权 UI / API**
- 需要把 `access-policy.json` 和当前 auth actor、admin grant、read/write/share 权限做成可管理界面。
- 管理员可授权任意 canonical 目录;普通用户只能访问自己的受管 my-space 或被授权目录。
2. **VSCode-like 冲突处理 UI**
7. **VSCode-like 冲突处理 UI**
- 当前已有 clean/dirty watcher 行为和冲突态 smoke。
- 还需要做可用的“接受磁盘版本 / 保留当前版本 / 打开 diff 合并”交互。
3. **agent 写入审计**
8. **agent 写入审计**
- local-first agent 应回收 changed files、diff summary、tool/run id、actor、workspace root、permission level。
- audit 默认本地落盘,同步开启时再上报控制面。
4. **本地全文搜索、引用和索引**
9. **本地全文搜索、引用和索引**
- 本地 `.md` workspace 需要独立索引:全文搜索、反链、页面引用、资源引用、标签。
- 不能依赖 Convex search 才能搜索本地工作区。
5. **分享与同步闭环**
10. **分享与同步闭环**
- 需要把 share grants、shared workspace cache、shared AI session、只读/可写权限和冲突处理连成产品级闭环。
- share grant 是产品层共享授权,local access grant 是本机目录授权;显式分享不能自动扩大本机 filesystem root 权限。
- 控制面不可用时不得扩大本地缓存权限。
6. **插件资源模型**
11. **插件资源模型**
- simplemindmap / office 应作为 Resource Tree 对象打开和保存。
- Markdown 中只保留链接或嵌入引用,不把复杂对象强塞进普通正文块。
7. **旧 Convex 数据迁移产品化**
12. **旧 Convex 数据迁移产品化**
- 当前已有 fixture/offline 导出脚本。
- 后续需要真实 Convex workspace 导出入口、迁移进度、冲突报告、回滚/备份策略。
+17 -5
View File
@@ -1,11 +1,20 @@
# MNOTE — Reasonix working knowledge
## Current Baseline
- MNote 已进入 **local-first MVP 后阶段**:最小可用闭环已建立,后续重点是底座统一、compat 瘦身和产品化闭环。
- 产品形态固定为:`VSCode 简化版工作区 + tiptap 的 Markdown 前端编辑器 + Hermes/Reasonix agent + simplemindmap/office 插件 + Wolai 主题 Web 壳 + 鉴权控制面`
- 本地 workspace folder 是默认数据真相;本地 `.md` 是页面正文真相;Page Aggregate、tiptap state、AI context 都是投影或工作副本。
- Rust kernel / projection / command 持有树、页面、资源和权限语义;新增树规则不要散落到前端、Next route 或临时 compat 层。
- Convex / 服务端不再是默认正文、附件、AI 会话全文主存储,只保留 auth、membership、share grants、sync state、AI policy、cloud source、compat 和 sync replica 控制面。
- `mnote-web` 是当前 3000 owner;旧 Next / React / wolai-frontend 已移入 `recycle/`,默认不作为当前实现依据。
## Stack
- **Rust** — workspace of 11 crates, edition 2021, resolver 2, wasm32 target (`rust/rust-toolchain.toml`)
- **Axum 0.8** — HTTP/WebSocket server in `mnote-web` crate
- **Leptos 0.8.14** — SSR island rendering in `mnote-web`
- **Convex 1.x** — self-hosted document/real-time/storage backend (`infra/convex/docker-compose.yml`)
- **Convex 1.x** — auth/share/sync/ACP/Hermes control plane and optional cloud/compat/sync replica, not the default document/media/session full-text store
- **Playwright** — browser smoke tests in `scripts/task-*.js`
## Layout
@@ -14,10 +23,10 @@
|------|----------|
| `rust/crates/mnote-web/` | Rust web gateway (axum + Leptos SSR, `:3000`) |
| `rust/crates/core-protocol/` | Kernel types, projection protocol, tree/graph terms |
| `rust/crates/bridge-runtime/` | Kernel query/command, Convex transport args generation |
| `rust/crates/bridge-runtime/` | Kernel query/command, projection bridge, source normalization |
| `rust/crates/mnote-cli/` | CLI tool (`sidebar dataset`, `page get/create/title/save/move`, `block insert/patch`) |
| `rust/crates/storage-convex-bridge/` | Convex storage bridge for Rust |
| `convex/` | Convex schema (`schema.ts`) + functions (`aiSessions.ts`) |
| `rust/crates/storage-convex-bridge/` | Convex compat / control-plane bridge for Rust |
| `convex/` | Active Convex deploy source for ACP / Hermes runtime state (`schema.ts` + `aiSessions.ts`) |
| `infra/convex/` | Docker Compose for self-hosted Convex backend |
| `infra/onlyoffice/` | Docker Compose for OnlyOffice |
| `scripts/` | Node.js dev/build scripts + Playwright smoke tests |
@@ -52,9 +61,11 @@
## Conventions
- **Local-first default**: start from Rust `mnote-web`, LocalFS/local_folder projection, Page Aggregate, File Tree, and Resource Tree. Use Convex paths only for auth/cloud/share/ACP/compat-specific work.
- **Smoke test pattern**: standalone Playwright scripts under `scripts/task-*.js` using a shared harness (`ensureAuthenticated`, `createTempDocument`, `cleanupDocuments`). All use `"use strict"` and `require("playwright")` (`scripts/task110-page-title-single-truth-smoke.js:1-4`).
- **Rust workspace**: edition 2021, resolver "2", MIT license. All crates inherit workspace version (`rust/Cargo.toml:2-8`).
- **Convex boundary**: root `convex/` is the active deploy source for ACP / Hermes runtime state (`schema.ts` + `aiSessions.ts`). Do not deploy from `recycle/wolai-frontend/convex`; old `documents` / `mediaAssets` / `users` / `workspaces` functions must be explicitly migrated to active code, moved to an auxiliary area, or retired.
- **AI editing path**: local-first Markdown editing should prefer `currentFile + selection + allowedRoots / aiAccessScope + agent native patch/diff + watcher sync`. `mnote.doc.*` / `mnote.block.*` are cloud, remote, compat, or complex-structure helpers.
- **Bug tracking**: bugs organized by design domain directory (e.g. `bugs/04-tree-domain/process/`), moved to `done/` when fixed (`bugs/README.md`).
- **Design docs**: organized by domain in `design/`, with `process/` (in-progress) and `done/` (completed) subdirs (`design/README.md`).
@@ -63,6 +74,7 @@
- **write_file path rule**: paths are sandbox-relative; leading `/` is stripped. Use `path: "design/..."` not `/mnt/Data1T/mnote/design/...`.
- **`.aionrs/`, `.claw/`, `.codex/`, `.gemini/`** — session data from other AI tools, not project code. Don't read or edit.
- **`recycle/`** — retired code, not current implementation. Don't use as evidence for current behavior.
- **Convex must be running** (`infra/convex/`) for full-stack dev — smoke tests depend on a live Convex backend.
- **Convex availability**: local-folder / Rust projection smoke should not require Convex unless the test explicitly covers auth, cloud source, share grants, ACP/Hermes runtime state, or sync replica behavior.
- **MVP 后阶段优先级**: prefer WorkspacePath/ObjectIdentity runtime consumption, DocumentBuffer/BufferStore, Page Aggregate compat slimming, tree command context/context key, live cache unification, agent diff audit, conflict merge, local index, and share/sync productization.
- **Rust wasm32 target** required (`rust/rust-toolchain.toml`), needed for `tree-shell-runtime-wasm` crate.
- **`rust/target/`** is gitignored and large — `cargo build` runs from scratch if cache is missing.
+1
View File
@@ -0,0 +1 @@
Total messages: 0 (Errors: 0, Warnings: 0)
View File
@@ -0,0 +1,64 @@
# 4-46 FileTree / Editor Context Menu Download v1
> 状态:done
>
> 日期:2026-05-21
>
> OwnerFileTree action layer / editor attachment context menu
## 1. 问题
- 文件树 asset/resource 行右键菜单没有 `下载` 动作,虽然后端已有 `/api/local-folder/files/open?download=true` 下载入口。
- 主编辑器附件当前主要依赖 hover 后的更多按钮打开附件菜单,真实右键附件链接没有复用同一套菜单。
- 本地文件下载响应只有 `Content-Disposition: attachment`,缺少 filename,浏览器可能无法使用原文件名保存,中文文件名尤其不稳定。
## 2. 目标
- 文件树 asset 右键菜单显示 `下载`
- 点击文件树 asset 下载时复用现有 attachment 下载链路,并记录 `data-mnote-filetree-last-action=download` / `data-mnote-filetree-last-action-status=requested`
- 文件树已选中多个 asset/resource 时,右键任一已选文件执行 `下载` 会批量下载当前选择集中的资源文件。
- 文件树主 Markdown 文件行和本地文件夹行也应支持下载;文件夹下载为 `.tar` 包。
- 文件拖到本地文件树文件夹时,应上传到该文件夹路径,不应因缺少 `rootUri``documentId` 报错。
- 主编辑器附件右键打开附件 context menu,菜单内容与 hover 更多按钮一致。
- 本地文件下载响应带 `filename``filename*`,支持中文文件名。
## 3. Checklist
- [x] 文件树 asset 菜单增加 `download` 动作。
- [x] `handleTreeContextMenuAction` 处理 filetree asset `download`
- [x] `Ctrl/Cmd` 多选和 `Shift` 范围多选后支持文件树 asset 批量下载。
- [x] 文件树主 Markdown 文件行支持下载原始 `.md` 文件。
- [x] 文件树本地文件夹行支持下载 `.tar` 包。
- [x] 文件拖到本地文件树文件夹时使用目标文件夹 `targetRelativePath` 上传。
- [x] 主编辑器附件链接右键打开 attachment menu。
- [x] `/api/local-folder/files/open?download=true` 响应补 `Content-Disposition` 文件名。
- [x] Rust contract / route 测试覆盖新增行为。
- [x] Reasonix 浏览器截图复核右键菜单与下载入口。
- [x] Codex post-fix browser smoke 复核文件树 asset 单文件下载与 Ctrl/Cmd、Shift 多选下载。
## 4. 验收
- `cargo test -p mnote-web sidebar_tree_runtime_renders_context_menu_and_scoped_title_updates -- --test-threads=1`
- `cargo test -p mnote-web local_file_open_download_sets_content_disposition_filename -- --test-threads=1`
- `cargo test -p mnote-web local_folder_open_downloads_directory_as_tar -- --test-threads=1`
- `cargo test -p mnote-web web_shell -- --test-threads=1`
- `node --check scripts/task476-filetree-editor-context-menu-download-smoke.js`
- `node scripts/task476-filetree-editor-context-menu-download-smoke.js`
- Reasonix pre-fix browser result`tmp/reasonix-filetree-editor-context-menu-2026-05-21/result.json`
- Codex post-fix browser result`tmp/task476-filetree-editor-context-menu-download-smoke/result.json`
## 4.1 执行记录
- 2026-05-21Reasonix 浏览器测试确认:文件树 asset 右键菜单缺少 `download`,主编辑器附件 hover/菜单下载可用。
- 2026-05-21:已补 filetree asset `download` 菜单项和 action handler,下载时复用 attachment 下载链并记录 filetree action 状态。
- 2026-05-21:已补文件树多选下载,右键已选资源时按当前选择集批量触发浏览器下载;`task476` smoke 覆盖 `Ctrl` 多选 2 个文件与 `Shift` 范围多选 3 个文件。
- 2026-05-21:已补主 Markdown 文件行下载、文件夹 `.tar` 下载和拖到文件夹上传;`task476` smoke 覆盖 `README.md` 下载、`README.tar` 下载,以及 `README/drag-upload-note.txt` 上传落盘。
- 2026-05-21:已补主编辑器附件链接真实右键菜单,右键打开与 hover 更多按钮一致的 attachment menu。
- 2026-05-21:已补 local-folder open 下载文件名:`Content-Disposition` 同时提供 ASCII fallback `filename` 与 UTF-8 `filename*`
- 2026-05-21`task476` post-fix browser smoke 通过;文件树 asset 单文件下载得到浏览器 suggested filename `report-a.pdf``Ctrl/Cmd` 多选下载得到 `report-a.pdf` / `report-c.pdf``Shift` 范围下载得到 `report-a.pdf` / `report-b.pdf` / `report-c.pdf`。该 smoke 中编辑器附件链接未渲染为可见项,作为本轮文件树多选下载的非阻塞项记录为 `editor_attachment_not_visible`
## 5. 非目标
- 不一次性实现 attachment 菜单中所有历史占位动作。
- 不改变文件树删除语义。
- 不重构完整 command service。
+9 -2
View File
@@ -1,6 +1,11 @@
# 01-05 当前主线与优先级总览
> 更新时间:2026-05-19
> 更新时间:2026-05-21
>
> 2026-05-21 口径补充:
> - 当前已进入 local-first MVP 后阶段:最小可用闭环已建立,后续重点是底座统一、compat 瘦身和产品化闭环。
> - 新设计与 bug 修复默认从 `WorkspacePath/ObjectIdentity`、`DocumentBuffer/BufferStore`、Page Aggregate compat 瘦身、tree command context、live cache 统一、agent diff / 冲突合并 / 本地索引 / 分享同步产品化这些基线出发。
> - 旧 Convex / Next / BlockNote 主链只作为 legacy / recycle / compat 背景,不再作为新增能力默认方向。
>
> 2026-05-18 口径补充:
> - 产品形态已调整为 `Local-first workspace`:本地文件夹是默认数据真相,Rust kernel 是唯一语义真相,Convex / 服务端降级为账号、分享、同步、协作和 AI 隔离控制面。
@@ -108,7 +113,7 @@ P0-P6 已经有最小闭环证据;P7 已完成阶段性瘦身,但还不是
## 3. 当前仍应保留但不在第一线的 process
下面这些文档仍然有效,但当前不应排在第一优先:
下面这些文档仍然有效,但当前不应排在第一优先;引用时必须叠加 2026-05-21 的 local-first MVP 后阶段口径
- `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-1-tree-first-graph-kernel-checklist-v2.md`
- `/mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md`
@@ -167,6 +172,8 @@ P0-P6 已经有最小闭环证据;P7 已完成阶段性瘦身,但还不是
持续推进 checklist
- `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-3-current-priority-execution-checklist-v1.md`
- MVP 后阶段 process 执行总序:
`/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-8-mvp-post-process-execution-order-v1.md`
## 7. 一句话收口
@@ -1,5 +1,9 @@
# 1-7 Local Workspace Root Page Bundle Checklist v1
> 状态:done
>
> 归档说明(2026-05-21):默认 local workspace root/page bundle 契约、时间戳新页面、同名目录子页面容器与文件树不合并规则均已由 Rust 单测、HTTP smoke 和浏览器验收覆盖。本文件从 `process/` 归档到 `done/`
## 背景
当前默认本地工作区初始化会创建 `pages/``assets/``mindmaps/``ai-sessions/` 等顶层目录。这更像早期产品内部布局,不像用户真实打开的 VSCode 文件夹,也不利于页面导出。
@@ -93,7 +93,7 @@
| `tree.*` 命令切流(Stage 2| DONE | `design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md` |
| 文件树资源行与页面命令分离 | DONE | `bugs/04-tree-domain/done/4-46-filetree-resource-row-document-command-leak-v1.md` |
| 垃圾箱恢复位置记录 + reveal/focus | DONE | `design/04-tree-domain/process/4-28-trash-restore-location-reveal-focus-v1.md` §2 |
| 资源生命周期命令 `tree.resource.*`mindmap/table rename 未完成)| PARTIAL | `design/04-tree-domain/process/4-27-resource-lifecycle-command-cutover-v1.md` |
| 资源生命周期命令 `tree.resource.*`核心 cutover 已完成,局部尾项由后续文档承接)| DONE/PARTIAL tails | `design/04-tree-domain/done/4-27-resource-lifecycle-command-cutover-v1.md`,尾项见 `4-28` / `4-34` / `4-44` / `4-45` |
| 双浏览器 no-refresh(页面 + 文件 + 垃圾箱实时同步)| PROCESS | `design/04-tree-domain/process/4-34-filetree-trash-dual-browser-no-refresh-v1.md` |
| Local/Cloud 桥接 | P3 CANDIDATE | `design/04-tree-domain/process/4-23-local-cloud-explicit-bridge-p3-candidate-v1.md` |
@@ -7,7 +7,7 @@
> 上位依据:
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/done/2-2-local-first-workspace-convex-control-plane-v1.md`
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/process/2-3-local-workspace-access-control-productization-v1.md`
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/done/2-3-local-workspace-access-control-productization-v1.md`
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
> - `/mnt/Data1T/mnote/CURRENT_ARCHITECTURE.md`
>
@@ -0,0 +1,413 @@
# 1-8 [process] MVP 后阶段 process 执行总序 v1
> 创建时间:2026-05-21
>
> 当前状态:`PROCESS`
>
> 目标:把本轮 design governance 后剩余的主线 `process/` 文档排成可执行顺序,避免后续 worker 在 40 份 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`
## 1. 一句话顺序
后续执行顺序固定为:
> **先补阻塞验证和工作区身份底座,再收 Page Aggregate / BufferStore / live cache / command context,随后补资源打开与文件树 action,最后扩 AI、mindmap、Wolai 对标和远期分享同步。**
不要从 `process/` 中按编号顺序机械执行。编号只代表历史产生顺序,不代表当前优先级。
## 2. 状态分类
### 2.1 Active Process
代表仍需继续推进的执行或架构收口文档。
判定规则:
- 有未完成 checklist。
- 或者是当前主线架构入口,仍会指导后续实现。
- 或者有明确阻塞项、smoke 缺口、compat 瘦身缺口。
### 2.2 Process Reference
保留在 `process/`,但不是当前直接执行 checklist。
典型文档:
- 参考实现采用矩阵。
- Wolai / 思源 / Sidex / Hermes 对照证据。
- P3 / future 候选合同。
- 已拆成执行 checklist 的 review 原始证据。
处理规则:
- 不迁入 `done/`,因为不是“当前代码已完成”的实现文档。
- 不当作当前 sprint 的任务源。
- 需要转实施时,新建更小的执行 checklist。
### 2.3 Done / Old
- 已完成且有真实代码、测试、smoke 或明确验收证据:移动到同域 `done/`
- 已被覆盖且口径过时但未完成:移动到 `design/old/<domain>/process/` 并标 `[recycle]`
- 本轮已完成一批 `done/` 归档,详见 `15-design-governance-mvp-post-review-v1.md`
## 3. P0:先清阻塞和验证红灯
这一步先处理“会干扰后续判断”的验证缺口,不扩新产品面。
### P0.1 FileTree action / open editors smoke 前置
入口文档:
- `design/04-tree-domain/process/4-45-filetree-open-editors-lifecycle-link-checklist-v1.md`
- `design/04-tree-domain/process/4-44-filetree-action-layer-paste-drop-delete-v1.md`
执行目标:
1. 修复或明确 `task430-vscode-explorer-stage7-smoke.js` / `task471-local-folder-bulk-resource-trash-smoke.js` 的 workspace bootstrap 前置条件。
2. 把 4-44 中未通过的 browser smoke 跑绿,或把环境阻塞写成可复现 bug。
3. 补 open editor / filetree active row / lifecycle 状态断言。
验收:
- `node --check scripts/task430-vscode-explorer-stage7-smoke.js`
- `node --check scripts/task471-local-folder-bulk-resource-trash-smoke.js`
- 对应 smoke 实跑通过,或形成 `bugs/04-tree-domain/process/` 中明确 bug。
归档条件:
- `4-45` 四项 checklist 完成后归档到 `04-tree-domain/done/`
- `4-44` browser smoke 全绿后归档到 `04-tree-domain/done/`
### P0.2 local folder no-refresh / restore focus
入口文档:
- `design/04-tree-domain/process/4-34-filetree-trash-dual-browser-no-refresh-v1.md`
- `design/04-tree-domain/process/4-28-trash-restore-location-reveal-focus-v1.md`
执行目标:
1. 补 local folder 非 md 资源 trash / restore / purge 的双浏览器 no-refresh 证据。
2. 补 restore 后 DOM focus / reveal 的浏览器证据。
验收:
- 新增或复用 task436 等价 smoke,证明 B 浏览器不刷新能看到 local folder 资源生命周期变化。
- restore location / reveal / focus smoke 截图和 JSON 证据落盘。
归档条件:
- local folder 子项通过后,`4-34` 归档。
- React / Rust 主壳 focus 证据完成后,`4-28` 归档。
## 4. P1:统一工作区身份、BufferStore 和 Page Aggregate
这一步是 MVP 后阶段真正的底座收口。
### P1.1 WorkspacePath / ObjectIdentity
入口文档:
- `design/01-tree-first-graph-kernel/process/1-4-next-phase-execution-roadmap-v1.md`
- `design/01-tree-first-graph-kernel/process/1-5-next-phase-sequential-execution-checklist-v1.md`
- `design/01-tree-first-graph-kernel/process/1-6-next-phase-gap-closure-checklist-v1.md`
- `design/05-editor-mainline/process/5-26-resource-open-resolver-convergence-checklist-v1.md`
执行目标:
1. 统一 page `.md`、raw local file、directory、asset folder、mindmap、office 的 canonical identity。
2. 让 FileTree open target、主编辑器 tab、Open Editors snapshot 和 URL active state 消费同一套 identity。
3. 解决 `5-26` 中“正文附件 open resolver 与 FileTree open resolver 收敛”未闭环问题。
验收:
- `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。
归档条件:
- `5-26` checklist 完成后归档。
- `1-5 / 1-6` 中对应 identity 缺口完成打勾。
### P1.2 DocumentBuffer / BufferStore
入口文档:
- `design/01-tree-first-graph-kernel/process/1-5-next-phase-sequential-execution-checklist-v1.md`
- `design/01-tree-first-graph-kernel/process/1-6-next-phase-gap-closure-checklist-v1.md`
- `design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
执行目标:
1. 让 tiptap、resource tab、watcher、agent 写入和冲突 UI 共用打开态与文件版本仲裁。
2. 避免 dirty editor、资源 tab、文件树点击分别维护状态。
3. 把已有 BufferStore 基础设施接到更多真实 runtime 消费点。
验收:
- `cargo test -p mnote-web document_buffer -- --test-threads=1`
- `cargo test -p mnote-web local_folder -- --test-threads=1`
- 外部修改、dirty buffer、agent writeback、tab 切换 smoke 全部可解释。
归档条件:
- `1-6` 中 BufferStore 运行时接入、浏览器读回、冲突 UI 相关项完成。
### P1.3 Page Aggregate compat 瘦身
入口文档:
- `design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
- `design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`reference
- `design/03-rust-web/process/3-13-rust-web-local-markdown-gfm-ast-parser-migration-v1.md`
执行目标:
1. 收敛 `body.content` / compat join / 临时转换分支。
2. 补 GFM AST parser 写侧尾项:web shell 单测、手写 parser 过渡标记、临时适配分支清理。
3. 明确 `PageAggregateClientState` 哪些仍是合法 draft state,哪些应退出事实源地位。
验收:
- `cargo test -p mnote-web local_markdown -- --test-threads=1`
- `cargo test -p mnote-web web_shell -- --test-threads=1`
- Page Aggregate body/options/title smoke 不回退。
归档条件:
- `3-13` 三个未完成项完成后归档。
- `5-6` 剩余 Phase G/H 小尾项完成或拆成更小 checklist 后归档。
## 5. P2tree live cache 与 command context
### P2.1 Tree realtime live cache
入口文档:
- `design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
- `design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md`
执行目标:
1. 把 local-folder watcher、projection refresh、WS/SSE snapshot/delta/resync 收进同一 live cache。
2. 减少 polling、手动 refresh 和双路径补偿。
3. 明确 Convex realtime 只服务 cloud / sync replica,不回到 local-first 默认真相层。
验收:
- tree create / rename / move / archive / restore / purge 双浏览器 smoke。
- local folder watcher 修改 `.md`、resource、directory 后主壳不整页刷新。
归档条件:
- `3-3` 中统一 live cache 和 no-refresh 矩阵完成后归档。
### P2.2 Command context / context key
入口文档:
- `design/01-tree-first-graph-kernel/process/1-5-next-phase-sequential-execution-checklist-v1.md`
- `design/04-tree-domain/process/4-44-filetree-action-layer-paste-drop-delete-v1.md`
执行目标:
1. 菜单、快捷键、按钮、AI 写入能力共享 enablement 判断。
2. 让 readonly、dirty、permission、resource kind、open target 统一成为 context key。
3. 降低 filetree / editor / AI 各自硬编码能力判断的概率。
验收:
- Rust 或 JS 单测覆盖 context key `when` 判断。
- 文件树右键、主编辑器附件菜单、快捷键的 disabled/hidden 行为一致。
归档条件:
- `4-44` action layer smoke 全绿。
- `1-5` command context 对应项完成。
## 6. P3AI 主线继续收口
### P3.1 online markdown final content truth
入口文档:
- `design/07-ai/process/7-27-online-markdown-writeback-final-content-truth-v2.md`
执行目标:
1. 修正 online / compat `mnote.doc.markdown_edit` 的 final markdown 写回真相。
2. 避免 full_content 和多 operation 场景仍从旧 blocks 二次推导。
3. 保持 local-first 默认编辑仍走 agent 原生 patch/diff,不把 `markdown_edit` 升回默认主路径。
验收:
- `cargo test -p mnote-web hermes_tools -- --test-threads=1`
- 覆盖 full_content、多 op、inline content、selection scope 的回归测试。
归档条件:
- 7-27 的 6 步实施计划完成后归档。
### P3.2 ACP runtime 后续步骤
入口文档:
- `design/07-ai/process/7-15-page-ai-acp-agent-runtime-unified-layer-v1.md`
- `design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
执行目标:
1. 继续拆 `7-15` step 8 之后:Reasonix wrapper、前端 runtime selector、存量迁移、旧 HTTP proxy 兼容隔离。
2.`7-12` 的 manifest / review surface / state event 仍未完成项拆新 checklist。
3. Phase C / Review Mode 仍冻结,不在未解冻前实现流式 apply。
验收:
- ACP Hermes 与 ACP Reasonix 均能真实回复。
- session load、permission、tool location、plan UI 不回退。
归档条件:
- `7-15` 后续步骤拆成并完成具体 checklist 后再归档。
- `7-12` Phase B-F 完成或冻结项移出后再归档。
## 7. P4mindmap、Wolai 对标和参考流程
### P4.1 Mindmap 尾项
入口文档:
- `design/06-mindmap/process/6-mindmap-kernel-phase6-projection-editor-v1.md`
- `design/06-mindmap/process/6-mindmap-phase6-leptos-ui-shell-reuse-checklist-v1.md`
- `design/06-mindmap/process/6-mindmap-leptos-adapter-reference-notes-v1.md`reference
执行目标:
1. 明确 simplemindmap 当前产品化边界。
2. 给 KMind visual parity 设置可退出标准,避免无限尾项。
3. 后续 AI 操作 mindmap 时以 Resource Tree 对象能力为入口,不塞入 Markdown 正文真相。
验收:
- mindmap open / save / filetree active / resource tab smoke 继续通过。
- KMind parity 未完成项要么完成,要么明确降级为参考。
### P4.2 Wolai-aline 持续项
入口文档:
- `design/05-editor-mainline/process/5-9-wolai-aline-continuous-checklist-v1.md`
- `design/08-wolai-aline-test-flow/process/wolai-aline-test-flow-v1.md`workflow reference
- `design/05-editor-mainline/process/5-7-wolai-page-tree-main-editor-experience-restoration-v1.md`reference
执行目标:
1. 清理 `5-9` 中已被 `5-6` 覆盖但仍显示 TODO 的 Page Aggregate 项。
2. 对 A4 字体 token、E18 paste/clipboard 等仍有价值项补 smoke 或降级。
3. 所有 Wolai 对标任务继续按 `wolai-aline` skill 和 `08` 流程执行。
归档条件:
- `5-9` 剩余项全部完成或降级后归档。
## 8. P5:远期 / 候选 / 参考
这些文档不进入当前直接执行序列,只有当产品优先级明确时再拆 checklist:
- `design/03-rust-web/process/3-17-convex-export-web-entry-v1.md`
- CLI 已有迁移闭环;Web 管理入口未排期。
- `design/04-tree-domain/process/4-23-local-cloud-explicit-bridge-p3-candidate-v1.md`
- P3 候选,不能进入 local-first 默认主链。
- `design/07-ai/process/7-17-acp-session-convex-sharing-contract-v1.md`
- Future sharing contract。
- `design/07-ai/process/7-28-resource-ai-tool-contract-v1.md`
- mindmap / office Resource AI tool 草案。
- `design/09-siyuan-reference/process/9-siyuan-reference-boundary-and-adoption-v1.md`
- 参考边界,不是实施入口。
- `design/10-review/process/12-*``13-*``14-*`
- 原始 review 证据;执行项已拆出或归档。
## 9. 推荐执行批次
### Batch A:清验证阻塞
1. `4-45`
2. `4-44`
3. `4-34`
4. `4-28`
完成后预期:tree/filetree action 和 trash/no-refresh 的判断不再被 smoke 红灯干扰。
### Batch B:身份与打开态
1. `5-26`
2. `1-5` identity 子项
3. `1-6` BufferStore / conflict browser 子项
完成后预期:页面、资源、tab、filetree active、open editors 可围绕同一 identity 和 buffer state 工作。
### Batch CPage Aggregate / local markdown 写侧
1. `3-13`
2. `5-6`
3. `1-5` Page Aggregate 子项
完成后预期:正文真相、runtime projection、compat join 的边界更薄。
### Batch Dlive cache / command context
1. `3-3`
2. `1-5` command context 子项
3. `4-44` 若 Batch A 未完成则回补
完成后预期:树域实时与 enablement 判断更统一。
### Batch EAI 与资源工具
1. `7-27`
2. `7-15` 后续拆分
3. `7-12` Phase B-F 拆分
4. `7-28` 仅在 mindmap / office AI 明确排期后进入
完成后预期:local-first AI 主路径稳定,cloud/compat 工具不再误导默认路径。
### Batch F:体验对标与远期
1. `6-mindmap-*`
2. `5-9`
3. `3-17`
4. `4-23`
5. `7-17`
完成后预期:产品体验和分享同步进入产品化,而不是继续扩底层 compat。
## 10. 每轮执行的固定流程
每次从本文选一个 batch 后,按以下流程推进:
1. 先读本文和对应入口文档。
2. 对该 batch 新建或更新一个小 checklist,不直接修改大而旧的全景文档。
3. 如果有 2 个以上互不冲突方向,按 Reasonix parallel flow 拆 worker。
4. worker 只改其 ownership 范围,不提交 git。
5. Codex 复核 diff、跑 targeted tests / browser smoke。
6. 完成后:
- 更新 checklist 状态和证据。
- 能归档的移动到 `done/`
- 被覆盖但未完成的移入 `old/` 并标 `[recycle]`
- 更新本文对应 batch 状态。
- 运行 `codegraph sync .`
## 11. 不做事项
- 不按文件编号顺序清空 process。
- 不把 `process-reference` 当作当前实现任务。
- 不因为文档老就移动到 `done/`;必须有真实完成证据。
- 不把 Phase C / Review Mode / cloud sync / sharing future 合同提前实现。
- 不恢复旧 Next / BlockNote / Convex 默认主存储口径。
@@ -13,7 +13,7 @@
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-15-local-markdown-asset-upload-relative-path-v1.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/done/3-15-local-markdown-asset-upload-relative-path-v1.md`
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-22-local-folder-convex-unified-tree-execution-checklist-v1.md`
> - `/mnt/Data1T/mnote/design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md`
> - `/mnt/Data1T/mnote/bugs/03-rust-web/done/3-16-local-markdown-upload-uses-convex-media-asset-v1.md`
@@ -1,8 +1,10 @@
# 2-3 [process] Local Workspace Access Control 产品化 checklist v1
# 2-3 [done] Local Workspace Access Control 产品化 checklist v1
> 创建时间:2026-05-19
>
> 当前状态:`PROCESS`
> 当前状态:`DONE`
>
> 归档说明(2026-05-21):P0 管理员控制面 API 与 P1 权限覆盖审计已完成;管理员 UI/API、local folder open、page body write、tree command、Hermes/Reasonix 与 shared AI session 权限链已有代码与测试证据。后续授权产品化扩展应新开文档。
>
> 上位依据:
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/done/2-2-local-first-workspace-convex-control-plane-v1.md`
@@ -1,8 +1,10 @@
# 3-15 [process] 本地 Markdown 图片与附件上传相对路径设计 v1
# 3-15 [done] 本地 Markdown 图片与附件上传相对路径设计 v1
> 创建时间:2026-05-18
>
> 状态:`[process]`(代码、单测和 HTTP smoke 已完成;真实浏览器 smoke 待补)
> 状态:`[done]`
>
> 归档说明(2026-05-21):代码、单测、HTTP smoke 与真实浏览器 smoke 均已完成;`scripts/task443-local-markdown-asset-upload-smoke.js` 覆盖本地 `.md` 页面上传图片/附件、刷新后恢复与文件树可见。
>
> 所属主线:`03-rust-web` / `local_folder` / `local markdown`
>
@@ -192,7 +194,7 @@ multipart/form-data
- [x] 单测覆盖本地上传目标路径和冲突递增。
- [x] 单测覆盖 local markdown 图片/附件 roundtrip。
- [x] HTTP smoke 覆盖 `/api/local-folder/assets/upload` 真实 multipart 上传与落盘。
- [ ] 浏览器 smoke 覆盖本地 `.md` 页面上传图片/附件、刷新后恢复、文件树可见。
- [x] 浏览器 smoke 覆盖本地 `.md` 页面上传图片/附件、刷新后恢复、文件树可见`scripts/task443-local-markdown-asset-upload-smoke.js`
## 10. 验收命令
@@ -205,5 +207,5 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web local_folder -- --nocapt
浏览器 smoke 后续新增:
```bash
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task-local-markdown-asset-upload-smoke.js
MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task443-local-markdown-asset-upload-smoke.js
```
@@ -1,6 +1,8 @@
# 3-17 Convex Export Web Entry v1
状态:process
状态:process-reference
归档说明(2026-05-21):CLI 迁移闭环已具备并有 smoke 证据;本文只保留为后续 Web 管理入口设计参考,当前不作为 active implementation checklist。若决定短期落地 Web UI,应新建执行 checklist。
## 目标
@@ -1,6 +1,7 @@
# 4-27 Resource Lifecycle Command Cutover v1
> 状态:process
> 状态:done
> 归档说明(2026-05-21):核心 `tree.resource.*` 生命周期命令 cutover 已完成;双浏览器 no-refresh、restore reveal/focus 等尾项已由 `4-34``4-28` 继续跟踪。
> 更新时间:2026-05-15
> 范围:`tree.resource.archive / restore / purge / rename` 正式命令面,以及 `/api/media/*`、mindmap、table 资源垃圾箱兼容入口的收口边界。
@@ -1,6 +1,8 @@
# 4-39 FileTree Open Target 与 Resource Identity Checklist v1
> 状态:process
> 状态:done
>
> 归档说明(2026-05-21):FileTree open target 与 resource identity 合同已完成;`rootUri` 收尾由 `4-42` 覆盖并已归档。
>
> 日期:2026-05-20
>
@@ -1,6 +1,8 @@
# 4-40 Resource Lifecycle Command Cutover 与垃圾箱 Smoke Checklist v1
> 状态:process
> 状态:done
>
> 归档说明(2026-05-21):resource lifecycle browser smoke 与 command cutover 验证已完成;bulk delete 剩余项由 `4-41` 覆盖并已归档。
>
> 日期:2026-05-20
>
@@ -1,6 +1,8 @@
# 4-41 FileTree Bulk Resource Command Cutover Checklist v1
> 状态:process
> 状态:done
>
> 归档说明(2026-05-21):FileTree bulk resource command cutover 已完成,云端 table 旧 fallback 为显式保留边界。
>
> 日期:2026-05-20
>
@@ -1,6 +1,8 @@
# 4-42 FileTree Open Target RootUri Contract Checklist v1
> 状态:process
> 状态:done
>
> 归档说明(2026-05-21):FileTree open target `rootUri` 合同已完成,缺失 `rootUri` 的旧 payload 兼容仍保留。
>
> 日期:2026-05-20
>
@@ -1,6 +1,8 @@
# 4-43 Local Folder Sort Order Persistence Checklist v1
> 状态:process
> 状态:done
>
> 归档说明(2026-05-21):local folder sort order 持久化、projection 消费与浏览器 smoke 均已完成。
>
> 日期:2026-05-20
>
@@ -2,6 +2,8 @@
> 更新时间:2026-05-07
>
> 当前状态:`process-reference / P3 candidate`。本文只冻结 Local / Cloud 显式桥接候选合同,未进入当前实施;不得把它当作 local-first 默认读写路径。
>
> 关联:
> - `4-21-local-folder-convex-unified-tree-source-v1.md`
> - `4-22-local-folder-convex-unified-tree-execution-checklist-v1.md`
@@ -1,5 +1,9 @@
# 5-15 本地 Markdown 路径身份统一 Checklist v1
> 状态:done
>
> 归档说明(2026-05-21):本地 Markdown 路径身份统一 checklist 已完成并从 `process/` 归档到 `done/`
## 背景
当前本地 Markdown 主链正在对齐 VS Code 文件模型:普通 `.md` 文件的运行时身份应来自文件资源地址,而不是 frontmatter 或 MNote 内部稳定 ID。
@@ -132,4 +136,3 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web local_page_aggregate_acc
cargo test --manifest-path rust/Cargo.toml -p mnote-web local_page_id_initialization_returns_path_id_without_writing_page_ids -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_command_local_folder -- --nocapture
```
@@ -1,6 +1,8 @@
# 5-16 Editor Group 与 Resource Tab 安全收口 Checklist v1
> 状态:process
> 状态:done
>
> 归档说明(2026-05-21):本文件核心安全收口已完成;正式 tab state 与错误占位剩余项已由 `5-18` 覆盖并归档。
>
> 日期:2026-05-20
>
@@ -20,7 +22,7 @@
## 3. Checklist
- [ ] 定义轻量 `MainEditorTabEntry` / `mainEditorTabRegistry` 语义,至少包含 `id``kind``title``dirty``saving``hasExternalConflict``lastActiveAt`
- [x] 定义轻量 `MainEditorTabEntry` / `mainEditorTabRegistry` 语义,至少包含 `id``kind``title``dirty``saving``hasExternalConflict``lastActiveAt`(由 `5-18` 的主 tab state 收口覆盖)
- [x] 保持 page tab 为固定 tab,但 resource tab 激活/关闭应走同一套 registry 状态更新。
- [x] `closeResourceTab` 关闭前检查 resource session
- [x] `saving=true` 时禁止直接关闭或等待保存完成。
@@ -28,14 +30,14 @@
- [x] `dirty=true` 时触发保存或确认流程;不得直接释放 session。
- [x] 关闭 active resource tab 后按 MRU 回到上一个 resource tab;没有可用 resource tab 时才回到 page tab。
- [x] resource tab 激活时更新稳定 `aria-selected``tabindex` 和 panel hidden 状态。
- [ ] 打开失败时保留 error placeholder tab,允许用户关闭或重试,不要留下半注册状态。
- [x] 打开失败时保留 error placeholder tab,允许用户关闭或重试,不要留下半注册状态。(由 `5-18` 覆盖)
## 4. 验收
- [x] 打开两个 resource tab,切到第二个并关闭后,应回到第一个,而不是无条件回 page tab。
- [x] 编辑 resource markdown 后立即点关闭,不应静默丢弃未保存内容。
- [x] 保存中关闭不会释放 editor session。
- [ ] 打开失败的 resource tab 有明确错误占位,并可关闭。
- [x] 打开失败的 resource tab 有明确错误占位,并可关闭。(由 `5-18` 覆盖)
- [x] 现有 `task457-main-editor-resource-tab-smoke.js` 继续通过。
- [x] 新增 close dirty / MRU smoke 通过。
@@ -43,7 +45,7 @@
- 已合入:resource tab MRU、关闭 guard、`aria-selected` / `tabindex` 同步、关闭后回退到上一个 resource tab。
- 已新增:`scripts/task460-resource-tab-close-dirty-smoke.js` 覆盖近期本地输入后的关闭阻止。
- 未完成:正式 `MainEditorTabEntry` 模型打开失败 placeholder tab。
- 后续覆盖:正式 `MainEditorTabEntry` 模型打开失败 placeholder tab 已由 `5-18` 收口
## 5. 非目标
@@ -1,6 +1,8 @@
# 5-17 Resource Editor Kind 与 Smoke 矩阵 Checklist v1
> 状态:process
> 状态:done
>
> 归档说明(2026-05-21):资源 editor kind 与 smoke 矩阵已完成;resolver 与 OnlyOffice 回源后续由 `5-19` 覆盖并归档。
>
> 日期:2026-05-20
>
@@ -1,6 +1,8 @@
# 5-18 Main Editor Tab State 与错误占位 Checklist v1
> 状态:process
> 状态:done
>
> 归档说明(2026-05-21):主编辑器 tab state 与错误占位已完成并验证。
>
> 日期:2026-05-20
>
@@ -1,6 +1,8 @@
# 5-19 Resource Open Resolver 与 OnlyOffice 回源 Checklist v1
> 状态:process
> 状态:done
>
> 归档说明(2026-05-21):resource open resolver 与 OnlyOffice 回源已完成;外部 DocumentServer 协同验证不作为本 checklist 阻塞项。
>
> 日期:2026-05-20
>
@@ -1,6 +1,8 @@
# 5-20 Resource Tab Close Confirm Checklist v1
> 状态:process
> 状态:done
>
> 归档说明(2026-05-21):resource tab close guard 可见提示已完成。
>
> 日期:2026-05-20
>
@@ -1,6 +1,8 @@
# 5-21 Editor Tab URL / Active Row / Canonical Identity Checklist v1
> 状态:process
> 状态:done
>
> 归档说明(2026-05-21):editor tab URL、active row 与 canonical identity 收口已完成。
>
> 日期:2026-05-20
>
@@ -1,6 +1,8 @@
# 5-22 Mindmap Main Editor Tab Kind Checklist v1
> 状态:process
> 状态:done
>
> 归档说明(2026-05-21):mindmap main editor tab kind 收口已完成。
>
> 日期:2026-05-20
>
@@ -1,6 +1,8 @@
# 5-23 Editor Side Target / Secondary Pane Checklist v1
> 状态:process
> 状态:done
>
> 归档说明(2026-05-21):`openTarget=side` 到 secondary pane 的轻量合同已完成。
>
> 日期:2026-05-20
>
@@ -1,6 +1,8 @@
# 5-24 Open Editors Lightweight View Checklist v1
> 状态:process
> 状态:done
>
> 归档说明(2026-05-21):open editors lightweight snapshot / 可观测模型已完成;完整 sidebar UI 不是本阶段目标。
>
> 日期:2026-05-21
>
@@ -1,6 +1,8 @@
# 5-25 Editor Tab Keyboard and MRU Checklist v1
> 状态:process
> 状态:done
>
> 归档说明(2026-05-21):editor tab keyboard 与 MRU 行为已完成。
>
> 日期:2026-05-21
>
@@ -2,6 +2,8 @@
> 更新时间:2026-05-19
>
> 当前状态:`process-reference`。本文是参考实现采用矩阵,不是当前执行 checklist;具体实现任务应拆到对应 domain 的 process/done 文档。
>
> 关联背景:
> - `/mnt/Data1T/mnote/CURRENT_ARCHITECTURE.md`
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
@@ -2,6 +2,8 @@
> 更新时间:2026-04-19
>
> 当前状态:`process-reference`。本文只作为 tiptap / Notion-like 行为模型参考,不再作为当前执行 checklist;默认主编辑器已是页面内 `leptos-tiptap` island。
>
> 这份文档已经按当前进展修正:
>
> 现在的问题不再是“这套模板能不能迁进来”,而是:
@@ -2,6 +2,8 @@
> 更新时间:2026-05-18
>
> 当前状态:`process-reference`。本文作为 Page Aggregate 单一真源长期方案保留;当前执行进度与剩余项以 `5-6` 及 MVP 后阶段 checklist 为准。
>
> 2026-05-18 口径补充:
> - 页面聚合的默认 source 已开始向 local-first workspace 收口;页面正文、标题、设置和上传资源不应再默认把 Convex 视作主数据层。
> - 本文中涉及 Convex 的表述只应理解为 local-first 与 Convex-backed 两种 `WorkspaceSource` 的过渡兼容背景,不应再作为默认产品形态解释。
@@ -2,6 +2,7 @@
> 更新时间:2026-04-30
>
> 当前状态:`process-reference`。本文只保留 Wolai 体验复刻目标和取证基线;持续执行与安全边界以 `08-wolai-aline-test-flow``wolai-aline` skill 为准。
> **Wolai-aline 执行口径更新(2026-04-30):** 本文只保留体验复刻任务拆解和产品目标。所有 Wolai 对标测试、浏览器取证、编辑权限、安全边界、subagent 使用、截图复核和 smoke 补齐,统一以 `/home/lix/.codex/skills/wolai-aline``/mnt/Data1T/mnote/design/08-wolai-aline-test-flow/process/wolai-aline-test-flow-v1.md` 为准。若本文旧段落与该 skill 冲突,以 skill 为准。
>
@@ -2,6 +2,8 @@
> 日期:2026-05-10
>
> 当前状态:`process-reference`。本文是 adapter/reference 审计笔记,不作为当前执行 checklist;mindmap 实施状态以 `6-mindmap-phase6-leptos-ui-shell-reuse-checklist-v1``done/6-mindmap-phase6-kmind-parity-detail-checklist-v1.md` 为准。
>
> 目的:记录 `lx-doc/mind-map` 与 KMind 插件审计结果,为 Phase 6 `leptos-mindmap` 的 simple-mind-map bridge、command bridge 和第一阶段 UI 复刻定界。
## 1. 参考入口
@@ -1,8 +1,10 @@
# 7-16 [process] Page Block AI 真实页面 Smoke 后续矩阵 v1
# 7-16 [done] Page Block AI 真实页面 Smoke 后续矩阵 v1
> 创建时间:2026-05-18
>
> 当前状态:`PROCESS`
> 当前状态:`DONE`
>
> 归档说明(2026-05-21):本文范围内可执行的真实页面 smoke 已完成;剩余 UI / Review Mode 属于 Phase C 冻结范围,解冻时应新建设计稿承接。
>
> 来源:
> - `/mnt/Data1T/mnote/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
@@ -1,6 +1,8 @@
# 7-30 ACP session/load 恢复 checklist v1
> 状态:process
> 状态:done
>
> 归档说明(2026-05-21):ACP `session/load` / resume 核心闭环已完成并通过聚焦测试;session id 全链路持久化作为后续产品化项继续跟踪。
>
> OwnerACP session lifecycle
>
@@ -1,6 +1,8 @@
# 7-32 ACP tool locations 与打开动作 checklist v1
> 状态:process
> 状态:done
>
> 归档说明(2026-05-21):ACP tool locations 解析、SSE 桥接与前端打开动作已完成;浏览器 smoke 可作为后续补强。
>
> OwnerACP tool event data + Page AI open action
>
@@ -1,6 +1,8 @@
# 7-33 ACP session info / plan UI checklist v1
> 状态:process
> 状态:done
>
> 归档说明(2026-05-21):ACP session info / plan SSE 与 Page AI 轻量 UI 已完成;todo JSON 细化显示作为后续格式约定项。
>
> OwnerACP metadata SSE + Page AI lightweight UI
>
@@ -79,7 +79,7 @@
仍未完成,本文继续留在 `process/`
- 剩余真实页面 smoke 已拆到 `/mnt/Data1T/mnote/design/07-ai/process/7-16-page-block-ai-real-smoke-followup-matrix-v1.md` 持续跟踪;本文只保留总 checklist历史证据。
- 剩余真实页面 smoke 已 `/mnt/Data1T/mnote/design/07-ai/done/7-16-page-block-ai-real-smoke-followup-matrix-v1.md` 完成并归档;本文只保留总 checklist历史证据和 Phase 8 / Review Mode 冻结缺口
- 复杂块移动阻断矩阵已补隔离 route smoke 覆盖标题带子块、列表项、表格、mindmap/resource;真实浏览器页面 smoke 仍未补。
- 持久审阅/preview UI 仍是后续可选项;当前默认 yolo 模式不做写入审批,`scope=selection``format=page_xml/text` 已进入工具与 PageAIContextBuilder 首版。
- PageAIIntentParser / PageAIOperationPlanner / PageAIOperationValidator / PageAIApplyController 仍需继续建设;当前本地 planner 只覆盖低歧义文本块增删改,不应被视为完整 AI 编辑 runtime。
@@ -2,7 +2,9 @@
> 创建时间:2026-05-18
>
> 当前状态:`PROCESS / FUTURE`
> 当前状态:`process-reference / future`
>
> 归档说明(2026-05-21):本文冻结 ACP session 与控制面分享/账号作用域远期合同;当前只作为 future reference,不作为 MVP 后阶段 active implementation checklist。
>
> 本稿目的:
> 1. 明确 ACP session 必须绑定当前账号权限与控制面作用域,不能以内存态绕过权限。
@@ -1,6 +1,8 @@
# 7-28 Resource AI Tool Contract v1
状态:process
状态:process-reference
说明(2026-05-21):本文冻结 mindmap / office 等 Resource AI tool 合同草案;当前不是 local-first 普通 Markdown 编辑主路径,也不是 active implementation checklist。
主线:VSCode 简化版工作区 + tiptap markdown 编辑器 + Hermes / Reasonix agent + simplemindmap / office 插件 + Wolai 主题 web 壳 + 鉴权控制面。
@@ -1,5 +1,7 @@
# Wolai-aline 对标测试流程 v1
> 状态:process-reference / active workflow
>
> 本流程用于所有以 Wolai 体验复刻、对标、aline/alignment 为目标的任务。目标不是只做出类似文案,而是用真实 Wolai 行为和本地 `3000` 行为做可复现对比,形成可回归的 smoke 与截图证据。
## 适用范围
@@ -2,6 +2,8 @@
> 更新时间:2026-05-11
>
> 当前状态:`process-reference`。本文只保留思源参考边界与可借鉴能力,不覆盖 `01-05` 当前优先级,也不作为当前执行 checklist。
>
> 上位依据:
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
@@ -0,0 +1,131 @@
# 15 Design Governance MVP 后阶段 Review v1
> 状态:done
>
> 归档说明(2026-05-21):四个 Reasonix worker 审查、Codex 复核、done 归档与 process-reference 标注已完成;本文归档为本轮 design governance 事实源。
>
> 创建时间:2026-05-21
>
> 目标:在 local-first MVP 已初步完成、架构方向基本稳定后,统一 `design/` 目录口径,归档过时 process 文档,并明确仍需继续执行的 process 文档。
## 1. 当前统一口径
MNote 当前已进入 local-first MVP 后阶段:
- 本地 workspace folder 是默认数据真相。
- 本地 `.md` 是页面正文真相。
- Rust kernel / projection / command 持有树、页面、资源和权限语义。
- `mnote-web` 是 3000 主 Web 执行面。
- Convex / 服务端降级为 auth、membership、share grants、sync state、AI policy、cloud source、compat 和 sync replica 控制面。
- 旧 Next / React / wolai-frontend、BlockNote 默认主链、Convex 默认正文/附件/AI 会话全文主存储口径均为 legacy / recycle / compat 背景。
## 2. 本轮整理原则
1. 仍代表长期方向或当前执行入口的文档保留在 `process/`,但必须叠加 MVP 后阶段口径。
2. 已完成、已有真实代码或 smoke 证据的执行清单移动到同域 `done/`,并补充归档说明。
3. 被后续文档覆盖、但不代表已完成的旧稿移动到 `design/old/<domain>/process/`,标题或开头标记 `[recycle]`
4. 参考资料、外部产品取证、长期方向说明可以保留在 `process/`,但需要明确“不作为当前执行 checklist”。
5. 不把旧 Convex / Next / BlockNote 说法改写成当前主线;若保留,必须标注为历史快照或反例。
## 3. 并行审查切片
- Worker A`01` / `02` / `03`,重点判断 long-term 架构稿和 Rust Web process 是否仍应继续执行。
- Worker B`04-tree-domain`,重点判断 file tree / resource lifecycle / trash / open target 相关 process 是否已由近期 bug/smoke 完成。
- Worker C`05-editor-mainline` / `06-mindmap` / `08-wolai` / `09-siyuan`,重点判断编辑器 tab、OnlyOffice、mindmap 和对标流程文档是否仍活跃。
- Worker D`07-ai` / `10-review`,重点判断 AI process 与 review process 是否已被后续 Hermes/ACP/local-first 文件编辑口径覆盖。
## 4. Codex 归档判定状态
本轮使用以下状态:
- `keep-process`:继续留在 `process/`,后续仍需执行。
- `mark-process-reference`:留在 `process/`,但只作为长期方向、流程或参考基线,不作为当前直接执行 checklist。
- `archive-done`:移动到同域 `done/`,因为目标已完成或当前代码已覆盖。
- `archive-old-process`:移动到 `design/old/<domain>/process/` 并标 `[recycle]`,因为被后续稿覆盖或口径过时但未必完成。
- `needs-human-confirmation`:证据不足,暂不移动。
## 5. 初步本地发现
- `design/01-tree-first-graph-kernel/process/1-1-*``design/03-rust-web/process/3-1-*` 已自述为全景参考,当前优先级应由 `01-05` 和后续 MVP 后阶段 checklist 驱动。
- `design/03-rust-web/process/3-15-*` 已写明代码、单测和 HTTP smoke 完成,仅真实浏览器 smoke 待补;需要核对是否已有后续真实浏览器证据。
- `design/04-tree-domain/process/4-38``4-45` 是 2026-05-20 之后针对资源生命周期、open target、下载、拖拽上传等 bug 的近期 checklist,可能有部分已经由当前未提交 smoke / bug done 覆盖。
- `design/05-editor-mainline/process/5-15``5-26` 初始盘点时大多是近期 main editor tab/resource open/OnlyOffice 收口 checklist;本轮已按真实代码和 smoke 证据逐项判定,`5-15``5-25` 中除继续执行项外已归档到 `done/``5-26` 继续保留为 active process。
- `design/07-ai/process/7-15` 已包含大量完成记录,但仍有 HTTP proxy compat、session resume、permission loop、tool location 等后续切片,不能仅凭完成项归档。
## 6. 本轮不做事项
- 不改 `design/11-wolai/``design/90-reference/` 的参考资料归类。
- 不删除历史文档。
- 不把 `old/` 下历史稿移动回主线。
- 不改代码,不跑浏览器 smoke;本轮只做设计文档治理。
## 7. Reasonix 并行审查结果
本轮已派发四个只读 Reasonix worker,报告路径如下:
- Worker A01/02/03):`.codex/reasonix-tasks/results/design-governance-worker-a-01-03.md`
- Worker B04 tree-domain):`.codex/reasonix-tasks/results/design-governance-worker-b-04-tree.md`
- Worker C05/06/08/09):`.codex/reasonix-tasks/results/design-governance-worker-c-05-06-08-09.md`
- Worker D07/10):`.codex/reasonix-tasks/results/design-governance-worker-d-07-10.md`
Codex 已复核 worker 输出,并额外核对 `3-15` 的真实浏览器 smoke 证据。
## 8. 本轮归档到 done
以下文档已从 `process/` 移动到对应 `done/`,并在文件头部补充 `done` 状态或归档说明:
- `design/01-tree-first-graph-kernel/done/1-7-local-workspace-root-page-bundle-checklist-v1.md`
- `design/02-convex-rust-long-term-architecture/done/2-3-local-workspace-access-control-productization-v1.md`
- `design/03-rust-web/done/3-15-local-markdown-asset-upload-relative-path-v1.md`
- `design/04-tree-domain/done/4-27-resource-lifecycle-command-cutover-v1.md`
- `design/04-tree-domain/done/4-39-filetree-open-target-resource-identity-checklist-v1.md`
- `design/04-tree-domain/done/4-40-resource-lifecycle-command-cutover-smoke-checklist-v1.md`
- `design/04-tree-domain/done/4-41-filetree-bulk-resource-command-cutover-v1.md`
- `design/04-tree-domain/done/4-42-filetree-open-target-rooturi-contract-v1.md`
- `design/04-tree-domain/done/4-43-local-folder-sort-order-persistence-v1.md`
- `design/05-editor-mainline/done/5-15-local-markdown-path-identity-unification-checklist-v1.md`
- `design/05-editor-mainline/done/5-16-editor-group-resource-tab-safety-checklist-v1.md`
- `design/05-editor-mainline/done/5-17-resource-editor-kind-and-smoke-matrix-checklist-v1.md`
- `design/05-editor-mainline/done/5-18-main-editor-tab-state-and-error-placeholder-checklist-v1.md`
- `design/05-editor-mainline/done/5-19-resource-open-resolver-and-onlyoffice-checklist-v1.md`
- `design/05-editor-mainline/done/5-20-resource-tab-close-confirm-checklist-v1.md`
- `design/05-editor-mainline/done/5-21-editor-tab-url-active-identity-checklist-v1.md`
- `design/05-editor-mainline/done/5-22-mindmap-main-editor-tab-kind-v1.md`
- `design/05-editor-mainline/done/5-23-editor-side-target-secondary-pane-v1.md`
- `design/05-editor-mainline/done/5-24-open-editors-lightweight-view-checklist-v1.md`
- `design/05-editor-mainline/done/5-25-editor-tab-keyboard-and-mru-checklist-v1.md`
- `design/07-ai/done/7-16-page-block-ai-real-smoke-followup-matrix-v1.md`
- `design/07-ai/done/7-30-acp-session-load-resume-checklist-v1.md`
- `design/07-ai/done/7-31-acp-permission-decision-loop-checklist-v1.md`
- `design/07-ai/done/7-32-acp-tool-location-and-open-action-checklist-v1.md`
- `design/07-ai/done/7-33-acp-session-info-plan-ui-checklist-v1.md`
## 9. 本轮标记为 process-reference
以下文档仍保留在 `process/`,但已标注为 `process-reference`,不再作为当前直接执行 checklist:
- `design/03-rust-web/process/3-17-convex-export-web-entry-v1.md`
- `design/04-tree-domain/process/4-23-local-cloud-explicit-bridge-p3-candidate-v1.md`
- `design/05-editor-mainline/process/5-2-tiptap-notion-like-template-adoption-v1.md`
- `design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
- `design/05-editor-mainline/process/5-7-wolai-page-tree-main-editor-experience-restoration-v1.md`
- `design/05-editor-mainline/process/5-14-zed-lapce-vscode-reference-adoption-matrix-v1.md`
- `design/06-mindmap/process/6-mindmap-leptos-adapter-reference-notes-v1.md`
- `design/07-ai/process/7-17-acp-session-convex-sharing-contract-v1.md`
- `design/07-ai/process/7-28-resource-ai-tool-contract-v1.md`
- `design/08-wolai-aline-test-flow/process/wolai-aline-test-flow-v1.md`
- `design/09-siyuan-reference/process/9-siyuan-reference-boundary-and-adoption-v1.md`
- `design/10-review/process/12-sidex-vscode-workbench-gap-review-v1.md`
- `design/10-review/process/13-hermes-vscode-plugin-gap-review-v1.md`
- `design/10-review/process/14-sidex-workbench-gap-followup-review-v1.md`
## 10. 仍需继续执行的 process
本轮保留为 active process 的主要文档与原因:
- `01``1-tree-first-graph-kernel-v1``1-1``1-2``1-3``1-4``1-5``1-6` 仍分别承担长期主线、全景参考、产品愿景、持续执行与下一阶段缺口跟踪。
- `03``3-rust-web-long-term-architecture-v1``3-1``3-3``3-13` 仍有架构或未完成迁移项。
- `04``4-28``4-34``4-38``4-44``4-45` 仍有 restore focus、local folder no-refresh、open target 前端 cutover、action-layer smoke 和 open editors lifecycle 验证缺口。
- `05``5-6``5-9``5-26` 仍分别跟踪 Page Aggregate 小尾项、Wolai-aline 持续项和 resource open resolver 收敛。
- `06``6-mindmap-kernel-phase6-projection-editor-v1``6-mindmap-phase6-leptos-ui-shell-reuse-checklist-v1` 仍保留 mindmap 内核/视觉尾项。
- `07``7-10``7-12``7-14``7-15``7-27` 仍分别承担 Phase 8 冻结缺口、Hermes tool routing、online/local markdown convergence、ACP 统一层和 final markdown truth 后续修复。
@@ -1,6 +1,8 @@
# 12 Sidex / VSCode Workbench 对照审查 v1
> 状态:process
> 状态:process-reference
>
> 归档说明(2026-05-21):本 review 的 P0/P1/P2 缺口已拆分到 `4-*` / `5-*` 执行 checklist;本文保留为 Sidex / VSCode 对照原始证据,不再作为直接执行清单。
>
> 日期:2026-05-20
>
@@ -96,27 +98,27 @@ local office 可进入主编辑区 tabConvex office 仍可能新窗口;mind
本 review 拆分为三份执行 checklist
- `design/05-editor-mainline/process/5-16-editor-group-resource-tab-safety-checklist-v1.md`
- `design/05-editor-mainline/done/5-16-editor-group-resource-tab-safety-checklist-v1.md`
- `design/04-tree-domain/process/4-38-filetree-resource-lifecycle-open-target-checklist-v1.md`
- `design/05-editor-mainline/process/5-17-resource-editor-kind-and-smoke-matrix-checklist-v1.md`
- `design/05-editor-mainline/done/5-17-resource-editor-kind-and-smoke-matrix-checklist-v1.md`
第二轮 P0 收口继续拆为四份独立 checklist:
- `design/05-editor-mainline/process/5-18-main-editor-tab-state-and-error-placeholder-checklist-v1.md`
- `design/05-editor-mainline/process/5-19-resource-open-resolver-and-onlyoffice-checklist-v1.md`
- `design/04-tree-domain/process/4-39-filetree-open-target-resource-identity-checklist-v1.md`
- `design/04-tree-domain/process/4-40-resource-lifecycle-command-cutover-smoke-checklist-v1.md`
- `design/05-editor-mainline/done/5-18-main-editor-tab-state-and-error-placeholder-checklist-v1.md`
- `design/05-editor-mainline/done/5-19-resource-open-resolver-and-onlyoffice-checklist-v1.md`
- `design/04-tree-domain/done/4-39-filetree-open-target-resource-identity-checklist-v1.md`
- `design/04-tree-domain/done/4-40-resource-lifecycle-command-cutover-smoke-checklist-v1.md`
第三轮继续收口剩余 P0,并在 P0 清零后推进相邻 P1:
- `design/05-editor-mainline/process/5-20-resource-tab-close-confirm-checklist-v1.md`
- `design/04-tree-domain/process/4-41-filetree-bulk-resource-command-cutover-v1.md`
- `design/04-tree-domain/process/4-42-filetree-open-target-rooturi-contract-v1.md`
- `design/05-editor-mainline/process/5-21-editor-tab-url-active-identity-checklist-v1.md`
- `design/05-editor-mainline/done/5-20-resource-tab-close-confirm-checklist-v1.md`
- `design/04-tree-domain/done/4-41-filetree-bulk-resource-command-cutover-v1.md`
- `design/04-tree-domain/done/4-42-filetree-open-target-rooturi-contract-v1.md`
- `design/05-editor-mainline/done/5-21-editor-tab-url-active-identity-checklist-v1.md`
第四轮进入 P1,按 editor workbench 语义继续拆分:
- `design/04-tree-domain/process/4-43-local-folder-sort-order-persistence-v1.md`
- `design/05-editor-mainline/process/5-22-mindmap-main-editor-tab-kind-v1.md`
- `design/05-editor-mainline/process/5-23-editor-side-target-secondary-pane-v1.md`
- `design/04-tree-domain/done/4-43-local-folder-sort-order-persistence-v1.md`
- `design/05-editor-mainline/done/5-22-mindmap-main-editor-tab-kind-v1.md`
- `design/05-editor-mainline/done/5-23-editor-side-target-secondary-pane-v1.md`
- `design/04-tree-domain/process/4-44-filetree-action-layer-paste-drop-delete-v1.md`
@@ -1,6 +1,8 @@
# 13 Hermes VSCode 插件对照审查 v1
> 状态:process
> 状态:process-reference
>
> 归档说明(2026-05-21):P0 缺口已拆分到 `7-30``7-33` 并完成归档;本文保留 Hermes VSCode 插件对照证据和 P1/P2 参考项。
>
> 日期:2026-05-20
>
@@ -87,10 +89,10 @@ MNote 已在 `AcpSessionManager` 解析 `SessionInfoUpdate` 和 `PlanUpdate`
本 review 先拆四份 P0 checklist,允许并行推进:
- `design/07-ai/process/7-30-acp-session-load-resume-checklist-v1.md`
- `design/07-ai/process/7-31-acp-permission-decision-loop-checklist-v1.md`
- `design/07-ai/process/7-32-acp-tool-location-and-open-action-checklist-v1.md`
- `design/07-ai/process/7-33-acp-session-info-plan-ui-checklist-v1.md`
- `design/07-ai/done/7-30-acp-session-load-resume-checklist-v1.md`
- `design/07-ai/done/7-31-acp-permission-decision-loop-checklist-v1.md`
- `design/07-ai/done/7-32-acp-tool-location-and-open-action-checklist-v1.md`
- `design/07-ai/done/7-33-acp-session-info-plan-ui-checklist-v1.md`
P0 收口后再拆 P1
@@ -1,6 +1,8 @@
# 14 Sidex / MNote Workbench Gap Follow-up Review v1
> 状态:process
> 状态:process-reference
>
> 归档说明(2026-05-21):本 follow-up review 已拆分出 `5-24``5-25``5-26``4-45` 等执行项;本文保留为后续 workbench 对照参考,不作为直接执行清单。
>
> 日期:2026-05-21
>
@@ -51,8 +53,8 @@ VSCode 的 Open Editors / Explorer / working copy / dirty indicator 是一条链
拆为四个方向并行推进:
- `design/05-editor-mainline/process/5-24-open-editors-lightweight-view-checklist-v1.md`
- `design/05-editor-mainline/process/5-25-editor-tab-keyboard-and-mru-checklist-v1.md`
- `design/05-editor-mainline/done/5-24-open-editors-lightweight-view-checklist-v1.md`
- `design/05-editor-mainline/done/5-25-editor-tab-keyboard-and-mru-checklist-v1.md`
- `design/05-editor-mainline/process/5-26-resource-open-resolver-convergence-checklist-v1.md`
- `design/04-tree-domain/process/4-45-filetree-open-editors-lifecycle-link-checklist-v1.md`
+8 -2
View File
@@ -1,21 +1,26 @@
# design 设计稿索引
> 更新时间:2026-05-19
> 更新时间:2026-05-21
>
> 当前产品口径:`VSCode 简化版工作区 + tiptap 的 Markdown 前端编辑器 + Hermes/Reasonix agent + simplemindmap/office 插件 + Wolai 主题 Web 壳 + 鉴权控制面`
>
> 当前上位主线:local-first workspace 是默认数据形态,本地 `.md` 是页面正文真相,Convex / 服务端只作为控制面、cloud source、compat 和 sync replica。入口设计见:
> 当前上位主线:local-first MVP 已初步完成;local-first workspace 是默认数据形态,本地 `.md` 是页面正文真相,Rust kernel / projection / command 持有语义,Convex / 服务端只作为控制面、cloud source、compat 和 sync replica。入口设计见:
> `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/done/2-2-local-first-workspace-convex-control-plane-v1.md`
>
> 后续新增设计默认以 MVP 后阶段为基线:优先推进 `WorkspacePath/ObjectIdentity``DocumentBuffer/BufferStore`、Page Aggregate compat 瘦身、tree command context、live cache 统一、agent diff / 冲突合并 / 本地索引 / 分享同步产品化;不要再把旧 Convex / Next / BlockNote 主链当作新增能力默认方向。
>
> 状态口径以当前仓库真实代码为准:
> - `[done]`:对应阶段或收口目标已经在当前主线代码中成立
> - `[process]`:方向已进入主线,但仍在推进中
> - `[process-reference]`:仍在 `process/` 下保留,但只作为参考流程、候选合同、对照证据或长期设计边界;不代表当前可直接执行的 checklist
> - `[recycle]`:已废弃、已被后续稿件替代,统一归档到 `old/`
## 当前优先级入口
- `01-05` 当前有效主线与优先级,请先看:
`/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
- MVP 后阶段剩余 `process/` 的执行顺序,请看:
`/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-8-mvp-post-process-execution-order-v1.md`
## 主线顺序
@@ -53,6 +58,7 @@
- 主线设计稿完成后,必须从对应大类的 `process/` 移动到 `done/`
- 仅以当前真实代码为准判断是否完成,不能只按稿件自述判断。
- 对于未完成但当前不排期的候选设计、参考矩阵、对标流程和 review 原始证据,不移动到 `done/`,必须在文件头部标记 `process-reference` 并说明当前不作为 active implementation checklist。
- 废弃稿统一移动到 `old/` 对应大类下,再按历史成熟度放入 `process/``done/`
## 辅助目录
@@ -204,6 +204,7 @@ struct LocalAssetUploadFields {
file: LocalUploadFile,
root_uri: String,
document_id: String,
target_relative_path: Option<String>,
kind: String,
}
@@ -2314,12 +2315,21 @@ pub async fn upload_local_markdown_asset(
let fields = read_local_asset_upload_multipart(multipart).await?;
ensure_local_workspace_access(&context, &fields.root_uri)
.map_err(|error| error.with_context(&context))?;
let asset = write_local_markdown_asset(
let asset = if fields.target_relative_path.is_some() || fields.document_id.trim().is_empty() {
write_local_folder_file_upload(
&fields.root_uri,
fields.target_relative_path.as_deref().unwrap_or(""),
&fields.kind,
fields.file,
)?
} else {
write_local_markdown_asset(
&fields.root_uri,
&fields.document_id,
&fields.kind,
fields.file,
)?;
)?
};
Ok((StatusCode::OK, Json(json!({ "ok": true, "asset": asset }))))
}
@@ -2329,7 +2339,33 @@ pub async fn open_local_file(
) -> Result<(StatusCode, HeaderMap, Vec<u8>), WebError> {
ensure_local_workspace_read_access(&context, &query.root_uri)
.map_err(|error| error.with_context(&context))?;
let target = resolve_local_file_open_path(&query.root_uri, &query.path)?;
let target = resolve_local_open_path(&query.root_uri, &query.path)?;
let mut headers = HeaderMap::new();
if target.is_dir() {
if !query.download.unwrap_or(false) {
return Err(WebError::bad_request_code(
"local_file_open_is_directory",
"不能直接读取本地目录",
)
.with_context(&context));
}
let bytes = build_local_directory_tar_archive(&target).map_err(|error| {
WebError::bad_request_code(
"local_directory_download_failed",
format!("无法打包本地目录 {}: {error}", target.display()),
)
.with_context(&context)
})?;
headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_static("application/x-tar"),
);
headers.insert(
header::CONTENT_DISPOSITION,
content_disposition_attachment_for_filename(&local_directory_tar_filename(&target)),
);
return Ok((StatusCode::OK, headers, bytes));
}
let bytes = fs::read(&target).map_err(|error| {
WebError::bad_request_code(
"local_file_open_read_failed",
@@ -2337,12 +2373,11 @@ pub async fn open_local_file(
)
.with_context(&context)
})?;
let mut headers = HeaderMap::new();
headers.insert(header::CONTENT_TYPE, content_type_for_path(&target));
if query.download.unwrap_or(false) {
headers.insert(
header::CONTENT_DISPOSITION,
HeaderValue::from_static("attachment"),
content_disposition_attachment_for_path(&target),
);
}
Ok((StatusCode::OK, headers, bytes))
@@ -2485,7 +2520,7 @@ pub async fn write_local_resource(
))
}
fn resolve_local_file_open_path(root_uri: &str, relative_path: &str) -> Result<PathBuf, WebError> {
fn resolve_local_open_path(root_uri: &str, relative_path: &str) -> Result<PathBuf, WebError> {
let root_path = parse_file_root_uri(root_uri)?;
let canonical_root = root_path.canonicalize().map_err(|error| {
WebError::bad_request_code(
@@ -2519,6 +2554,11 @@ fn resolve_local_file_open_path(root_uri: &str, relative_path: &str) -> Result<P
"本地文件路径不能越过 root",
));
}
Ok(target)
}
fn resolve_local_file_open_path(root_uri: &str, relative_path: &str) -> Result<PathBuf, WebError> {
let target = resolve_local_open_path(root_uri, relative_path)?;
if target.is_dir() {
return Err(WebError::bad_request_code(
"local_file_open_is_directory",
@@ -2534,6 +2574,7 @@ async fn read_local_asset_upload_multipart(
let mut file: Option<LocalUploadFile> = None;
let mut root_uri = String::new();
let mut document_id = String::new();
let mut target_relative_path: Option<String> = None;
let mut kind = String::new();
while let Some(field) = multipart.next_field().await.map_err(|error| {
@@ -2583,6 +2624,9 @@ async fn read_local_asset_upload_multipart(
match name.as_str() {
"rootUri" => root_uri = value.trim().to_string(),
"documentId" => document_id = value.trim().to_string(),
"targetRelativePath" | "targetDirectoryPath" => {
target_relative_path = Some(value.trim().to_string())
}
"kind" => kind = value.trim().to_string(),
_ => {}
}
@@ -2591,7 +2635,10 @@ async fn read_local_asset_upload_multipart(
let file = file.ok_or_else(|| {
WebError::bad_request_code("local_asset_upload_file_missing", "缺少 file")
})?;
if file.bytes.is_empty() || root_uri.is_empty() || document_id.is_empty() {
if file.bytes.is_empty()
|| root_uri.is_empty()
|| (document_id.is_empty() && target_relative_path.is_none())
{
return Err(WebError::bad_request_code(
"local_asset_upload_required_missing",
"缺少必要参数",
@@ -2601,10 +2648,85 @@ async fn read_local_asset_upload_multipart(
file,
root_uri,
document_id,
target_relative_path,
kind,
})
}
pub(crate) fn write_local_folder_file_upload(
root_uri: &str,
target_relative_path: &str,
kind: &str,
file: LocalUploadFile,
) -> Result<Value, WebError> {
let root_path = parse_file_root_uri(root_uri)?;
let canonical_root = root_path.canonicalize().map_err(|error| {
WebError::bad_request_code(
"local_folder_unavailable",
format!("无法访问本地文件夹: {error}"),
)
})?;
if !canonical_root.is_dir() {
return Err(WebError::bad_request_code(
"local_folder_not_directory",
"本地 rootUri 必须指向目录",
));
}
let target_relative_path = target_relative_path.trim();
let requested = if target_relative_path.is_empty() {
Path::new(".")
} else {
Path::new(target_relative_path)
};
if requested.is_absolute()
|| requested
.components()
.any(|component| matches!(component, std::path::Component::ParentDir))
{
return Err(WebError::bad_request_code(
"local_folder_upload_root_escape",
"上传目标目录不能越过 root",
));
}
let target_dir = canonical_root
.join(requested)
.canonicalize()
.map_err(|error| {
WebError::bad_request_code(
"local_folder_upload_target_missing",
format!("找不到上传目标目录: {error}"),
)
})?;
if !target_dir.starts_with(&canonical_root) || !target_dir.is_dir() {
return Err(WebError::bad_request_code(
"local_folder_upload_target_invalid",
"上传目标必须是本地 root 内的目录",
));
}
let sanitized_name = sanitize_file_name(&file.name, "附件");
let target = next_available_raw_path(&target_dir, &sanitized_name);
fs::write(&target, &file.bytes).map_err(|error| {
WebError::bad_request_code(
"local_folder_upload_write_failed",
format!("无法写入本地文件 {}: {error}", target.display()),
)
})?;
let root_relative_path = normalize_relative_path(&canonical_root, &target)?;
let asset_type = local_upload_asset_type(kind, &file.content_type);
Ok(json!({
"id": format!("local-file:{root_relative_path}"),
"asset_type": asset_type,
"file_name": target.file_name().and_then(|value| value.to_str()).unwrap_or(&sanitized_name),
"mime_type": file.content_type,
"file_size": file.bytes.len(),
"file_url": root_relative_path,
"sourcePath": root_relative_path,
"sourceKind": "local_folder",
"rootUri": file_uri_for_path(&canonical_root),
"rootRelativePath": root_relative_path,
}))
}
pub(crate) fn write_local_markdown_asset(
root_uri: &str,
document_id: &str,
@@ -6532,6 +6654,217 @@ fn content_type_for_path(path: &Path) -> HeaderValue {
HeaderValue::from_static(content_type)
}
fn percent_encode_content_disposition_filename(value: &str) -> String {
let mut encoded = String::new();
for byte in value.as_bytes() {
match *byte {
b'A'..=b'Z'
| b'a'..=b'z'
| b'0'..=b'9'
| b'!'
| b'#'
| b'$'
| b'&'
| b'+'
| b'-'
| b'.'
| b'^'
| b'_'
| b'`'
| b'|'
| b'~' => encoded.push(*byte as char),
_ => encoded.push_str(&format!("%{byte:02X}")),
}
}
encoded
}
fn content_disposition_attachment_for_path(path: &Path) -> HeaderValue {
let filename = path
.file_name()
.and_then(|value| value.to_str())
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("download");
content_disposition_attachment_for_filename(filename)
}
fn content_disposition_attachment_for_filename(filename: &str) -> HeaderValue {
let filename = filename.trim();
let filename = if filename.is_empty() {
"download"
} else {
filename
};
let ascii_fallback = filename
.chars()
.map(|ch| {
if ch.is_ascii_alphanumeric() || matches!(ch, '.' | '-' | '_' | ' ') {
ch
} else {
'_'
}
})
.collect::<String>();
let ascii_fallback = if ascii_fallback.trim().is_empty() {
"download".to_string()
} else {
ascii_fallback
};
let quoted = ascii_fallback.replace('\\', "\\\\").replace('"', "\\\"");
let encoded = percent_encode_content_disposition_filename(filename);
HeaderValue::from_str(&format!(
"attachment; filename=\"{quoted}\"; filename*=UTF-8''{encoded}"
))
.unwrap_or_else(|_| HeaderValue::from_static("attachment"))
}
fn local_directory_tar_filename(path: &Path) -> String {
let name = path
.file_name()
.and_then(|value| value.to_str())
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("folder");
format!("{name}.tar")
}
fn tar_octal(value: u64, width: usize) -> Vec<u8> {
let mut encoded = format!("{value:o}").into_bytes();
let max_digits = width.saturating_sub(1);
if encoded.len() > max_digits {
encoded = vec![b'7'; max_digits];
}
let mut out = vec![b'0'; max_digits.saturating_sub(encoded.len())];
out.extend(encoded);
out.push(0);
out
}
fn split_tar_path(path: &str) -> Result<(&str, &str), std::io::Error> {
let bytes = path.as_bytes();
if bytes.len() <= 100 {
return Ok(("", path));
}
let mut best: Option<usize> = None;
for (index, ch) in path.char_indices() {
if ch == '/' {
let prefix_len = path[..index].as_bytes().len();
let name_len = path[index + 1..].as_bytes().len();
if prefix_len <= 155 && name_len <= 100 {
best = Some(index);
}
}
}
if let Some(index) = best {
return Ok((&path[..index], &path[index + 1..]));
}
Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"tar entry path is too long",
))
}
fn append_tar_header(
out: &mut Vec<u8>,
path: &str,
size: u64,
typeflag: u8,
mode: u64,
mtime: u64,
) -> Result<(), std::io::Error> {
let normalized = path.trim_start_matches('/').replace('\\', "/");
let (prefix, name) = split_tar_path(&normalized)?;
let mut header = [0u8; 512];
header[0..name.as_bytes().len()].copy_from_slice(name.as_bytes());
let mode = tar_octal(mode, 8);
header[100..108].copy_from_slice(&mode);
header[108..116].copy_from_slice(&tar_octal(0, 8));
header[116..124].copy_from_slice(&tar_octal(0, 8));
header[124..136].copy_from_slice(&tar_octal(size, 12));
header[136..148].copy_from_slice(&tar_octal(mtime, 12));
header[148..156].fill(b' ');
header[156] = typeflag;
header[257..263].copy_from_slice(b"ustar\0");
header[263..265].copy_from_slice(b"00");
if !prefix.is_empty() {
header[345..345 + prefix.as_bytes().len()].copy_from_slice(prefix.as_bytes());
}
let checksum = header.iter().map(|byte| u32::from(*byte)).sum::<u32>();
let checksum_bytes = format!("{checksum:06o}\0 ").into_bytes();
header[148..156].copy_from_slice(&checksum_bytes);
out.extend_from_slice(&header);
Ok(())
}
fn append_tar_padding(out: &mut Vec<u8>, size: usize) {
let remainder = size % 512;
if remainder > 0 {
out.extend(std::iter::repeat(0).take(512 - remainder));
}
}
fn append_local_directory_tar_entries(
out: &mut Vec<u8>,
root: &Path,
current: &Path,
base_name: &str,
) -> Result<(), std::io::Error> {
let metadata = fs::symlink_metadata(current)?;
if metadata.file_type().is_symlink() {
return Ok(());
}
let relative = current.strip_prefix(root).unwrap_or(current);
let mut entry_name = if relative.as_os_str().is_empty() {
base_name.to_string()
} else {
format!(
"{base_name}/{}",
relative.to_string_lossy().replace('\\', "/")
)
};
let mtime = metadata
.modified()
.ok()
.and_then(|time| time.duration_since(UNIX_EPOCH).ok())
.map(|duration| duration.as_secs())
.unwrap_or(0);
if metadata.is_dir() {
if !entry_name.ends_with('/') {
entry_name.push('/');
}
append_tar_header(out, &entry_name, 0, b'5', 0o755, mtime)?;
let mut children = fs::read_dir(current)?.collect::<Result<Vec<_>, _>>()?;
children.sort_by_key(|entry| entry.file_name());
for child in children {
append_local_directory_tar_entries(out, root, &child.path(), base_name)?;
}
return Ok(());
}
if metadata.is_file() {
append_tar_header(out, &entry_name, metadata.len(), b'0', 0o644, mtime)?;
let bytes = fs::read(current)?;
out.extend_from_slice(&bytes);
append_tar_padding(out, bytes.len());
}
Ok(())
}
fn build_local_directory_tar_archive(directory: &Path) -> Result<Vec<u8>, std::io::Error> {
let canonical = directory.canonicalize()?;
let base_name = canonical
.file_name()
.and_then(|value| value.to_str())
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("folder")
.to_string();
let mut out = Vec::new();
append_local_directory_tar_entries(&mut out, &canonical, &canonical, &base_name)?;
out.extend_from_slice(&[0u8; 1024]);
Ok(out)
}
fn editor_block_table_to_markdown(block: &Value) -> String {
let table = block
.get("props")
@@ -8873,6 +9206,90 @@ fn main() {}
let _ = std::fs::remove_dir_all(&root);
}
#[tokio::test]
async fn local_file_open_download_sets_content_disposition_filename() {
let root = temp_root("mnote-local-file-open-download-filename");
init_workspace(&root);
std::fs::write(root.join("报告 2026.docx"), b"docx").expect("write docx");
let root_uri = format!("file://{}", root.display());
let mut headers = HeaderMap::new();
headers.insert("x-mnote-actor-id", "user_test".parse().unwrap());
headers.insert("x-mnote-actor-type", "user".parse().unwrap());
let context = RequestContext::from_http_parts(
&Method::GET,
&"/api/local-folder/files/open".parse().expect("uri"),
&headers,
);
let (_, response_headers, _) = open_local_file(
Extension(context),
Query(LocalFileOpenQuery {
root_uri,
path: "报告 2026.docx".into(),
download: Some(true),
}),
)
.await
.expect("download local file");
let disposition = response_headers
.get(axum::http::header::CONTENT_DISPOSITION)
.and_then(|value| value.to_str().ok())
.expect("content disposition");
assert!(disposition.starts_with("attachment;"));
assert!(disposition.contains("filename=\"__ 2026.docx\""));
assert!(
disposition.contains("filename*=UTF-8''%E6%8A%A5%E5%91%8A%202026.docx"),
"{disposition}"
);
let _ = std::fs::remove_dir_all(&root);
}
#[tokio::test]
async fn local_folder_open_downloads_directory_as_tar() {
let root = temp_root("mnote-local-folder-open-download-directory");
init_workspace(&root);
let reports = root.join("Reports");
std::fs::create_dir_all(&reports).expect("create reports");
std::fs::write(reports.join("summary.txt"), b"summary").expect("write summary");
let root_uri = format!("file://{}", root.display());
let mut headers = HeaderMap::new();
headers.insert("x-mnote-actor-id", "user_test".parse().unwrap());
headers.insert("x-mnote-actor-type", "user".parse().unwrap());
let context = RequestContext::from_http_parts(
&Method::GET,
&"/api/local-folder/files/open".parse().expect("uri"),
&headers,
);
let (_, response_headers, bytes) = open_local_file(
Extension(context),
Query(LocalFileOpenQuery {
root_uri,
path: "Reports".into(),
download: Some(true),
}),
)
.await
.expect("download local directory");
assert_eq!(
response_headers
.get(axum::http::header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok()),
Some("application/x-tar")
);
let disposition = response_headers
.get(axum::http::header::CONTENT_DISPOSITION)
.and_then(|value| value.to_str().ok())
.expect("content disposition");
assert!(disposition.contains("filename=\"Reports.tar\""));
assert!(bytes.windows(5).any(|window| window == b"ustar"));
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn local_file_tree_classifies_mindmap_and_office_assets() {
let root = temp_root("mnote-local-filetree-resource-kinds");
+15 -3
View File
@@ -799,7 +799,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
if (typeof runtime.default !== 'function' || typeof runtime.mount !== 'function' || typeof runtime.unmount !== 'function') {
throw new Error('island runtime ');
}
await runtime.default(wasmUrl);
await runtime.default({ module_or_path: wasmUrl });
if (typeof runtime.mount_mindmap_shell === 'function' && typeof runtime.unmount_mindmap_shell === 'function') {
window.__MNOTE_MINDMAP_RUST_SHELL__ = {
mount: runtime.mount_mindmap_shell,
@@ -3260,6 +3260,14 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
}
};
const currentWebShellDocumentId = () => {
const fromBody = document.body?.dataset?.documentId || '';
if (fromBody) return String(fromBody).trim();
const pageTab = document.querySelector('[data-mnote-main-tab="page"]');
if (pageTab instanceof HTMLElement) return String(pageTab.getAttribute('data-document-id') || '').trim();
return '';
};
const normalizeResourceTabKind = (input) => {
const kind = String(input?.kind || '').trim().toLowerCase();
const title = String(input?.title || input?.fileName || input?.path || '').trim().toLowerCase();
@@ -3365,7 +3373,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
: '';
const pageEntry = {
objectIdentity: 'page',
documentId: String(nodes.pageTab?.getAttribute?.('data-document-id') || currentDocumentId() || '').trim(),
documentId: String(nodes.pageTab?.getAttribute?.('data-document-id') || currentWebShellDocumentId() || '').trim(),
workspaceId: String(nodes.pageTab?.getAttribute?.('data-workspace-id') || currentWebShellWorkspaceId() || '').trim(),
title: pageTitle || '',
kind: 'page',
@@ -3772,7 +3780,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
};
const openMindmapResourceTab = async (entry, input) => {
const documentId = String(input.documentId || currentDocumentId() || '').trim();
const documentId = String(input.documentId || currentWebShellDocumentId() || '').trim();
const mindmapId = String(input.mindmapId || input.assetId || '').trim();
if (!documentId || !mindmapId) throw new Error('mindmap_resource_identity_missing');
const targetUrl = new URL(`/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}`, window.location.origin);
@@ -4645,6 +4653,10 @@ mod tests {
assert!(html.contains("getOpenEditorsSnapshot"));
assert!(html.contains("bindMainEditorTabStrip"));
assert!(html.contains("data-mnote-tab-strip-bound"));
assert!(html.contains("currentWebShellDocumentId"));
assert!(!html
.contains("nodes.pageTab?.getAttribute?.('data-document-id') || currentDocumentId()"));
assert!(html.contains("runtime.default({ module_or_path: wasmUrl })"));
assert!(html.contains("positionSlashMenuForRoot"));
assert!(html.contains("menu.style.position = 'fixed';"));
assert!(html.contains("installGlobalSlashMenuPositioning();"));
+249 -10
View File
@@ -1764,6 +1764,18 @@ const SIDEBAR_TREE_JS: &str = r##"
return String(item && item.title || '').trim() || '';
}
function fileWorkspaceRelativePath(item) {
var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
var workspacePath = meta.workspacePath && typeof meta.workspacePath === 'object' ? meta.workspacePath : {};
var fromWorkspacePath = String(workspacePath.relativePath || '').trim();
if (fromWorkspacePath) return fromWorkspacePath;
var extra = meta.extra && typeof meta.extra === 'object' ? meta.extra : {};
var source = extra.source && typeof extra.source === 'object' ? extra.source : {};
var fromSource = String(source.relativePath || '').trim();
if (fromSource) return fromSource;
return String(item && (item.relativePath || item.rootRelativePath) || '').trim();
}
function groupRowsByParent(rows) {
var ids = new Set(rows.map(nodeIdOf).filter(Boolean));
var grouped = new Map();
@@ -1890,6 +1902,7 @@ const SIDEBAR_TREE_JS: &str = r##"
var parent = parentIdOf(item);
var documentId = fileDocumentId(item);
var assetId = fileAssetId(item);
var relativePath = fileWorkspaceRelativePath(item);
var objectIdentity = fileObjectIdentity(item);
var iconKind = iconKindOf(item);
var title = isFileTreeProjectionPageRow(rowKind, assetId)
@@ -1909,7 +1922,7 @@ const SIDEBAR_TREE_JS: &str = r##"
var childHtml = expandable
? '<ul class="tree-children' + (expanded ? '' : ' tree-children--collapsed') + '">' + renderFileRows(nodeId, grouped, activeId, activeRowId) + '</ul>'
: '';
return '<li class="tree-node" data-node-id="' + escapeHtml(nodeId) + '"><div class="tree-row" role="treeitem" aria-level="' + (depth + 1) + '" aria-expanded="' + String(expandable && expanded) + '" data-rust-rendered-row="filetree" data-testid="' + testId + '" data-row-id="' + escapeHtml(rowId) + '" data-row-kind="' + escapeHtml(rowKind) + '" data-node-id="' + escapeHtml(nodeId) + '"' + (parent ? ' data-parent-id="' + escapeHtml(parent) + '"' : '') + ' data-document-id="' + escapeHtml(documentId) + '" data-doc-id="' + escapeHtml(documentId) + '" data-asset-id="' + escapeHtml(assetId) + '" data-object-identity="' + escapeHtml(objectIdentityAttr(objectIdentity)) + '" data-object-kind="' + escapeHtml(String(objectIdentity.objectKind || '')) + '" data-capabilities="' + escapeHtml(fileCapabilitiesAttr(item)) + '" data-shell-mode="filetree" data-selected="' + String(selected) + '" data-active="false" draggable="true">' + toggle + '<span class="tree-kind-badge" data-kind="' + escapeHtml(iconKind) + '" aria-hidden="true"></span><button type="button" class="tree-link" data-rust-action="open" data-row-id="' + escapeHtml(rowId) + '" data-document-id="' + escapeHtml(documentId) + '" data-asset-id="' + escapeHtml(assetId) + '"><span class="tree-link-title">' + escapeHtml(title) + '</span></button><div class="tree-actions">' + createAction + '<button type="button" class="tree-action" data-testid="filetree-action-menu" data-rust-action="menu" data-row-id="' + escapeHtml(rowId) + '" aria-label="更多操作"></button></div></div>' + childHtml + '</li>';
return '<li class="tree-node" data-node-id="' + escapeHtml(nodeId) + '"><div class="tree-row" role="treeitem" aria-level="' + (depth + 1) + '" aria-expanded="' + String(expandable && expanded) + '" data-rust-rendered-row="filetree" data-testid="' + testId + '" data-row-id="' + escapeHtml(rowId) + '" data-row-kind="' + escapeHtml(rowKind) + '" data-node-id="' + escapeHtml(nodeId) + '"' + (parent ? ' data-parent-id="' + escapeHtml(parent) + '"' : '') + ' data-document-id="' + escapeHtml(documentId) + '" data-doc-id="' + escapeHtml(documentId) + '" data-asset-id="' + escapeHtml(assetId) + '" data-local-relative-path="' + escapeHtml(relativePath) + '" data-object-identity="' + escapeHtml(objectIdentityAttr(objectIdentity)) + '" data-object-kind="' + escapeHtml(String(objectIdentity.objectKind || '')) + '" data-capabilities="' + escapeHtml(fileCapabilitiesAttr(item)) + '" data-shell-mode="filetree" data-selected="' + String(selected) + '" data-active="false" draggable="true">' + toggle + '<span class="tree-kind-badge" data-kind="' + escapeHtml(iconKind) + '" aria-hidden="true"></span><button type="button" class="tree-link" data-rust-action="open" data-row-id="' + escapeHtml(rowId) + '" data-document-id="' + escapeHtml(documentId) + '" data-asset-id="' + escapeHtml(assetId) + '" data-local-relative-path="' + escapeHtml(relativePath) + '"><span class="tree-link-title">' + escapeHtml(title) + '</span></button><div class="tree-actions">' + createAction + '<button type="button" class="tree-action" data-testid="filetree-action-menu" data-rust-action="menu" data-row-id="' + escapeHtml(rowId) + '" aria-label="更多操作"></button></div></div>' + childHtml + '</li>';
}).join('');
}
@@ -2581,6 +2594,15 @@ const SIDEBAR_TREE_JS: &str = r##"
function fallbackFileTreeUploadTarget(detail) {
var workspaceId = String(detail && detail.workspaceId || resolveWorkspaceId(document.body) || '').trim();
var documentId = String(detail && detail.documentId || currentDocumentId() || '').trim();
if (detail && Object.prototype.hasOwnProperty.call(detail, 'targetRelativePath')) {
return {
workspaceId: workspaceId,
targetDocumentId: documentId,
targetMindmapId: null,
targetSubPath: null,
targetRelativePath: String(detail.targetRelativePath || '')
};
}
if (!workspaceId || !documentId) {
throw new Error('');
}
@@ -2593,8 +2615,15 @@ const SIDEBAR_TREE_JS: &str = r##"
}
async function resolveFileTreeUploadTarget(detail) {
if (detail && Object.prototype.hasOwnProperty.call(detail, 'targetRelativePath')) {
return fallbackFileTreeUploadTarget(detail || {});
}
try {
return await preflightFileTreeUploadTarget(detail || {});
var plan = await preflightFileTreeUploadTarget(detail || {});
if (detail && Object.prototype.hasOwnProperty.call(detail, 'targetRelativePath')) {
plan.targetRelativePath = String(detail.targetRelativePath || '');
}
return plan;
} catch (error) {
console.warn('[mnote upload] upload target preflight fallback', error);
return fallbackFileTreeUploadTarget(detail || {});
@@ -2869,6 +2898,16 @@ const SIDEBAR_TREE_JS: &str = r##"
var detail = detailFromEditorAttachmentLink(link);
var assetId = String(detail && detail.assetId || '').trim();
if (!assetId) return;
var localFilePath = localFilePathFromAssetId(assetId);
if (localFilePath) {
var localMeta = {
assetId: assetId,
fileSize: String(detail && detail.fileSize || '').trim()
};
attachmentMetaCache[assetId] = localMeta;
applyEditorAttachmentMeta(link, localMeta);
return;
}
if (attachmentMetaCache[assetId]) {
applyEditorAttachmentMeta(link, attachmentMetaCache[assetId]);
return;
@@ -3258,13 +3297,15 @@ const SIDEBAR_TREE_JS: &str = r##"
if (currentSourceKind() === 'local_folder') {
var rootUri = (new URLSearchParams(window.location.search).get('rootUri') || '').trim();
var documentId = String(plan && plan.targetDocumentId || currentDocumentId() || '').trim();
if (!rootUri || !documentId) {
var hasFolderTarget = plan && Object.prototype.hasOwnProperty.call(plan, 'targetRelativePath');
if (!rootUri || (!documentId && !hasFolderTarget)) {
throw new Error(' Markdown rootUri documentId');
}
var localForm = new FormData();
localForm.append('file', file);
localForm.append('rootUri', rootUri);
localForm.append('documentId', documentId);
if (documentId) localForm.append('documentId', documentId);
if (hasFolderTarget) localForm.append('targetRelativePath', String(plan.targetRelativePath || ''));
localForm.append('kind', file && String(file.type || '').indexOf('image/') === 0 ? 'image' : 'attachment');
var localResponse = await fetch('/api/local-folder/assets/upload', {
method: 'POST',
@@ -3546,6 +3587,28 @@ const SIDEBAR_TREE_JS: &str = r##"
return Promise.resolve();
}
function triggerBrowserDownload(url) {
if (!url) return false;
var link = document.createElement('a');
link.href = url;
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.download = '';
link.style.position = 'fixed';
link.style.left = '-9999px';
link.style.top = '0';
document.body.appendChild(link);
try {
link.click();
} catch (_) {
if (typeof window.open === 'function') window.open(url, '_blank', 'noopener,noreferrer');
}
window.setTimeout(function() {
if (link.parentElement) link.parentElement.removeChild(link);
}, 1000);
return true;
}
function recordFileTreeAction(action, detail) {
var normalized = String(action || '').trim() || 'unknown';
var rowId = String(detail && detail.rowId || '').trim();
@@ -3698,6 +3761,10 @@ const SIDEBAR_TREE_JS: &str = r##"
var workspaceId = detail.workspaceId || resolveWorkspaceId(trigger || document.body);
var title = detail.title || '';
var isAsset = detail.contextKind === 'filetree' && detail.assetId && detail.rowKind !== 'document' && detail.rowKind !== 'index';
if (detail.contextKind === 'filetree' && action === 'download') {
downloadSelectedFileTreeAssetRows(detail, trigger);
return;
}
if (isAsset && action === 'new-window') {
recordFileTreeAction('new-window', detail);
void openConvexAssetFromFileTree({ ...detail, openTarget: 'new-window' });
@@ -3903,6 +3970,7 @@ const SIDEBAR_TREE_JS: &str = r##"
menu.setAttribute('data-kind', kind);
var isAttachment = kind === 'attachment';
var isAsset = kind === 'filetree' && detail.assetId && detail.rowKind !== 'document' && detail.rowKind !== 'index';
var isFileTreeDownload = kind === 'filetree' && detail.downloadable;
var items = isAttachment ? [
{ action: 'duplicate', icon: 'file_copy', label: '', shortcut: 'Ctrl + D' },
{ action: 'delete-trash', icon: 'delete', label: '', shortcut: 'Del', danger: true },
@@ -3926,6 +3994,7 @@ const SIDEBAR_TREE_JS: &str = r##"
] : isAsset ? [
{ action: 'open-edit-mode', icon: 'edit_note', label: '使' },
{ action: 'new-window', icon: 'open_in_new', label: '' },
{ action: 'download', icon: 'download', label: Number(detail.selectedDownloadCount || detail.selectedAssetCount || 0) > 1 ? ' ' + Number(detail.selectedDownloadCount || detail.selectedAssetCount || 0) + ' ' : '' },
{ action: 'rename', icon: 'edit', label: '', shortcut: 'F2' },
{ action: 'copy-id', icon: 'tag', label: ' ID' },
{ action: 'delete-trash', icon: 'delete', label: '', shortcut: 'Del', danger: true },
@@ -3935,6 +4004,7 @@ const SIDEBAR_TREE_JS: &str = r##"
{ action: 'reveal', icon: 'my_location', label: 'Reveal' }
] : kind === 'filetree' ? [
{ action: 'open-right', icon: 'right_panel_open', label: '', shortcut: 'Alt+' },
{ action: 'download', icon: 'download', label: Number(detail.selectedDownloadCount || 0) > 1 ? ' ' + Number(detail.selectedDownloadCount || 0) + ' ' : '', disabled: !isFileTreeDownload, title: isFileTreeDownload ? '' : '' },
{ separator: true },
{ action: 'copy-link', icon: 'link', label: '访' },
{ action: 'copy-reference-inline', icon: 'content_copy', label: '' },
@@ -3987,19 +4057,31 @@ const SIDEBAR_TREE_JS: &str = r##"
function openFileTreeContextMenu(row, x, y, trigger) {
if (!(row instanceof HTMLElement)) return;
var documentId = row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || '';
var selectedDownloadRows = selectedSidebarFileTreeRowsForDownload(row);
var selectedAssetRows = selectedDownloadRows.filter(isFileTreeDownloadableAssetRow);
openTreeContextMenu('filetree', {
documentId: documentId,
rowId: row.getAttribute('data-row-id') || '',
rowKind: row.getAttribute('data-row-kind') || '',
assetId: row.getAttribute('data-asset-id') || '',
localRelativePath: fileTreeRowLocalRelativePath(row),
title: rowTitle(row),
workspaceId: resolveWorkspaceId(row)
workspaceId: resolveWorkspaceId(row),
downloadable: isFileTreeDownloadableRow(row),
selectedDownloadCount: selectedDownloadRows.length,
selectedDownloadRowIds: selectedDownloadRows.map(function(downloadRow) { return downloadRow.getAttribute('data-row-id') || ''; }).filter(Boolean),
selectedAssetCount: selectedAssetRows.length,
selectedAssetRowIds: selectedAssetRows.map(function(assetRow) { return assetRow.getAttribute('data-row-id') || ''; }).filter(Boolean)
}, x, y, trigger || row);
}
function visibleFileTreeRows() {
return Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]'))
.filter(function(row) { return row instanceof HTMLElement && row.offsetParent !== null; });
.filter(function(row) {
if (!(row instanceof HTMLElement)) return false;
if (row.closest('.tree-children--collapsed')) return false;
return row.offsetParent !== null || row.getClientRects().length > 0;
});
}
function syncSidebarFileTreeSelection() {
@@ -4116,11 +4198,130 @@ const SIDEBAR_TREE_JS: &str = r##"
return '';
}
function decodeLocalEncodedPath(value) {
var path = String(value || '').trim().replace(/~2F/g, '/');
if (!path) return '';
try {
return decodeURIComponent(path);
} catch (_) {
return path;
}
}
function fileTreeRowLocalRelativePath(row) {
if (!(row instanceof HTMLElement)) return '';
var direct = String(row.getAttribute('data-local-relative-path') || '').trim();
if (direct) return direct;
var assetPath = localFilePathFromAssetId(fileTreeRowAssetId(row));
if (assetPath) return assetPath;
var rowId = String(row.getAttribute('data-row-id') || '').trim();
var nodeId = String(row.getAttribute('data-node-id') || '').trim();
var documentId = fileTreeRowDocumentId(row);
var kind = fileTreeRowKind(row);
if (rowId.indexOf('local:asset:') === 0) return rowId.slice('local:asset:'.length);
if (rowId.indexOf('local:markdown:') === 0) return rowId.slice('local:markdown:'.length);
if (rowId.indexOf('local:folder:') === 0) return rowId.slice('local:folder:'.length);
if (rowId.indexOf('local:node:') === 0) return rowId.slice('local:node:'.length);
if (nodeId.indexOf('local:node:') === 0) return nodeId.slice('local:node:'.length);
if ((kind === 'document' || kind === 'doc' || kind === 'markdown') && documentId.indexOf('local-md:') === 0) {
return decodeLocalEncodedPath(documentId.slice('local-md:'.length));
}
if ((kind === 'folder' || kind === 'directory' || kind === 'index') && documentId.indexOf('local-dir:') === 0) {
return decodeLocalEncodedPath(documentId.slice('local-dir:'.length));
}
return '';
}
function fileTreeRowKind(row) {
if (!(row instanceof HTMLElement)) return '';
return String(row.getAttribute('data-row-kind') || '').trim();
}
function isFileTreeDownloadableAssetRow(row) {
if (!(row instanceof HTMLElement)) return false;
var kind = fileTreeRowKind(row);
if (kind === 'document' || kind === 'doc' || kind === 'index' || kind === 'markdown' || kind === 'folder' || kind === 'directory') return false;
return Boolean(fileTreeRowAssetId(row));
}
function isFileTreeDownloadableRow(row) {
if (!(row instanceof HTMLElement)) return false;
if (currentSourceKind() !== 'local_folder') return isFileTreeDownloadableAssetRow(row);
return Boolean(fileTreeRowLocalRelativePath(row) || isFileTreeDownloadableAssetRow(row));
}
function fileTreeAssetDownloadDetail(row) {
if (!isFileTreeDownloadableRow(row)) return null;
var title = rowTitle(row);
var relativePath = fileTreeRowLocalRelativePath(row);
var assetId = fileTreeRowAssetId(row) || (relativePath ? 'local-file:' + relativePath : '');
return {
contextKind: 'filetree',
documentId: fileTreeRowDocumentId(row),
rowId: row.getAttribute('data-row-id') || '',
rowKind: fileTreeRowKind(row),
assetId: assetId,
localRelativePath: relativePath,
title: title,
fileName: title,
workspaceId: resolveWorkspaceId(row)
};
}
function selectedSidebarFileTreeRowsForDownload(contextRow) {
var contextRowId = contextRow instanceof HTMLElement ? contextRow.getAttribute('data-row-id') || '' : '';
var selectedRows = selectedSidebarFileTreeRows().filter(isFileTreeDownloadableRow);
if (contextRowId && sidebarFileTreeSelection.selectedRowIds.has(contextRowId) && selectedRows.length > 0) return selectedRows;
return isFileTreeDownloadableRow(contextRow) ? [contextRow] : selectedRows;
}
function selectedSidebarFileTreeAssetRowsForDownload(contextRow) {
return selectedSidebarFileTreeRowsForDownload(contextRow).filter(isFileTreeDownloadableAssetRow);
}
function downloadSelectedFileTreeAssetRows(detail, trigger) {
var contextRow = trigger && trigger.closest ? trigger.closest('.tree-row[data-shell-mode="filetree"]') : trigger;
if (!(contextRow instanceof HTMLElement) && detail && detail.rowId) {
contextRow = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(detail.rowId) + '"]');
}
var rows = selectedSidebarFileTreeRowsForDownload(contextRow);
var seen = new Set();
var downloads = [];
rows.forEach(function(row) {
var item = fileTreeAssetDownloadDetail(row);
var key = item && (item.localRelativePath || item.assetId || item.rowId);
if (!item || !key || seen.has(key)) return;
seen.add(key);
downloads.push(item);
});
if (downloads.length === 0 && detail && detail.assetId) {
downloads.push(Object.assign({}, detail, { fileName: detail.fileName || detail.title || '' }));
} else if (downloads.length === 0 && detail && detail.localRelativePath) {
downloads.push(Object.assign({}, detail, {
assetId: detail.assetId || 'local-file:' + detail.localRelativePath,
fileName: detail.fileName || detail.title || ''
}));
}
if (downloads.length === 0) {
recordFileTreeActionStatus('blocked', Object.assign({}, detail || {}, { reason: 'no-downloadable-assets' }));
return false;
}
var primary = Object.assign({}, downloads[0], {
count: downloads.length,
assetIds: downloads.map(function(item) { return item.assetId || ''; }).filter(Boolean),
localRelativePaths: downloads.map(function(item) { return item.localRelativePath || ''; }).filter(Boolean)
});
recordFileTreeAction(downloads.length > 1 ? 'bulk-download' : 'download', primary);
recordFileTreeActionStatus('requested', primary);
document.documentElement.setAttribute('data-mnote-filetree-download-count', String(downloads.length));
document.documentElement.setAttribute('data-mnote-filetree-download-asset-ids', downloads.map(function(item) { return item.assetId || ''; }).filter(Boolean).join(','));
document.documentElement.setAttribute('data-mnote-filetree-download-paths', downloads.map(function(item) { return item.localRelativePath || ''; }).filter(Boolean).join(','));
downloads.forEach(function(item) {
void openEditorAttachmentDownload(item);
});
return true;
}
function hasSelectedDocumentAncestor(row, selectedDocRowIds) {
var node = row instanceof HTMLElement ? row.closest('.tree-node') : null;
while (node && node.parentElement) {
@@ -8010,7 +8211,7 @@ const SIDEBAR_TREE_JS: &str = r##"
if (localFilePath) {
var localDownloadUrl = buildLocalFileOpenUrl(localFilePath, true);
if (localDownloadUrl) {
window.open(localDownloadUrl, '_blank', 'noopener,noreferrer');
triggerBrowserDownload(localDownloadUrl);
return;
}
}
@@ -8028,15 +8229,25 @@ const SIDEBAR_TREE_JS: &str = r##"
}
} catch (_) {}
}
var localRelativePath = String(detail.localRelativePath || '').trim();
if (localRelativePath) {
var localPathDownloadUrl = buildLocalFileOpenUrl(localRelativePath, true);
if (localPathDownloadUrl) {
triggerBrowserDownload(localPathDownloadUrl);
return;
}
}
var target = detail.fileUrl || detail.href;
if (!target) return;
window.open(target, '_blank', 'noopener,noreferrer');
}
function openEditorAttachmentMenu(link, trigger) {
function openEditorAttachmentMenu(link, trigger, point) {
var detail = detailFromEditorAttachmentLink(link);
var rect = trigger && trigger.getBoundingClientRect ? trigger.getBoundingClientRect() : link.getBoundingClientRect();
openTreeContextMenu('attachment', detail, rect.right, rect.bottom + 4, trigger || link);
var x = point && typeof point.x === 'number' ? point.x : rect.right;
var y = point && typeof point.y === 'number' ? point.y : rect.bottom + 4;
openTreeContextMenu('attachment', detail, x, y, trigger || link);
}
function openEditorAttachmentLink(link) {
@@ -8485,7 +8696,9 @@ const SIDEBAR_TREE_JS: &str = r##"
}
if (fileAction === 'menu') {
e.preventDefault();
if (rowId && !sidebarFileTreeSelection.selectedRowIds.has(rowId)) {
selectSidebarFileTreeRow(fileRow, { ctrlKey: false, metaKey: false, shiftKey: false });
}
var point = rowCenter(fileBtn || fileRow);
dispatchSidebarEvent('tree.filetree.context-menu', { rowId: rowId, rowKind: rowKind, documentId: documentId || null, assetId: assetId || null });
openFileTreeContextMenu(fileRow, point.x, point.y, fileBtn || fileRow);
@@ -8559,6 +8772,14 @@ const SIDEBAR_TREE_JS: &str = r##"
});
document.addEventListener('contextmenu', function(event) {
var editorAttachmentLink = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row');
if (editorAttachmentLink instanceof HTMLAnchorElement) {
event.preventDefault();
event.stopPropagation();
enhanceEditorAttachmentLink(editorAttachmentLink);
openEditorAttachmentMenu(editorAttachmentLink, editorAttachmentLink, { x: event.clientX, y: event.clientY });
return;
}
var fileRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"]');
if (fileRow) {
event.preventDefault();
@@ -8904,7 +9125,10 @@ const SIDEBAR_TREE_JS: &str = r##"
targetRowId: targetRow ? targetRow.getAttribute('data-row-id') : null,
targetRowKind: targetRow ? targetRow.getAttribute('data-row-kind') : 'root',
documentId: targetRow ? targetRow.getAttribute('data-document-id') || targetRow.getAttribute('data-doc-id') : null,
assetId: targetRow ? targetRow.getAttribute('data-asset-id') : null
assetId: targetRow ? targetRow.getAttribute('data-asset-id') : null,
targetRelativePath: targetRow && (targetRow.getAttribute('data-row-kind') === 'folder' || targetRow.getAttribute('data-row-kind') === 'directory')
? fileTreeRowLocalRelativePath(targetRow)
: null
};
if (activeFileTreeDropRow instanceof HTMLElement) activeFileTreeDropRow.setAttribute('data-drop-target', 'false');
activeFileTreeDropRow = null;
@@ -9718,6 +9942,21 @@ mod tests {
assert!(SIDEBAR_TREE_JS.contains("function fileTreeMenuTargetParentId"));
assert!(SIDEBAR_TREE_JS.contains("if (action === 'new-folder')"));
assert!(SIDEBAR_TREE_JS.contains("fileTreeCopyPath(detail, trigger)"));
assert!(SIDEBAR_TREE_JS
.contains("if (detail.contextKind === 'filetree' && action === 'download')"));
assert!(SIDEBAR_TREE_JS.contains("downloadSelectedFileTreeAssetRows(detail, trigger)"));
assert!(SIDEBAR_TREE_JS.contains("function selectedSidebarFileTreeRowsForDownload"));
assert!(SIDEBAR_TREE_JS.contains("function selectedSidebarFileTreeAssetRowsForDownload"));
assert!(SIDEBAR_TREE_JS.contains("data-local-relative-path"));
assert!(SIDEBAR_TREE_JS.contains("targetRelativePath"));
assert!(SIDEBAR_TREE_JS.contains(
"recordFileTreeAction(downloads.length > 1 ? 'bulk-download' : 'download', primary)"
));
assert!(SIDEBAR_TREE_JS.contains("data-mnote-filetree-download-count"));
assert!(SIDEBAR_TREE_JS.contains("function triggerBrowserDownload"));
assert!(SIDEBAR_TREE_JS.contains("openEditorAttachmentMenu(editorAttachmentLink, editorAttachmentLink, { x: event.clientX, y: event.clientY });"));
assert!(SIDEBAR_TREE_JS.contains("var localFilePath = localFilePathFromAssetId(assetId);"));
assert!(SIDEBAR_TREE_JS.contains("attachmentMetaCache[assetId] = localMeta;"));
assert!(SIDEBAR_TREE_JS.contains(
"var pageRow = closestAction(e.target, '.tree-row[data-shell-mode=\"page\"]');"
));
@@ -0,0 +1,308 @@
#!/usr/bin/env node
"use strict";
const assert = require("node:assert");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { chromium } = require("playwright");
const BASE_URL = (process.env.MNOTE_WEB_SMOKE_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
const UI_TIMEOUT_MS = Number(process.env.MNOTE_SMOKE_UI_TIMEOUT_MS || 30_000);
const OUTPUT_DIR = process.env.MNOTE_CONTEXT_MENU_DOWNLOAD_OUTPUT_DIR
|| "/mnt/Data1T/mnote/tmp/task476-filetree-editor-context-menu-download-smoke";
const CHROMIUM_EXECUTABLE_PATH = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|| ["/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/snap/bin/chromium"]
.find((candidate) => fs.existsSync(candidate));
function fileUrl(localPath) {
return `file://${localPath}`;
}
function localMdDocumentId(relativePath) {
return `local-md:${relativePath.replaceAll("/", "~2F")}`;
}
function documentUrl(root, relativePath) {
const url = new URL(`${BASE_URL}/documents/${encodeURIComponent(localMdDocumentId(relativePath))}`);
url.searchParams.set("sourceKind", "local_folder");
url.searchParams.set("rootUri", fileUrl(root));
url.searchParams.set("treeView", "filetree");
return url.toString();
}
function writeWorkspaceManifest(root, ownerId) {
const metadataDir = path.join(root, ".mnote");
fs.mkdirSync(metadataDir, { recursive: true });
fs.writeFileSync(
path.join(metadataDir, "workspace.json"),
`${JSON.stringify({
workspaceId: `local-ws:${ownerId}:task476`,
ownerId,
createdAt: new Date().toISOString(),
capabilities: ["local_files", "markdown_edit", "asset_upload"],
}, null, 2)}\n`,
"utf8",
);
}
async function quickLogin(page) {
await page.goto(`${BASE_URL}/auth`, { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
const quickLoginButton = page.getByRole("button", { name: "测试账号快速登录" });
if (await quickLoginButton.count()) {
await quickLoginButton.click({ timeout: UI_TIMEOUT_MS });
await page.waitForURL((url) => url.pathname === "/", { timeout: UI_TIMEOUT_MS }).catch(() => undefined);
}
}
async function menuActions(page, kind) {
return await page.evaluate((targetKind) => {
const menu = document.querySelector(`[data-testid="mnote-tree-context-menu"][data-kind="${targetKind}"]`);
return Array.from(menu?.querySelectorAll("[data-action]") || []).map((button) => ({
action: button.getAttribute("data-action") || "",
label: button.textContent?.trim() || "",
}));
}, kind);
}
async function waitForDownloadCount(downloads, expectedCount, timeoutMs) {
const startedAt = Date.now();
while (downloads.length < expectedCount && Date.now() - startedAt < timeoutMs) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
assert(
downloads.length >= expectedCount,
`等待下载数量达到 ${expectedCount} 失败,当前 ${downloads.length}`,
);
}
async function waitForFileExists(filePath, timeoutMs) {
const startedAt = Date.now();
while (!fs.existsSync(filePath) && Date.now() - startedAt < timeoutMs) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
assert(fs.existsSync(filePath), `等待文件出现失败: ${filePath}`);
}
function suggestedFilenames(downloads, startIndex) {
return downloads.slice(startIndex).map((download) => download.suggestedFilename()).sort();
}
async function main() {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
const root = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-task476-context-download-"));
const relativePath = "README.md";
const resourceDir = path.join(root, "README");
const assetNames = ["report-a.pdf", "report-b.pdf", "report-c.pdf"];
fs.mkdirSync(resourceDir, { recursive: true });
writeWorkspaceManifest(root, "user_real");
fs.writeFileSync(
path.join(root, relativePath),
[
"---",
"title: Context Menu Download",
"---",
"",
"# Context Menu Download",
"",
"[report-a.pdf](README/report-a.pdf)",
"",
].join("\n"),
"utf8",
);
for (const fileName of assetNames) {
fs.writeFileSync(path.join(resourceDir, fileName), Buffer.from(`%PDF-1.4\n% task476 ${fileName}\n`, "utf8"));
}
const browser = await chromium.launch({
headless: process.env.HEADFUL !== "1",
executablePath: CHROMIUM_EXECUTABLE_PATH,
});
const context = await browser.newContext({
viewport: { width: 1360, height: 900 },
acceptDownloads: true,
extraHTTPHeaders: {
"x-mnote-actor-id": "user_real",
"x-mnote-actor-type": "user",
},
});
const page = await context.newPage();
const consoleErrors = [];
const capturedDownloads = [];
page.on("console", (message) => {
if (message.type() === "error") consoleErrors.push(message.text());
});
page.on("download", (download) => {
capturedDownloads.push(download);
});
try {
await quickLogin(page);
await page.goto(documentUrl(root, relativePath), { waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
const editorSurface = page.locator(".document-pane[data-pane-role=\"primary\"] .editor-surface .ProseMirror").first();
await editorSurface.waitFor({
state: "attached",
timeout: UI_TIMEOUT_MS,
}).catch(() => undefined);
const editorReady = await editorSurface.isVisible().catch(() => false);
const assetRow = page.locator('[data-testid="filetree-asset-row"]', { hasText: "report-a.pdf" }).first();
await assetRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await assetRow.click({ button: "right", timeout: UI_TIMEOUT_MS });
const filetreeMenu = page.locator('[data-testid="mnote-tree-context-menu"][data-kind="filetree"]').first();
await filetreeMenu.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
const filetreeActions = await menuActions(page, "filetree");
assert(filetreeActions.some((item) => item.action === "download"), `文件树 asset 菜单缺少下载: ${JSON.stringify(filetreeActions)}`);
await page.screenshot({ path: path.join(OUTPUT_DIR, "01-filetree-asset-menu-with-download.png"), fullPage: false });
const singleDownloadStart = capturedDownloads.length;
const [filetreeDownload] = await Promise.all([
page.waitForEvent("download", { timeout: UI_TIMEOUT_MS }),
filetreeMenu.locator('[data-action="download"]').click({ timeout: UI_TIMEOUT_MS }),
]);
assert.equal(filetreeDownload.suggestedFilename(), "report-a.pdf");
await filetreeDownload.saveAs(path.join(OUTPUT_DIR, filetreeDownload.suggestedFilename()));
await waitForDownloadCount(capturedDownloads, singleDownloadStart + 1, UI_TIMEOUT_MS);
const reportCRow = page.locator('[data-testid="filetree-asset-row"]', { hasText: "report-c.pdf" }).first();
await reportCRow.click({ modifiers: ["Control"], timeout: UI_TIMEOUT_MS });
const selectedAfterCtrl = await page.evaluate(() => Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-selected="true"]')).map((row) => ({
title: row.textContent?.trim() || "",
rowId: row.getAttribute("data-row-id") || "",
})));
await reportCRow.click({ button: "right", timeout: UI_TIMEOUT_MS });
const ctrlMenu = page.locator('[data-testid="mnote-tree-context-menu"][data-kind="filetree"]').first();
await ctrlMenu.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
const ctrlActions = await menuActions(page, "filetree");
assert(
ctrlActions.some((item) => item.action === "download" && item.label.includes("2")),
`Ctrl 多选菜单没有显示 2 个文件下载: actions=${JSON.stringify(ctrlActions)} selected=${JSON.stringify(selectedAfterCtrl)}`,
);
await page.screenshot({ path: path.join(OUTPUT_DIR, "03-filetree-ctrl-multi-download-menu.png"), fullPage: false });
const ctrlDownloadStart = capturedDownloads.length;
await ctrlMenu.locator('[data-action="download"]').click({ timeout: UI_TIMEOUT_MS });
await waitForDownloadCount(capturedDownloads, ctrlDownloadStart + 2, UI_TIMEOUT_MS);
const ctrlDownloadFiles = suggestedFilenames(capturedDownloads, ctrlDownloadStart);
assert.deepStrictEqual(ctrlDownloadFiles, ["report-a.pdf", "report-c.pdf"]);
await assetRow.click({ timeout: UI_TIMEOUT_MS });
await reportCRow.click({ modifiers: ["Shift"], timeout: UI_TIMEOUT_MS });
const selectedAfterShift = await page.evaluate(() => Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-selected="true"]')).map((row) => ({
title: row.textContent?.trim() || "",
rowId: row.getAttribute("data-row-id") || "",
})));
await reportCRow.click({ button: "right", timeout: UI_TIMEOUT_MS });
const shiftMenu = page.locator('[data-testid="mnote-tree-context-menu"][data-kind="filetree"]').first();
await shiftMenu.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
const shiftActions = await menuActions(page, "filetree");
assert(
shiftActions.some((item) => item.action === "download" && item.label.includes("3")),
`Shift 范围多选菜单没有显示 3 个文件下载: actions=${JSON.stringify(shiftActions)} selected=${JSON.stringify(selectedAfterShift)}`,
);
await page.screenshot({ path: path.join(OUTPUT_DIR, "04-filetree-shift-multi-download-menu.png"), fullPage: false });
const shiftDownloadStart = capturedDownloads.length;
await shiftMenu.locator('[data-action="download"]').click({ timeout: UI_TIMEOUT_MS });
await waitForDownloadCount(capturedDownloads, shiftDownloadStart + 3, UI_TIMEOUT_MS);
const shiftDownloadFiles = suggestedFilenames(capturedDownloads, shiftDownloadStart);
assert.deepStrictEqual(shiftDownloadFiles, ["report-a.pdf", "report-b.pdf", "report-c.pdf"]);
const markdownRow = page.locator('#sidebar-file-tree-root .tree-row[data-row-kind="markdown"]', { hasText: "README.md" }).first();
await markdownRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await markdownRow.click({ button: "right", timeout: UI_TIMEOUT_MS });
const markdownMenu = page.locator('[data-testid="mnote-tree-context-menu"][data-kind="filetree"]').first();
await markdownMenu.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
const markdownActions = await menuActions(page, "filetree");
assert(markdownActions.some((item) => item.action === "download"), `Markdown 行菜单缺少下载: ${JSON.stringify(markdownActions)}`);
const [markdownDownload] = await Promise.all([
page.waitForEvent("download", { timeout: UI_TIMEOUT_MS }),
markdownMenu.locator('[data-action="download"]').click({ timeout: UI_TIMEOUT_MS }),
]);
assert.equal(markdownDownload.suggestedFilename(), "README.md");
const folderRow = page.locator('#sidebar-file-tree-root .tree-row[data-row-kind="folder"]', { hasText: "README" }).first();
await folderRow.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await folderRow.click({ button: "right", timeout: UI_TIMEOUT_MS });
const folderMenu = page.locator('[data-testid="mnote-tree-context-menu"][data-kind="filetree"]').first();
await folderMenu.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
const folderActions = await menuActions(page, "filetree");
assert(folderActions.some((item) => item.action === "download"), `文件夹行菜单缺少下载: ${JSON.stringify(folderActions)}`);
await page.screenshot({ path: path.join(OUTPUT_DIR, "05-filetree-folder-download-menu.png"), fullPage: false });
const [folderDownload] = await Promise.all([
page.waitForEvent("download", { timeout: UI_TIMEOUT_MS }),
folderMenu.locator('[data-action="download"]').click({ timeout: UI_TIMEOUT_MS }),
]);
assert.equal(folderDownload.suggestedFilename(), "README.tar");
await folderDownload.saveAs(path.join(OUTPUT_DIR, folderDownload.suggestedFilename()));
const droppedFileName = "drag-upload-note.txt";
const droppedPath = path.join(resourceDir, droppedFileName);
await folderRow.dispatchEvent("dragover", {
dataTransfer: await page.evaluateHandle(({ fileName }) => {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(new File(["drag upload smoke"], fileName, { type: "text/plain" }));
return dataTransfer;
}, { fileName: droppedFileName }),
});
await folderRow.dispatchEvent("drop", {
dataTransfer: await page.evaluateHandle(({ fileName }) => {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(new File(["drag upload smoke"], fileName, { type: "text/plain" }));
return dataTransfer;
}, { fileName: droppedFileName }),
});
await waitForFileExists(droppedPath, UI_TIMEOUT_MS);
let attachmentActions = [];
let attachmentDownload = null;
const attachmentLink = page.locator('.editor-surface .ProseMirror a[data-mnote-attachment-link="true"]', { hasText: "report-a.pdf" }).first();
const attachmentReady = editorReady && await attachmentLink.isVisible().catch(() => false);
if (attachmentReady) {
await attachmentLink.click({ button: "right", timeout: UI_TIMEOUT_MS });
const attachmentMenu = page.locator('[data-testid="mnote-tree-context-menu"][data-kind="attachment"]').first();
await attachmentMenu.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
attachmentActions = await menuActions(page, "attachment");
assert(attachmentActions.some((item) => item.action === "download"), `附件右键菜单缺少下载: ${JSON.stringify(attachmentActions)}`);
await page.screenshot({ path: path.join(OUTPUT_DIR, "02-editor-attachment-context-menu.png"), fullPage: false });
[attachmentDownload] = await Promise.all([
page.waitForEvent("download", { timeout: UI_TIMEOUT_MS }),
attachmentMenu.locator('[data-action="download"]').click({ timeout: UI_TIMEOUT_MS }),
]);
assert.equal(attachmentDownload.suggestedFilename(), "report-a.pdf");
}
const result = {
ok: true,
root,
url: page.url(),
filetreeActions,
attachmentActions,
downloads: [
{ action: "filetree_asset_download", suggestedFilename: filetreeDownload.suggestedFilename() },
{ action: "filetree_ctrl_multi_download", suggestedFilenames: ctrlDownloadFiles },
{ action: "filetree_shift_multi_download", suggestedFilenames: shiftDownloadFiles },
{ action: "filetree_markdown_download", suggestedFilename: markdownDownload.suggestedFilename() },
{ action: "filetree_folder_download", suggestedFilename: folderDownload.suggestedFilename() },
{ action: "filetree_folder_drop_upload", targetPath: droppedPath, exists: fs.existsSync(droppedPath) },
attachmentReady
? { action: "editor_attachment_context_menu_download", suggestedFilename: attachmentDownload.suggestedFilename() }
: { action: "editor_attachment_context_menu_download", status: "blocked", reason: attachmentReady ? "" : "editor_attachment_not_visible" },
],
consoleErrors,
screenshots: [
path.join(OUTPUT_DIR, "01-filetree-asset-menu-with-download.png"),
path.join(OUTPUT_DIR, "03-filetree-ctrl-multi-download-menu.png"),
path.join(OUTPUT_DIR, "04-filetree-shift-multi-download-menu.png"),
path.join(OUTPUT_DIR, "05-filetree-folder-download-menu.png"),
...(attachmentReady ? [path.join(OUTPUT_DIR, "02-editor-attachment-context-menu.png")] : []),
],
};
fs.writeFileSync(path.join(OUTPUT_DIR, "result.json"), `${JSON.stringify(result, null, 2)}\n`, "utf8");
console.log(JSON.stringify(result, null, 2));
} finally {
await context.close().catch(() => undefined);
await browser.close().catch(() => undefined);
}
}
main().catch((error) => {
console.error(error && error.stack ? error.stack : error);
process.exit(1);
});