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:
@@ -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
|
||||
@@ -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 A:design 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 B:design 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 Tree,Rust 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 C:design 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 D:design 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 已 DONE,Phase 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 analysis(AI 操作思维导图 / 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 cache;WS 尚未成为主实时链路)。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 入口实现时参考。
|
||||
- **选项 B:keep-process** — 如果团队明确要在短期内实现 Web 入口。
|
||||
- **选项 C:archive-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 gate(P0/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-stage(local_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 preparation(4-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 ──→ 核心 cutover,4-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 state,filetree 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 输出 proposal,Codex 实现最小安全集。闭环。
|
||||
|
||||
### 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 D:design 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 最近的 checklist(7-30~7-33)大部分已完成,仅余次要项。10-review 的四份 doc 均为已消耗的审查参考。
|
||||
|
||||
---
|
||||
|
||||
## 逐文档审查
|
||||
|
||||
### 1. `design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||||
|
||||
**建议状态**:`keep-process`
|
||||
|
||||
**证据**:
|
||||
- Phase 1–7(Page 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 B(Manifest 合同收口)全部未勾 [phase-b-open]
|
||||
- Phase C(block-edit-workflow 改造)全部未勾 [phase-c-open]
|
||||
- Phase D(Review 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 B–F 是后续执行 roadmap。
|
||||
|
||||
**建议补充说明**:Phase C 与 Phase C policy(Phase 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 B(Phase 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 已被后续 checklist(7-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 统一架构的实施蓝图。后续 checklist(7-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.8(UI / 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 缺口已拆分为执行 checklist(5-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`
|
||||
|
||||
**证据**:
|
||||
- 审查发现的缺口已拆分为执行 checklist(5-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/` 之内保留。
|
||||
|
||||
---
|
||||
|
||||
## 最高风险项
|
||||
|
||||
按风险从高到低:
|
||||
|
||||
### 🔴 H1:7-27 尚未实施(keep-process)
|
||||
|
||||
`mnote.doc.markdown_edit` 的在线文档写回路径仍然以「二次推导」而非「最终 markdown」为真源。7-24/7-25 已修复拒绝写入的安全问题,但 full_content 和多 operation 场景的正确性仍未达到设计目标。如不尽快实施,已有 bugfix 的保护措施效果有限,且代码与设计文档的差距会随时间扩大。
|
||||
|
||||
**建议**:优先安排 7-27 的 6 步实施计划。
|
||||
|
||||
### 🟡 H2:7-10 Phase 8 冻结与后续解冻风险(keep-process)
|
||||
|
||||
Phase 1–7 全部完成,但 Phase 8(UI / Review Mode)因 Phase C 政策冻结。如果后续「冻结」解除,接手者需要从零启动——当前 design 文档不承诺 UI 形状、不包含 mockup、不定义用户交互路径。7-16 3.8 的 4 项未勾项同样属此问题。
|
||||
|
||||
**建议**:在 Phase C 解冻前标记「Phase 8 实施时需新建设计文档,当前只保留缺口清单」。
|
||||
|
||||
### 🟡 H3:7-15 执行计划分化(keep-process)
|
||||
|
||||
17 步计划中只有前 7 步被 7-30~7-33 覆盖实施。step-8 之后(Reasonix ACP wrapper、前端运行时选择器、存量迁移、旧代码退役)仍然没有对应的 checklist 或代码证据。若 ACP 架构的目标是「Hermes 与 Reasonix 可互换」,则后续步骤仍需要推动。
|
||||
|
||||
**建议**:创建 7-34 ~ 7-36 覆盖 Reasonix wrapper、前端选择器和存量迁移。
|
||||
|
||||
### 🟢 H4:7-30/31/32/33 剩余未完成项(archive-done)
|
||||
|
||||
这些 checklist 的核心功能已完成,但各有 1 项未完成:
|
||||
- 7-30:session id 全链路持久化
|
||||
- 7-31:end-to-end mock 测试缺口
|
||||
- 7-32:browser smoke 未补
|
||||
- 7-33:todo 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。
|
||||
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Total messages: 0 (Errors: 0, Warnings: 0)
|
||||
@@ -0,0 +1,64 @@
|
||||
# 4-46 FileTree / Editor Context Menu Download v1
|
||||
|
||||
> 状态:done
|
||||
>
|
||||
> 日期:2026-05-21
|
||||
>
|
||||
> Owner:FileTree 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-21:Reasonix 浏览器测试确认:文件树 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。
|
||||
@@ -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. 一句话收口
|
||||
|
||||
|
||||
+4
@@ -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 文件夹,也不利于页面导出。
|
||||
+1
-1
@@ -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` |
|
||||
|
||||
|
||||
+1
-1
@@ -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. P2:tree 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. P3:AI 主线继续收口
|
||||
|
||||
### 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. P4:mindmap、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 C:Page Aggregate / local markdown 写侧
|
||||
|
||||
1. `3-13`
|
||||
2. `5-6`
|
||||
3. `1-5` Page Aggregate 子项
|
||||
|
||||
完成后预期:正文真相、runtime projection、compat join 的边界更薄。
|
||||
|
||||
### Batch D:live cache / command context
|
||||
|
||||
1. `3-3`
|
||||
2. `1-5` command context 子项
|
||||
3. `4-44` 若 Batch A 未完成则回补
|
||||
|
||||
完成后预期:树域实时与 enablement 判断更统一。
|
||||
|
||||
### Batch E:AI 与资源工具
|
||||
|
||||
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 默认主存储口径。
|
||||
+1
-1
@@ -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`
|
||||
|
||||
+4
-2
@@ -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`
|
||||
+6
-4
@@ -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。
|
||||
|
||||
## 目标
|
||||
|
||||
|
||||
+2
-1
@@ -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 资源垃圾箱兼容入口的收口边界。
|
||||
|
||||
+3
-1
@@ -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
|
||||
>
|
||||
+3
-1
@@ -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
|
||||
>
|
||||
+3
-1
@@ -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
|
||||
>
|
||||
+3
-1
@@ -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
|
||||
>
|
||||
+3
-1
@@ -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`
|
||||
|
||||
+4
-1
@@ -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
|
||||
```
|
||||
|
||||
+7
-5
@@ -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. 非目标
|
||||
|
||||
+3
-1
@@ -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
|
||||
>
|
||||
+3
-1
@@ -1,6 +1,8 @@
|
||||
# 5-18 Main Editor Tab State 与错误占位 Checklist v1
|
||||
|
||||
> 状态:process
|
||||
> 状态:done
|
||||
>
|
||||
> 归档说明(2026-05-21):主编辑器 tab state 与错误占位已完成并验证。
|
||||
>
|
||||
> 日期:2026-05-20
|
||||
>
|
||||
+3
-1
@@ -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
|
||||
>
|
||||
+3
-1
@@ -1,6 +1,8 @@
|
||||
# 5-20 Resource Tab Close Confirm Checklist v1
|
||||
|
||||
> 状态:process
|
||||
> 状态:done
|
||||
>
|
||||
> 归档说明(2026-05-21):resource tab close guard 可见提示已完成。
|
||||
>
|
||||
> 日期:2026-05-20
|
||||
>
|
||||
+3
-1
@@ -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
|
||||
>
|
||||
+3
-1
@@ -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
|
||||
>
|
||||
+3
-1
@@ -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
|
||||
>
|
||||
+3
-1
@@ -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
|
||||
>
|
||||
+3
-1
@@ -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
@@ -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` 的过渡兼容背景,不应再作为默认产品形态解释。
|
||||
|
||||
+1
@@ -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. 参考入口
|
||||
|
||||
+4
-2
@@ -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`
|
||||
+3
-1
@@ -1,6 +1,8 @@
|
||||
# 7-30 ACP session/load 恢复 checklist v1
|
||||
|
||||
> 状态:process
|
||||
> 状态:done
|
||||
>
|
||||
> 归档说明(2026-05-21):ACP `session/load` / resume 核心闭环已完成并通过聚焦测试;session id 全链路持久化作为后续产品化项继续跟踪。
|
||||
>
|
||||
> Owner:ACP session lifecycle
|
||||
>
|
||||
+3
-1
@@ -1,6 +1,8 @@
|
||||
# 7-32 ACP tool locations 与打开动作 checklist v1
|
||||
|
||||
> 状态:process
|
||||
> 状态:done
|
||||
>
|
||||
> 归档说明(2026-05-21):ACP tool locations 解析、SSE 桥接与前端打开动作已完成;浏览器 smoke 可作为后续补强。
|
||||
>
|
||||
> Owner:ACP tool event data + Page AI open action
|
||||
>
|
||||
+3
-1
@@ -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 细化显示作为后续格式约定项。
|
||||
>
|
||||
> Owner:ACP 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 A(01/02/03):`.codex/reasonix-tasks/results/design-governance-worker-a-01-03.md`
|
||||
- Worker B(04 tree-domain):`.codex/reasonix-tasks/results/design-governance-worker-b-04-tree.md`
|
||||
- Worker C(05/06/08/09):`.codex/reasonix-tasks/results/design-governance-worker-c-05-06-08-09.md`
|
||||
- Worker D(07/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 可进入主编辑区 tab,Convex 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
@@ -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(
|
||||
&fields.root_uri,
|
||||
&fields.document_id,
|
||||
&fields.kind,
|
||||
fields.file,
|
||||
)?;
|
||||
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");
|
||||
|
||||
@@ -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();"));
|
||||
|
||||
@@ -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();
|
||||
selectSidebarFileTreeRow(fileRow, { ctrlKey: false, metaKey: false, shiftKey: false });
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user