chore: align local-first control plane and editor fixes
- wire SQLite control-plane access/session paths into Rust web local-folder routes - preserve local Markdown attachment semantics across upload, reload, and secondary-pane resource tabs - refresh design governance docs, Reasonix task templates, and bug records - retire root .mcp.json local MCP config
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
# Result: <title>
|
||||
|
||||
## Metadata
|
||||
|
||||
- Run ID: `<run-id>`
|
||||
- Lane: `<lane>`
|
||||
- Worktree: `<absolute-worktree-path>`
|
||||
- Branch: `<branch-name>`
|
||||
- Worker: `codex-<lane>`
|
||||
|
||||
## Summary
|
||||
|
||||
Briefly state what changed.
|
||||
|
||||
## Completed work
|
||||
|
||||
- [ ] ...
|
||||
|
||||
## Modified files
|
||||
|
||||
- `path`: reason
|
||||
|
||||
## Validation performed
|
||||
|
||||
```bash
|
||||
# command
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
```text
|
||||
...
|
||||
```
|
||||
|
||||
## Suggested smoke test
|
||||
|
||||
- URL:
|
||||
- Actions:
|
||||
- Expected result:
|
||||
|
||||
## Risks / unfinished items
|
||||
|
||||
- ...
|
||||
|
||||
## Notes for reviewer
|
||||
|
||||
- ...
|
||||
@@ -0,0 +1,59 @@
|
||||
# Review: <title>
|
||||
|
||||
## Metadata
|
||||
|
||||
- Run ID: `<run-id>`
|
||||
- Reviewer: `Hermes`
|
||||
- Main repo: `/mnt/Data1T/mnote`
|
||||
- Related worktrees:
|
||||
- `...`
|
||||
|
||||
## Inputs reviewed
|
||||
|
||||
- PLAN.md:
|
||||
- RESULT.md files:
|
||||
- SMOKE.md:
|
||||
- Git diffs:
|
||||
|
||||
## Integration decision
|
||||
|
||||
MERGE / REQUEST_FIX / REJECT / HOLD
|
||||
|
||||
## Summary
|
||||
|
||||
Short decision summary.
|
||||
|
||||
## Diff review
|
||||
|
||||
- File/path:
|
||||
- Observation:
|
||||
- Risk:
|
||||
- Decision:
|
||||
|
||||
## Smoke review
|
||||
|
||||
- Smoke result:
|
||||
- Evidence sufficient: yes/no
|
||||
- Gaps:
|
||||
|
||||
## Validation run by reviewer
|
||||
|
||||
```bash
|
||||
# command
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
```text
|
||||
...
|
||||
```
|
||||
|
||||
## Follow-up tasks
|
||||
|
||||
- [ ] ...
|
||||
|
||||
## Cleanup / recycle
|
||||
|
||||
- Worktrees kept:
|
||||
- Worktrees moved to recycle:
|
||||
- Branches kept/deleted:
|
||||
@@ -0,0 +1,49 @@
|
||||
# Smoke: <title>
|
||||
|
||||
## Metadata
|
||||
|
||||
- Run ID: `<run-id>`
|
||||
- Tester: `reasonix-flash`
|
||||
- Target worktree / branch: `<branch-or-main>`
|
||||
- Evidence directory: `<absolute-evidence-dir>`
|
||||
- URL: `<url>`
|
||||
|
||||
## Scope
|
||||
|
||||
What this smoke test validates.
|
||||
|
||||
## Browser actions
|
||||
|
||||
- [ ] Navigate to `<url>`
|
||||
- [ ] Login / setup state if needed
|
||||
- [ ] Click: ...
|
||||
- [ ] Hover: ...
|
||||
- [ ] Drag: ...
|
||||
- [ ] Select: ...
|
||||
|
||||
## Result
|
||||
|
||||
PASS / FAIL / BLOCKED
|
||||
|
||||
## Evidence
|
||||
|
||||
- Final URL:
|
||||
- Screenshot:
|
||||
- Console errors:
|
||||
- Network errors:
|
||||
- Visible text sample:
|
||||
|
||||
## Findings
|
||||
|
||||
### Issue 1
|
||||
|
||||
- Expected:
|
||||
- Actual:
|
||||
- Reproduction:
|
||||
- Evidence:
|
||||
|
||||
## Cleanup
|
||||
|
||||
- Test data created:
|
||||
- Test data cleaned:
|
||||
- Residual state:
|
||||
@@ -0,0 +1,54 @@
|
||||
# Task: <title>
|
||||
|
||||
## Metadata
|
||||
|
||||
- Run ID: `<run-id>`
|
||||
- Lane: `<lane>`
|
||||
- Worker: `codex-<lane>`
|
||||
- Main repo: `/mnt/Data1T/mnote`
|
||||
- Worktree: `<absolute-worktree-path>`
|
||||
- Branch: `<branch-name>`
|
||||
- Result path: `<run-dir>/codex-<lane>/RESULT.md`
|
||||
|
||||
## Goal
|
||||
|
||||
Describe the concrete outcome expected from this worker.
|
||||
|
||||
## Context
|
||||
|
||||
- Relevant design docs:
|
||||
- `...`
|
||||
- Relevant code paths:
|
||||
- `...`
|
||||
|
||||
## Allowed edit scope
|
||||
|
||||
- `path/or/dir/**`
|
||||
|
||||
## Forbidden scope
|
||||
|
||||
- Do not edit files outside allowed scope.
|
||||
- Do not edit user data.
|
||||
- Do not modify unrelated recycle/history directories.
|
||||
- Do not commit, push, reset, or revert unless explicitly instructed.
|
||||
|
||||
## Requirements
|
||||
|
||||
- ...
|
||||
|
||||
## Validation commands
|
||||
|
||||
```bash
|
||||
# command 1
|
||||
# command 2
|
||||
```
|
||||
|
||||
## Expected deliverable
|
||||
|
||||
Write `RESULT.md` with:
|
||||
|
||||
- completed work
|
||||
- modified files
|
||||
- validation commands and outputs
|
||||
- risks / unfinished items
|
||||
- suggested smoke scope
|
||||
@@ -0,0 +1,79 @@
|
||||
# Reasonix Worker A:SQLite control-plane store 骨架
|
||||
|
||||
Project root: `/mnt/Data1T/mnote`
|
||||
|
||||
你不是独自在代码库中工作。其他 worker 可能并行修改 auth、access policy 或迁移脚本。不要还原、覆盖或清理不属于你任务范围的改动。不要提交 git。
|
||||
|
||||
## 背景
|
||||
|
||||
上位设计稿:
|
||||
`design/02-convex-rust-long-term-architecture/process/2-8-convex-replace-with-rust-sqlite-control-plane-v1.md`
|
||||
|
||||
目标是用 Rust + SQLite 完全替换 Convex 控制面。你的任务只负责 SQLite store 骨架,不接运行时 route。
|
||||
|
||||
## Ownership
|
||||
|
||||
允许修改:
|
||||
- 优先新增 `rust/crates/control-plane/**`
|
||||
- 必要时修改 `rust/Cargo.toml`
|
||||
- 必要时新增/修改 `rust/crates/mnote-web/Cargo.toml` 仅用于依赖引用建议
|
||||
|
||||
禁止修改:
|
||||
- `rust/crates/mnote-web/src/routes/gateway.rs`
|
||||
- `rust/crates/mnote-web/src/routes/local_folder_source.rs`
|
||||
- `rust/crates/mnote-web/src/routes/session.rs`
|
||||
- `rust/crates/mnote-web/src/ssr/**`
|
||||
- `infra/convex/**`
|
||||
- `.env*`
|
||||
|
||||
如果发现 workspace crate 接入风险过大,先实现为可编译独立 crate;不要跨模块硬接 runtime。
|
||||
|
||||
## 任务
|
||||
|
||||
1. 新增 `control-plane` Rust crate 或等价最小模块,优先独立 crate。
|
||||
2. 定义核心 model 与 `ControlPlaneStore` trait。
|
||||
3. 实现 `SqliteControlPlaneStore`,支持临时路径 DB。
|
||||
4. 实现 migration v1,包含至少:
|
||||
- `users`
|
||||
- `auth_identities`
|
||||
- `auth_sessions`
|
||||
- `workspaces`
|
||||
- `workspace_members`
|
||||
- `directory_grants`
|
||||
- `share_links`
|
||||
- `sync_state`
|
||||
- `ai_policies`
|
||||
- `audit_log`
|
||||
- `outbox_events`
|
||||
- `legacy_id_map`
|
||||
5. 实现最小 CRUD:
|
||||
- upsert user
|
||||
- create/get session
|
||||
- ensure default workspace
|
||||
- grant/revoke directory access
|
||||
- append/drain outbox
|
||||
6. 补单测:migration 幂等、默认 workspace、owner grant、outbox drain。
|
||||
|
||||
## 验收命令
|
||||
|
||||
优先运行:
|
||||
|
||||
```bash
|
||||
cargo test --manifest-path rust/Cargo.toml -p control-plane -- --nocapture
|
||||
cargo fmt --check --all
|
||||
```
|
||||
|
||||
如果没有新增独立 crate,而是放入 mnote-web:
|
||||
|
||||
```bash
|
||||
cargo test --manifest-path rust/Cargo.toml -p mnote-web control_plane -- --nocapture
|
||||
cargo fmt --check --all
|
||||
```
|
||||
|
||||
## 最终回复格式
|
||||
|
||||
请列出:
|
||||
- 修改文件
|
||||
- 新增表和核心 API
|
||||
- 实际运行过的命令及结果
|
||||
- 未完成项/风险
|
||||
@@ -0,0 +1,60 @@
|
||||
# Reasonix Worker B:Auth/session SQLite 切换准备
|
||||
|
||||
Project root: `/mnt/Data1T/mnote`
|
||||
|
||||
你不是独自在代码库中工作。其他 worker 可能并行修改 SQLite store、access policy 或迁移脚本。不要还原、覆盖或清理不属于你任务范围的改动。不要提交 git。
|
||||
|
||||
## 背景
|
||||
|
||||
上位设计稿:
|
||||
`design/02-convex-rust-long-term-architecture/process/2-8-convex-replace-with-rust-sqlite-control-plane-v1.md`
|
||||
|
||||
当前用户报告过新注册账号显示“开发用户的空间”、不能新建页面。根因方向之一是真实 actor/profile/workspace name 没有统一来自控制面。本任务只处理 auth/session 切换准备,不处理 local folder access。
|
||||
|
||||
## Ownership
|
||||
|
||||
允许修改:
|
||||
- `rust/crates/mnote-web/src/routes/session.rs`
|
||||
- 可新增 `rust/crates/mnote-web/src/routes/auth_sqlite.rs`
|
||||
- 可新增 `rust/crates/mnote-web/src/control_plane_auth_adapter.rs`
|
||||
- `rust/crates/mnote-web/src/ssr/pages/auth.rs` 仅限必要表单字段/测试适配
|
||||
- focused tests in same files
|
||||
|
||||
只读参考但不要直接改,除非确实必要并最小修改:
|
||||
- `rust/crates/mnote-web/src/routes/gateway.rs`
|
||||
- `rust/crates/mnote-web/src/middleware/request_context.rs`
|
||||
- `rust/crates/mnote-web/src/context.rs`
|
||||
|
||||
禁止修改:
|
||||
- `rust/crates/mnote-web/src/routes/local_folder_source.rs`
|
||||
- `rust/crates/mnote-web/src/ssr/pages/admin.rs`
|
||||
- `rust/crates/mnote-web/src/ssr/styles.rs`
|
||||
- `infra/convex/**`
|
||||
- `.env*`
|
||||
|
||||
## 任务
|
||||
|
||||
1. 梳理现有 signIn/signUp/session cookie 链,写在代码注释或测试名中,不写长文档。
|
||||
2. 增加 SQLite session adapter 的 route/helper 骨架:
|
||||
- 从 `mnote_session` 查 session。
|
||||
- 生成/刷新 `mnote_actor_id/name/email/type` display cookies。
|
||||
- `/api/session` 优先使用 SQLite session,缺失时保留现有 fallback。
|
||||
3. 注册/登录 adapter 应明确输出真实 `user_id/email/name/actor_type`。
|
||||
4. 新用户注册测试必须断言不回退到 `DEV_USER_NAME`。
|
||||
5. 不要求本轮完全删除 Convex auth fallback。
|
||||
|
||||
## 验收命令
|
||||
|
||||
```bash
|
||||
cargo test --manifest-path rust/Cargo.toml -p mnote-web session -- --nocapture
|
||||
cargo test --manifest-path rust/Cargo.toml -p mnote-web auth -- --nocapture
|
||||
cargo fmt --check --all
|
||||
```
|
||||
|
||||
## 最终回复格式
|
||||
|
||||
请列出:
|
||||
- 修改文件
|
||||
- 新增/调整的 cookie 与 session 规则
|
||||
- 实际运行过的命令及结果
|
||||
- 未完成项/风险
|
||||
@@ -0,0 +1,56 @@
|
||||
# Reasonix Worker C:Access policy SQLite adapter
|
||||
|
||||
Project root: `/mnt/Data1T/mnote`
|
||||
|
||||
你不是独自在代码库中工作。其他 worker 可能并行修改 SQLite store、auth/session 或迁移脚本。不要还原、覆盖或清理不属于你任务范围的改动。不要提交 git。
|
||||
|
||||
## 背景
|
||||
|
||||
上位设计稿:
|
||||
`design/02-convex-rust-long-term-architecture/process/2-8-convex-replace-with-rust-sqlite-control-plane-v1.md`
|
||||
|
||||
当前 `access-policy.json` 是本地目录授权事实源,但完全替换 Convex 后,应迁到 SQLite。你的任务是抽象 access policy adapter,保留 JSON fallback,为 SQLite grant 接入打接口。
|
||||
|
||||
## Ownership
|
||||
|
||||
允许修改:
|
||||
- `rust/crates/mnote-web/src/routes/local_folder_source.rs`
|
||||
- 可新增 `rust/crates/mnote-web/src/local_access_sqlite.rs`
|
||||
- 可新增 focused tests
|
||||
|
||||
禁止修改:
|
||||
- `rust/crates/mnote-web/src/routes/session.rs`
|
||||
- `rust/crates/mnote-web/src/routes/gateway.rs`
|
||||
- `rust/crates/mnote-web/src/ssr/pages/admin.rs`
|
||||
- `rust/crates/mnote-web/src/ssr/styles.rs`
|
||||
- `infra/convex/**`
|
||||
- `.env*`
|
||||
|
||||
## 任务
|
||||
|
||||
1. 找出 `ensure_local_workspace_read_access` / write access / admin access 的现有调用和策略读取点。
|
||||
2. 抽象一个 access policy provider/adapter:
|
||||
- JSON provider 维持现有行为。
|
||||
- SQLite provider 可先是 trait/mock/stub,不要求真实 DB。
|
||||
3. 增加测试覆盖:
|
||||
- 默认 workspace owner 有 write。
|
||||
- read grant 禁写。
|
||||
- write grant 可写。
|
||||
- 未授权拒绝。
|
||||
4. 为后续 SQLite store 接入预留最小接口,不要重写 tree command 或 UI。
|
||||
|
||||
## 验收命令
|
||||
|
||||
```bash
|
||||
cargo test --manifest-path rust/Cargo.toml -p mnote-web local_access_policy -- --nocapture
|
||||
cargo test --manifest-path rust/Cargo.toml -p mnote-web local_workspace_access -- --nocapture
|
||||
cargo fmt --check --all
|
||||
```
|
||||
|
||||
## 最终回复格式
|
||||
|
||||
请列出:
|
||||
- 修改文件
|
||||
- 新增 adapter/trait 设计
|
||||
- 实际运行过的命令及结果
|
||||
- 未完成项/风险
|
||||
@@ -0,0 +1,62 @@
|
||||
# Reasonix Worker D:迁移工具与 Convex 下线 guard
|
||||
|
||||
Project root: `/mnt/Data1T/mnote`
|
||||
|
||||
你不是独自在代码库中工作。其他 worker 可能并行修改 Rust control-plane、auth/session 或 access policy。不要还原、覆盖或清理不属于你任务范围的改动。不要提交 git。
|
||||
|
||||
## 背景
|
||||
|
||||
上位设计稿:
|
||||
`design/02-convex-rust-long-term-architecture/process/2-8-convex-replace-with-rust-sqlite-control-plane-v1.md`
|
||||
|
||||
目标是给完全替换 Convex 准备迁移 dry-run 工具和 guard,不执行真实破坏性迁移。
|
||||
|
||||
## Ownership
|
||||
|
||||
允许修改:
|
||||
- 新增 `scripts/migrate-control-plane-to-sqlite.js`
|
||||
- 修改 `scripts/check-local-first-convex-guard.js` 仅限增加 SQLite 迁移期 guard 规则或建议输出
|
||||
- 可新增 `scripts/task*-sqlite-control-plane-*.js` focused smoke
|
||||
- 可向设计稿 `2-8` 追加“执行记录”,不得改设计结论
|
||||
|
||||
禁止修改:
|
||||
- `rust/crates/**`
|
||||
- `infra/convex/docker-compose.yml`
|
||||
- `.env*`
|
||||
- 不删除任何 Convex 数据、Docker volume 或本地数据文件
|
||||
|
||||
## 任务
|
||||
|
||||
1. 实现 dry-run 迁移工具:
|
||||
- 读取 `/mnt/Data1T/Mnote_data/control-plane/access-policy.json`,路径可用参数覆盖。
|
||||
- 扫描 `/mnt/Data1T/Mnote_data/users/*/workspaces/*/.mnote/workspace.json`,路径可用参数覆盖。
|
||||
- 生成 `migration-report.json`,包含 users/workspaces/grants/legacy_id_map 建议,不写 DB。
|
||||
2. 支持命令:
|
||||
|
||||
```bash
|
||||
node scripts/migrate-control-plane-to-sqlite.js --dry-run --out /tmp/mnote-control-plane-migration-report.json
|
||||
```
|
||||
|
||||
3. 更新 guard 或输出建议:新增 Convex 控制面代码应被标记为禁止或迁移例外。
|
||||
4. 补最小 node smoke,断言 dry-run report 可生成。
|
||||
|
||||
## 验收命令
|
||||
|
||||
```bash
|
||||
node scripts/migrate-control-plane-to-sqlite.js --dry-run --out /tmp/mnote-control-plane-migration-report.json
|
||||
npm run check:local-first-convex-guard
|
||||
```
|
||||
|
||||
如新增 smoke:
|
||||
|
||||
```bash
|
||||
node scripts/<new-smoke>.js
|
||||
```
|
||||
|
||||
## 最终回复格式
|
||||
|
||||
请列出:
|
||||
- 修改文件
|
||||
- dry-run report 字段
|
||||
- 实际运行过的命令及结果
|
||||
- 未完成项/风险
|
||||
@@ -0,0 +1,55 @@
|
||||
# Reasonix Worker E:control-plane share_links / audit / outbox 扩展
|
||||
|
||||
Project root: `/mnt/Data1T/mnote`
|
||||
|
||||
你不是独自在代码库中工作。其他 worker 可能并行修改 `mnote-web` 适配层、auth/session 或 access policy。不要还原、覆盖或清理不属于你任务范围的改动。不要提交 git。
|
||||
|
||||
## 背景
|
||||
|
||||
上位设计稿:
|
||||
`design/02-convex-rust-long-term-architecture/process/2-8-convex-replace-with-rust-sqlite-control-plane-v1.md`
|
||||
|
||||
当前 `control-plane` crate 已有 users/auth/workspaces/directory_grants/sync_state 的骨架,但 share_links、audit_log、outbox 的能力还不够完整。
|
||||
|
||||
## Ownership
|
||||
|
||||
允许修改:
|
||||
- `rust/crates/control-plane/src/model.rs`
|
||||
- `rust/crates/control-plane/src/store.rs`
|
||||
- `rust/crates/control-plane/src/sqlite.rs`
|
||||
- `rust/crates/control-plane/src/migrations.rs`
|
||||
- `rust/crates/control-plane/migrations/*.sql`
|
||||
- `rust/crates/control-plane/src/error.rs` 仅限必要错误映射
|
||||
|
||||
禁止修改:
|
||||
- `rust/crates/mnote-web/**`
|
||||
- `infra/convex/**`
|
||||
- `.env*`
|
||||
- `scripts/**`
|
||||
|
||||
## 任务
|
||||
|
||||
1. 补强 `control-plane` 模型与 store trait:
|
||||
- `share_links` 的 create/list/revoke/resolve
|
||||
- `audit_log` append/list 最小能力
|
||||
- `outbox_events` drain/mark-delivered 的稳定方法
|
||||
2. 为 share_links 增加 SQLite 实现与单测:
|
||||
- token 只保存 hash
|
||||
- revoke 后不可再 resolve
|
||||
3. 为 audit_log 增加最小单测:
|
||||
- 创建分享、撤销授权、AI/控制面关键事件可追加
|
||||
4. 保持 schema 迁移幂等。
|
||||
|
||||
## 验收命令
|
||||
|
||||
```bash
|
||||
cargo test --manifest-path rust/Cargo.toml -p control-plane -- --nocapture
|
||||
```
|
||||
|
||||
## 最终回复格式
|
||||
|
||||
请列出:
|
||||
- 修改文件
|
||||
- 新增 API / 表能力
|
||||
- 实际运行过的命令及结果
|
||||
- 未完成项 / 风险
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"supabase_local": {
|
||||
"type": "stdio",
|
||||
"command": "cmd",
|
||||
"args": ["/c", "npx", "-y", "mcp-remote@latest", "http://127.0.0.1:18000/mcp"],
|
||||
"startup_timeout_ms": 60000,
|
||||
"env": {
|
||||
"PROGRAMFILES": "C:\\Program Files",
|
||||
"SystemRoot": "C:\\Windows"
|
||||
}
|
||||
},
|
||||
"chrome-devtools": {
|
||||
"type": "stdio",
|
||||
"command": "cmd",
|
||||
"args": ["/c", "npx", "-y", "chrome-devtools-mcp@latest"],
|
||||
"startup_timeout_ms": 60000,
|
||||
"env": {
|
||||
"PROGRAMFILES": "C:\\Program Files",
|
||||
"SystemRoot": "C:\\Windows"
|
||||
}
|
||||
},
|
||||
"context7": {
|
||||
"type": "stdio",
|
||||
"command": "cmd",
|
||||
"args": ["/c", "npx", "-y", "@upstash/context7-mcp", "--api-key", "ctx7sk-8beae3e8-38a0-497a-a44d-a67c05669318"],
|
||||
"startup_timeout_ms": 40000,
|
||||
"env": {
|
||||
"SystemRoot": "C:\\Windows"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-6
@@ -23,7 +23,7 @@
|
||||
3. `mnote-web` 是当前 Rust Web 主执行面,负责 3000 gateway、server-first shell、query / command / projection / transport 与 realtime stream;Next App Router 已降为 legacy compat / island bundle source,不再是当前主入口
|
||||
4. 文档页默认主编辑器已切到页面内 `leptos-tiptap` island
|
||||
5. `BlockNote` 已退出文档页默认主路径,仅作为历史参考实现 / 对照材料保留
|
||||
6. 页面 AI 当前最合理的长期形态是:MNote 只负责页面定位、白名单目录权限、agent runtime 管理、文件变更同步;Hermes / Reasonix 直接在授权工作区内编辑本地文件。`page_ai_workflow` 与 `mnote.doc.*` / `mnote.block.*` 保留为兼容 / cloud / 复杂结构辅助层,不再作为 local-first 普通正文编辑默认主路径
|
||||
6. 页面 AI 当前最合理的长期形态是:MNote 只负责页面定位、白名单目录权限、agent runtime 管理、文件变更同步;Hermes / Reasonix 直接在授权工作区内编辑本地文件。`page_ai_workflow` 只保留为 debug / 历史兼容门面,`mnote.doc.markdown_edit` 不再作为 local-first 默认、fallback 或 remote fallback;`mnote.block.*` 只保留为复杂结构辅助层
|
||||
|
||||
一句话收口:
|
||||
|
||||
@@ -274,7 +274,7 @@ OnlyOffice 仍然是:
|
||||
对应设计稿:
|
||||
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
|
||||
|
||||
### 8.2 Tree Command Cutover
|
||||
@@ -325,18 +325,19 @@ OnlyOffice 仍然是:
|
||||
关键原则(2026-05-18 口径更新):
|
||||
|
||||
- **local-first 主路径**:当前页面解析成授权 `.md` 文件,Hermes / Reasonix 在白名单目录内直接读写,MNote 负责权限、审计和前台同步
|
||||
- **兼容两层模型**(参考 CLI Main Lark Doc):`mnote.doc.markdown_edit` 作为文本级兼容 / fallback,`mnote.doc.apply_block_ops` 作为块级结构性辅助;local-first 正常编辑优先走“授权文件引用 + agent 原生 patch/diff + 文件版本冲突模型”
|
||||
- **local-first 文件编辑模型**:普通 Markdown 编辑走“页面定位 + 授权文件引用 + agent 原生 patch/diff + 文件版本冲突模型”;MNote 不提供普通 Markdown 编辑工具,`mnote.doc.markdown_edit` 不再作为默认、fallback 或 remote fallback,`mnote.doc.apply_block_ops` 仅作为块级结构性辅助。CLI Main Lark Doc 只能作为 skill/workflow 纪律的有限参考,不作为当前编辑架构主参考。
|
||||
- `mnote.block.*` 降级为结构性辅助(拖拽排序、精确块删除等),不删除
|
||||
- 当前 `local_rule` planner(`direct_block_edit_operations`)是过渡实现,应继续退役
|
||||
- cloud / remote agent 无法直接访问本地文件时,才回到 `resolve_source` → Convex | LocalFS 的受控代理写入
|
||||
- cloud / remote agent 无法直接访问本地文件时,不默认回退到 `mnote.doc.markdown_edit`;需要另行设计显式同步 / cloud source 边界
|
||||
- Markdown 既是 AI 编辑格式也是人类可读格式,不需要 XML 中间层
|
||||
- 流式 apply + suggest/review(参考 BlockNote AI 的 `StreamToolExecutor` + `suggestChanges`)仅作为 Phase C 设计冻结,当前不实施
|
||||
|
||||
对应设计稿:
|
||||
|
||||
- `/mnt/Data1T/mnote/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/old/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`(历史执行记录;不再作为 active 入口)
|
||||
- `/mnt/Data1T/mnote/design/10-review/done/09-page-ai-fast-block-edit-runtime-review.md`
|
||||
- `/mnt/Data1T/mnote/design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/old/07-ai/process/7-14-local-first-ai-markdown-editing-convergence-v1.md`(历史回收稿;不再作为当前实现依据)
|
||||
|
||||
### 8.5 定向 Bug Hunt 与质量基线
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# 3-23 [done][bug] dev:hot 启动 inotify 限制导致 local-folder events 500 v1
|
||||
|
||||
> 发现时间:2026-05-23
|
||||
>
|
||||
> 状态:`[done]`
|
||||
>
|
||||
> 关联主线:`03-rust-web`
|
||||
|
||||
## 1. 用户可见症状
|
||||
|
||||
执行:
|
||||
|
||||
```bash
|
||||
npm run dev:hot
|
||||
```
|
||||
|
||||
启动日志出现:
|
||||
|
||||
```text
|
||||
WARN System notification limit is too small, falling back to polling mode.
|
||||
WARN Polling for changes every 500ms
|
||||
ERROR response failed classification=Status code: 500 Internal Server Error latency=0 ms
|
||||
```
|
||||
|
||||
`mnote-web` 本身能启动,但首屏随后会打一条 500。
|
||||
|
||||
## 2. 根因
|
||||
|
||||
浏览器网络请求定位到失败 endpoint:
|
||||
|
||||
```text
|
||||
GET /api/local-folder/events?rootUri=...
|
||||
```
|
||||
|
||||
该请求返回的错误体为:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "internal_error",
|
||||
"message": "本地文件夹监听失败: OS file watch limit reached. about [\"/mnt/Data1T/Mnote_data/users/mnote-e2e/workspaces/my-space\"]"
|
||||
}
|
||||
```
|
||||
|
||||
因此根因不是 route 业务逻辑回归,而是 Linux `inotify` watcher 容量不足:
|
||||
|
||||
- `cargo watch` 在启动时检测到系统通知额度不足,降级为 polling。
|
||||
- `mnote-web` 首屏建立本地工作区 SSE watcher 时也申请 watcher。
|
||||
- 当前用户已有 watcher 占用接近系统上限,导致 `/api/local-folder/events` 创建监听失败并返回 500。
|
||||
|
||||
## 3. 修复
|
||||
|
||||
已在系统层即时提高并持久化 inotify 容量:
|
||||
|
||||
```bash
|
||||
sudo sysctl fs.inotify.max_user_watches=1048576 fs.inotify.max_user_instances=1024 fs.inotify.max_queued_events=32768
|
||||
```
|
||||
|
||||
持久配置写入:
|
||||
|
||||
```text
|
||||
/etc/sysctl.d/99-mnote-dev.conf
|
||||
```
|
||||
|
||||
内容:
|
||||
|
||||
```text
|
||||
fs.inotify.max_user_watches=1048576
|
||||
fs.inotify.max_user_instances=1024
|
||||
fs.inotify.max_queued_events=32768
|
||||
```
|
||||
|
||||
## 4. 验证
|
||||
|
||||
验证系统参数:
|
||||
|
||||
```bash
|
||||
sysctl fs.inotify.max_user_watches fs.inotify.max_user_instances fs.inotify.max_queued_events
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
```text
|
||||
fs.inotify.max_user_watches = 1048576
|
||||
fs.inotify.max_user_instances = 1024
|
||||
fs.inotify.max_queued_events = 32768
|
||||
```
|
||||
|
||||
验证 SSE:
|
||||
|
||||
```text
|
||||
/api/local-folder/events?... 返回 200,并收到 ready 事件。
|
||||
```
|
||||
|
||||
验证启动:
|
||||
|
||||
```bash
|
||||
FRONTEND_PORT=3107 timeout 10s npm run dev:hot
|
||||
```
|
||||
|
||||
结果:无 `System notification limit` warning,无启动期 500。
|
||||
|
||||
重新启动真实入口:
|
||||
|
||||
```bash
|
||||
npm run dev:hot
|
||||
```
|
||||
|
||||
结果:`http://localhost:3000` 正常启动;浏览器首屏网络请求中 `/api/local-folder/events?...` 为 200。
|
||||
|
||||
## 5. 剩余边界
|
||||
|
||||
这是系统资源配置问题,本仓库代码不需要为本次 inotify 限制做业务改动。若后续再次出现同类 500,应先统计当前 inotify watcher 占用和后台进程,再判断是继续提高系统额度还是清理异常 watcher 进程。
|
||||
@@ -0,0 +1,38 @@
|
||||
# 4-49 [process][bug] 文件树拖拽上传文件落到同级而非目标文件夹下 v1
|
||||
|
||||
> 发现时间:2026-05-23
|
||||
>
|
||||
> 状态:`[process]`
|
||||
>
|
||||
> 关联主线:`04-tree-domain`
|
||||
|
||||
## 1. 用户可见症状
|
||||
|
||||
原始记录:
|
||||
|
||||
```text
|
||||
我的空间:向文件树中拖动文件上传文件,能正常在主编辑 md 中显示,点击也能正常打开,但是见 /mnt/Data1T/mnote/tmp/image copy 65.png,文件是和主文件夹同级(应该在我的文件夹下级)。
|
||||
```
|
||||
|
||||
## 2. 初步判断
|
||||
|
||||
这是文件树拖拽上传目标解析问题,不是附件点击打开问题:
|
||||
|
||||
- 上传后的附件块能正常插入主编辑器。
|
||||
- 点击附件能正常打开。
|
||||
- 异常点是资源文件在文件树中的落点:实际落到主文件夹同级,而不是拖拽目标文件夹下级。
|
||||
|
||||
## 3. 待排查方向
|
||||
|
||||
- 文件树拖拽上传时的 target row / parent folder 是否正确写入 upload preflight。
|
||||
- `local_folder/assets/upload` 或 upload target plan 是否丢失目标目录。
|
||||
- 页面文件夹 / 当前页面资源归属和文件树手动目标目录之间是否存在优先级覆盖。
|
||||
|
||||
## 4. 验收建议
|
||||
|
||||
后续修复时应补 smoke:
|
||||
|
||||
- 在文件树中创建一个目标文件夹。
|
||||
- 将附件拖拽到该文件夹行。
|
||||
- 断言附件在文件树中位于目标文件夹下级。
|
||||
- 断言主编辑器附件块仍能点击在 tab 打开。
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
# 5-38 [done][bug] 第二视图 Markdown 附件上传后主编辑区点击失效 v1
|
||||
|
||||
> 发现时间:2026-05-23
|
||||
>
|
||||
> 状态:`[done]`
|
||||
>
|
||||
> 关联主线:`05-editor-mainline`
|
||||
|
||||
## 1. 用户可见症状
|
||||
|
||||
打开第一视图和第二视图后,在第二视图上传 Markdown 附件会出现一组连续问题:
|
||||
|
||||
- 第一次点击第二视图上传后的文件时,第一视图菜单会短暂弹出后消失。
|
||||
- 上传第二个 Markdown 文件后,页面会卡住或主编辑区附件点击无反应。
|
||||
- 刷新后上传的 `.md` 附件曾退化成普通链接,只能新窗口打开。
|
||||
- 修复退化为链接后,仍存在“上传第二个文件后,之前所有附件都不能从主编辑器点击在 tab 打开”的问题。
|
||||
- 同时点击左侧文件树会把焦点跳到当前文件夹最上方文件,但文件树直接打开文件仍可工作。
|
||||
|
||||
## 2. 根因
|
||||
|
||||
问题不是第二视图和主视图是否应完全隔离这么简单,而是附件打开、上传插入、刷新重建三条链路没有共享同一个 pane-aware 资源 tab 合同:
|
||||
|
||||
- 编辑器内上传后的本地 `.md` 附件在 Markdown 回写 / 重新解析时没有稳定保留为 `data-mnote-attachment-link` 语义,刷新后容易回退成普通链接。
|
||||
- 资源 openTarget=side 和 document secondary pane 的 URL / DOM 状态边界不完整,导致 secondary 资源 tab 打开时可能污染 primary 的 slash menu / active tab。
|
||||
- 主编辑器内附件点击只识别部分增强后的链接形态,第二次上传后 DOM 重建路径会让既有附件链接失去可点击的 tab 打开行为。
|
||||
- 文件树 active/reveal 逻辑和编辑器附件打开逻辑共享了一部分全局焦点状态,导致点击文件树时出现跳到顶部文件的伴随现象。
|
||||
|
||||
## 3. 修复
|
||||
|
||||
本轮修复把上传、解析、回写、点击打开统一到附件资源 tab 语义:
|
||||
|
||||
- 本地 Markdown 页面和 Markdown 资源读取时,根据 `.mnote` 元数据把属于当前文档的上传附件路径传给 Markdown parser,恢复附件链接语义。
|
||||
- Markdown 回写时支持把 `/api/local-folder/files/open?...` 这类本地打开 URL 改写成相对路径,避免刷新后退化为不可识别的普通链接。
|
||||
- 编辑器附件增强逻辑扩大到 `.ProseMirror a[href*="/api/local-folder/files/open"]`,即使 DOM 重建后仍能恢复 `data-mnote-attachment-link`、附件样式和 tab 打开行为。
|
||||
- 上传插入使用当前编辑器 root / pane role 作为目标上下文,secondary 上传只插入 secondary 资源编辑器,不污染 primary。
|
||||
- 资源 `openTarget=side` 与 `secondaryDocumentId` 分离:资源 tab 可在 secondary 打开,但不冒充 secondary 文档页。
|
||||
- smoke 覆盖连续上传两个真实 `.md` 附件、刷新后点击两个附件、再次上传第二个资源并确认都在对应 pane 的 tab 内打开。
|
||||
|
||||
## 4. 验证
|
||||
|
||||
已执行过的关键验证:
|
||||
|
||||
```bash
|
||||
cargo check -p mnote-web
|
||||
node scripts/task459-local-markdown-attachment-tab-smoke.js
|
||||
node scripts/task472-side-target-secondary-pane-smoke.js
|
||||
```
|
||||
|
||||
本次提交前已重新执行:
|
||||
|
||||
```bash
|
||||
cargo check -p mnote-web
|
||||
node scripts/task459-local-markdown-attachment-tab-smoke.js
|
||||
node scripts/task472-side-target-secondary-pane-smoke.js
|
||||
```
|
||||
|
||||
结果:全部通过。
|
||||
|
||||
## 5. 剩余边界
|
||||
|
||||
本记录只覆盖本地文件夹 Markdown 附件在主编辑器 / secondary pane / resource tab 中的上传和打开链路。OnlyOffice 编辑保存、Office 插件噪声和远端 cloud source 附件合同仍由对应 Office / Rust Web 缺陷记录继续跟踪。
|
||||
@@ -20,9 +20,9 @@
|
||||
> - 旧的“Convex 作为默认自托管存储 / 实时 / 文件底座”口径只作为当前代码过渡态理解,不再作为新增能力默认方向。
|
||||
>
|
||||
> 2026-05-16 口径补充:
|
||||
> - `5-4` 的默认页面内 `leptos-tiptap` island 主链切流已完成并迁入 `done/`;官方模板视觉和菜单细节继续由 `5-2 / 5-7 / 5-9` 承接。
|
||||
> - `5-4` 的默认页面内 `leptos-tiptap` island 主链切流已完成并迁入 `done/`;官方模板视觉和菜单细节继续由 `5-2 / 5-7 / 5-9` 作为参考材料承接。
|
||||
> - Convex File Tree 默认可见页面正文行已由 `4-35` 收口为 `doc:<documentId>` + `{title}.md`,旧 `index.md` 可见 UI 口径只作为历史记录理解。
|
||||
> - 页面/块 AI tools 最小闭环已启动,执行验收继续由 `7-10` 承接,不改变 `Page Aggregate / tree command / tree realtime` 三条架构优先级。
|
||||
> - 页面/块 AI tools 最小闭环已启动;旧 `7-10` 已降级为历史执行记录,`7-14` 也已移入 `old/`,不再作为当前 AI 编辑实现依据;当前 AI 编辑 active 稿为 `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md`,直接基于 SQLite control-plane + local-first `.md` + allowed roots + agent 原生 patch/diff,不改变 `Page Aggregate / tree command / tree realtime` 三条架构优先级。
|
||||
|
||||
这份总览只做一件事:
|
||||
|
||||
@@ -32,10 +32,10 @@
|
||||
|
||||
下面三份仍然是当前架构判断的上位依据:
|
||||
|
||||
- `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-tree-first-graph-kernel-v1.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/done/2-8-convex-replace-with-rust-sqlite-control-plane-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/03-rust-web/reference/3-rust-web-long-term-architecture-v1.md`
|
||||
|
||||
补充:旧稿 `/mnt/Data1T/mnote/design/old/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md` 已降级为 `[recycle][legacy]` 历史过渡判断,只解释“不要无计划硬拆 Convex”,不再作为新增能力默认存储口径。
|
||||
|
||||
@@ -78,7 +78,7 @@ P0-P6 已经有最小闭环证据;P7 已完成阶段性瘦身,但还不是
|
||||
当前状态:
|
||||
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
|
||||
|
||||
阶段性结论:
|
||||
@@ -107,7 +107,9 @@ P0-P6 已经有最小闭环证据;P7 已完成阶段性瘦身,但还不是
|
||||
|
||||
当前状态:
|
||||
|
||||
- `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/03-rust-web/done/3-3-rust-web-tree-realtime-event-stream-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/03-rust-web/done/3-14-rust-web-tree-realtime-ws-push-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/03-rust-web/done/3-18-local-folder-tree-live-consumer-convergence-checklist-v1.md`
|
||||
|
||||
阶段性结论:
|
||||
|
||||
@@ -115,14 +117,14 @@ P0-P6 已经有最小闭环证据;P7 已完成阶段性瘦身,但还不是
|
||||
- `3000` 主壳已接入 WS snapshot / delta / resync consumer,SSE 仅作为 fallback
|
||||
- 双浏览器 page/file/resource/trash 操作已有 no-refresh smoke 证据
|
||||
- 本地 folder sidebar watch 已从整页 HTML refetch 改为 projection API 刷新
|
||||
- 下一步重点是把 local-folder watcher 与 tree live cache 进一步统一,减少 polling 型补偿
|
||||
- 当前重点已从“是否接入 live stream”转为减少补偿链、缩薄 legacy consumer 和补稳定 smoke。
|
||||
|
||||
## 3. 当前仍应保留但不在第一线的 process
|
||||
## 3. 当前仍应保留但不在第一线的 reference / done
|
||||
|
||||
下面这些文档仍然有效,但当前不应排在第一优先;引用时必须叠加 2026-05-21 的 local-first MVP 后阶段口径:
|
||||
下面这些文档仍然有效,但当前不应排在第一优先;引用时必须叠加 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`
|
||||
- `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-1-tree-first-graph-kernel-checklist-v2.md`
|
||||
- `/mnt/Data1T/mnote/design/03-rust-web/reference/3-1-rust-web-long-term-checklist-v2.md`
|
||||
- `/mnt/Data1T/mnote/design/04-tree-domain/done/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/05-editor-mainline/reference/5-2-tiptap-notion-like-template-adoption-v1.md`
|
||||
@@ -177,7 +179,7 @@ P0-P6 已经有最小闭环证据;P7 已完成阶段性瘦身,但还不是
|
||||
|
||||
持续推进 checklist:
|
||||
|
||||
- `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-3-current-priority-execution-checklist-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/done/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`
|
||||
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ Owner:
|
||||
|
||||
Owner:
|
||||
|
||||
- `design/01-tree-first-graph-kernel/process/1-6-next-phase-gap-closure-checklist-v1.md`
|
||||
- `design/01-tree-first-graph-kernel/done/1-6-next-phase-gap-closure-checklist-v1.md`
|
||||
- `scripts/task444-convex-workspace-export-local-fixture-smoke.js`
|
||||
- 可新增 `scripts/task48*-convex-export-*.js`
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-8-mvp-post-process-execution-order-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-26-resource-open-resolver-convergence-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-13-rust-web-local-markdown-gfm-ast-parser-migration-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-6-next-phase-gap-closure-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/done/1-6-next-phase-gap-closure-checklist-v1.md`
|
||||
|
||||
---
|
||||
|
||||
|
||||
+2
-2
@@ -58,7 +58,7 @@ Owner:
|
||||
|
||||
Owner:
|
||||
|
||||
- `design/01-tree-first-graph-kernel/process/1-6-next-phase-gap-closure-checklist-v1.md`
|
||||
- `design/01-tree-first-graph-kernel/done/1-6-next-phase-gap-closure-checklist-v1.md`
|
||||
- `rust/crates/mnote-web/src/document_buffer_store.rs`
|
||||
- `rust/crates/mnote-web/src/routes/documents.rs`
|
||||
- `rust/crates/mnote-web/src/routes/web_shell.rs`
|
||||
@@ -84,7 +84,7 @@ Owner:
|
||||
|
||||
Owner:
|
||||
|
||||
- `design/01-tree-first-graph-kernel/process/1-6-next-phase-gap-closure-checklist-v1.md`
|
||||
- `design/01-tree-first-graph-kernel/done/1-6-next-phase-gap-closure-checklist-v1.md`
|
||||
- `rust/crates/core-protocol/src/command.rs`
|
||||
- `rust/crates/mnote-web/src/ssr/pages/layout.rs`
|
||||
- `scripts/task476-filetree-editor-context-menu-download-smoke.js`
|
||||
|
||||
+3
-3
@@ -54,7 +54,7 @@ Owner:
|
||||
Owner:
|
||||
|
||||
- `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`
|
||||
- `design/03-rust-web/reference/3-1-rust-web-long-term-checklist-v2.md`
|
||||
- 只读审查:`rust/crates/mnote-web/src/routes/ws.rs`、`rust/crates/mnote-web/src/routes/tree_events.rs`、`rust/crates/mnote-web/src/ssr/pages/layout.rs`、相关 tree live smoke。
|
||||
|
||||
目标:
|
||||
@@ -73,8 +73,8 @@ Owner:
|
||||
|
||||
Owner:
|
||||
|
||||
- `design/01-tree-first-graph-kernel/process/1-6-next-phase-gap-closure-checklist-v1.md`
|
||||
- `design/01-tree-first-graph-kernel/process/1-5-next-phase-sequential-execution-checklist-v1.md`
|
||||
- `design/01-tree-first-graph-kernel/done/1-6-next-phase-gap-closure-checklist-v1.md`
|
||||
- `design/01-tree-first-graph-kernel/reference/1-5-next-phase-sequential-execution-checklist-v1.md`
|
||||
- `rust/crates/core-protocol/src/command.rs`
|
||||
- `rust/crates/mnote-web/src/hermes_tools/doc.rs`
|
||||
- `rust/crates/mnote-web/src/hermes_tools/page.rs`
|
||||
+2
-2
@@ -42,8 +42,8 @@ Owner:
|
||||
|
||||
Owner:
|
||||
|
||||
- `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/01-tree-first-graph-kernel/reference/1-5-next-phase-sequential-execution-checklist-v1.md`
|
||||
- `design/01-tree-first-graph-kernel/done/1-6-next-phase-gap-closure-checklist-v1.md`
|
||||
|
||||
目标:
|
||||
|
||||
+3
-3
@@ -337,8 +337,8 @@
|
||||
|
||||
### 7.1 导出与备份
|
||||
|
||||
- [x] 设计 Web 导出入口:选择 Convex workspace,选择目标本地 root。
|
||||
- 设计:`design/03-rust-web/reference/3-17-convex-export-web-entry-v1.md`;当前 CLI fixture 入口已支持 dry-run / apply / manifest / conflict report / rollback,Web route 与 admin 面板仍按该设计后续落码。
|
||||
- [x] 冻结旧 Web 导出入口设想,不再作为当前落码入口。
|
||||
- 历史设计:`design/old/03-rust-web/process/3-17-convex-export-web-entry-v1.md`;当前 CLI fixture 入口已支持 dry-run / apply / manifest / conflict report / rollback。由于 Convex 默认运行时与根 functions 已退役,Web route 与 admin 面板不再按该旧稿继续落码,未来如需 cloud/import UI 应按 SQLite control-plane 与 local-first 口径新建 checklist。
|
||||
- [x] 导出前创建 manifest,记录迁移计划、目标 root、workspace、操作列表、冲突、created files、backup dir 与索引刷新结果。
|
||||
- 实现:`scripts/export-convex-workspace-to-local.js --manifest <file>`;apply 成功后同时写入 `<root>/.mnote/migration-manifest.json`。
|
||||
- [x] 导出页面为 `.md`,资源写为本地文件,附件保持相对路径。
|
||||
@@ -467,7 +467,7 @@
|
||||
- [x] P5 simplemindmap / office 资源模型完成 Resource Tree 产品化。
|
||||
- 证据:资源命令面、资源对象壳、AI resource tools 与 changed-files 审计已闭环;`node scripts/task456-resource-object-shell-sync-smoke.js` 已通过。
|
||||
- [x] P6 Convex 导出到本地 workspace 有 dry run、备份、冲突报告和回滚。
|
||||
- 证据:`node scripts/task444-convex-workspace-export-local-fixture-smoke.js`;`node scripts/task455-convex-export-plan-rollback-smoke.js`;Web 入口设计见 `design/03-rust-web/reference/3-17-convex-export-web-entry-v1.md`。
|
||||
- 证据:`node scripts/task444-convex-workspace-export-local-fixture-smoke.js`;`node scripts/task455-convex-export-plan-rollback-smoke.js`;旧 Web 入口设想已退到 `design/old/03-rust-web/process/3-17-convex-export-web-entry-v1.md`,不作为当前 reference 或实施入口。
|
||||
- [x] P7 Page Aggregate / tree command / realtime 兼容链完成阶段性瘦身,并把被替代 process 稿移入 `old/` 或 `done/`。
|
||||
- 证据:Page Aggregate refresh、tree command、local folder projection refresh 与 realtime 双浏览器 smoke 均有验证;`5-5`、`5-6`、`3-3`、`4-34` 仍作为未完全 kernel-native / local-folder no-refresh 深水区过程稿保留在 `process/`,本 checklist 记录阶段性瘦身结果。
|
||||
- [x] `01-05-current-priority-overview.md` 同步更新状态,不再把已完成项描述为当前第一优先级。
|
||||
+35
-24
@@ -2,11 +2,13 @@
|
||||
|
||||
> 创建时间:2026-05-19
|
||||
>
|
||||
> 当前状态:`PARTIALLY_DONE`(Reasonix 已补 A2/A7 基础设施与 A2 保存/watcher 接入;A2 浏览器刷新可见读回、A0 browser smoke、A7 UI 接入、B2/C1 产品化仍未闭环)
|
||||
> 当前状态:`DONE`(A0/A2/A7/B2/C1 已完成;`task431` 只读 DnD smoke 仅作为可选回归项,不再阻塞本清单归档)
|
||||
>
|
||||
> 2026-05-22 归档治理补充:上位路线图 `1-4` 与顺序清单 `1-5` 已移动到 `reference/`;本文只保留真实仍需收口的缺口与证据尾项,不再承接完整下一阶段路线图职责。
|
||||
>
|
||||
> 上位依据:
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-5-next-phase-sequential-execution-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-4-next-phase-execution-roadmap-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-5-next-phase-sequential-execution-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-4-next-phase-execution-roadmap-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference/5-14-zed-lapce-vscode-reference-adoption-matrix-v1.md`
|
||||
>
|
||||
> 目的:基于 Reasonix 执行汇总和本轮核对结果,列出仍不能标完成的缺口,形成可继续顺序执行的补充清单。
|
||||
@@ -25,12 +27,12 @@
|
||||
|
||||
本轮 Reasonix 执行后新增:
|
||||
|
||||
- [x] **Gap A0(部分)**: local-first guard 已通过 ✅(`npm run check:local-first-convex-guard` → `{ok: true, guard: "local-first-convex"}`);browser smoke 尚未复跑。
|
||||
- [x] **Gap A0(主路径完成)**: local-first guard 已通过 ✅(`npm run check:local-first-convex-guard` → `{ok: true, guard: "local-first-convex"}`);`task451` / `task452` / `task453` 已复跑,旧 `task431` 仅保留为可选回归项。
|
||||
- [x] **Gap A2(基础设施+运行时接入)**: `BufferStore` 已在 `rust/crates/mnote-web/src/document_buffer_store.rs` 实现,并加入 `AppState.buffer_store`。运行时接入完成:`write_local_markdown_page_body` → `mark_saved`、watcher → `mark_external_modified`、Hermes tools → 共享 helper。11 个单测全部通过(含 3 个新集成测试)。
|
||||
- [x] **Gap A2(运行时接入—完成)**: `write_local_markdown_page_body` 已接入 `BufferStore.mark_saved()`;`documents.rs` `page_body_write` 和 `save` 路由已传递 `state.buffer_store`;Hermes `doc_apply_block_ops` 和 `page_command` 已传递 `state.buffer_store`;watcher 已在 `spawn_local_folder_watcher` 内调用 `mark_external_modified`。3 个新集成测试覆盖运行时入口。
|
||||
- [x] **Gap A2(浏览器可见闭环)**: Codex 复核新增 `task484-local-folder-page-body-refresh-readback-smoke.js`,验证 `POST /api/page-body/write` 后磁盘 `.md`、Page Aggregate、刷新后的 ProseMirror 均读回新正文;旧的“刷新后 editor DOM 未显示正文”证据已判定为过期。
|
||||
- [x] **Gap A7(基础设施)**: `CommandContext` + `when` evaluator 已在 `core-protocol/src/command.rs` 实现。7 个单测全部通过(含 3 个必补测试)。
|
||||
- [ ] **Gap A7(UI/命令接入)**: File Tree 右键菜单、快捷键、AI 写入能力尚未消费 `CommandContext`。
|
||||
- [x] **Gap A7(UI/命令接入)**: File Tree 右键菜单、Delete/Backspace 快捷键、AI 写入能力已消费 `CommandContext` / `workspace.readonly` / `ai.canWrite`。
|
||||
- [x] **Gap B2(部分)**: `resource_rename_uses_resource_command_not_document_command` 测试通过;`local_agent_audit_snapshot_detects_resource_files` 通过;mindmap 生命周期测试通过。
|
||||
- [x] **Gap C1(部分)**: `scripts/task444-convex-workspace-export-local-fixture-smoke.js` 通过;`export-convex-workspace-to-local.js` 既有 `--dry-run`、`--manifest`、`--conflict-report`、`--rollback` 能力已确认,但本轮未新增对应 smoke。
|
||||
- [x] **core-protocol 全量测试**: 41 单元 + 6 editor bridge + 2 resource tree = 49 通过 ✅
|
||||
@@ -62,16 +64,17 @@
|
||||
- 编号已存在脚本目录
|
||||
- 验收:已复跑 browser smoke;`local_agent_audit_snapshot_detects_changed_files` 和 `detects_resource_files` 只能覆盖 audit 单元路径。
|
||||
- **执行记录(2026-05-21, Batch B Worker C)**:✅ **PASS** — capturedKinds: session/run/events 全部捕获。结果文件:`tmp/task453-local-folder-page-ai-changed-files-smoke/result.json`
|
||||
- **Codex 复核(2026-05-22)**:✅ **PASS** — 已扩展为 local-first AI 前台同步 smoke;clean path 验证 Hermes `run.completed.agentAudit.changedFiles` 后工具卡、磁盘 `.md`、Page Aggregate、ProseMirror 均出现 marker 且不进冲突态;dirty path 验证同一路径触发 `external-change-conflict`、冲突面板显示 agent run、冲突信封包含 `externalActor / dirtyState / bufferFileVersion`,diff 同时显示本地未保存 token 与 AI 写盘 token。
|
||||
|
||||
### 1.2 收尾
|
||||
|
||||
- [x] guard 输出已记录(见上)
|
||||
- [x] browser smokes 需完整后端环境运行(mnote-web + Convex + Playwright)
|
||||
- [x] browser smokes 已完成主路径复跑;`task443` / `task445` 已迁移到 local-first fixture 并作为归档证据。
|
||||
- [x] 失败归属:本轮已无 A0 smoke 失败;`task452` 为旧断言假失败
|
||||
|
||||
---
|
||||
|
||||
## 2. Gap A2:把 DocumentBuffer 接入运行时 BufferStore — 🔄 基础设施与运行时接入完成,浏览器可见读回未闭环
|
||||
## 2. Gap A2:把 DocumentBuffer 接入运行时 BufferStore — ✅ 完成
|
||||
|
||||
### 2.1 当前代码锚点(更新后)
|
||||
|
||||
@@ -87,7 +90,7 @@
|
||||
- [x] 接入 tiptap 保存链:`write_local_markdown_page_body` / `documents.rs` `save` 路由已调用 `state.buffer_store.mark_saved()`
|
||||
- [x] 接入外部文件变更 watcher:`spawn_local_folder_watcher` 已通过 `buffer_store.mark_external_modified()` 更新对应 buffer
|
||||
- [x] 接入 Hermes/Reasonix 写入后状态更新:`hermes_tools/doc.rs` 和 `hermes_tools/page.rs` 已通过共享 helper `write_local_markdown_page_body` 自动消费 BufferStore
|
||||
- [ ] conflict UI 数据源改为 buffer state:API 已就绪,未接入
|
||||
- [x] conflict UI 数据源改为 buffer state:冲突信封已包含 `externalActor / dirtyState / bufferFileVersion`,`task451` 已覆盖
|
||||
- [x] 浏览器可见读回:`task484-local-folder-page-body-refresh-readback-smoke.js` 已验证保存后刷新 ProseMirror 可见正文不丢失
|
||||
|
||||
### 2.3 必补测试
|
||||
@@ -118,7 +121,7 @@
|
||||
|
||||
---
|
||||
|
||||
## 3. Gap A7:落地 command context / context key — 🔄 基础设施完成,UI/命令接入未完成
|
||||
## 3. Gap A7:落地 command context / context key — ✅ 主路径完成
|
||||
|
||||
### 3.1 当前代码锚点(更新后)
|
||||
|
||||
@@ -149,7 +152,7 @@
|
||||
- [x] readonly / grant / resource kind 的禁用态可测试
|
||||
- [x] **FileTree 右键菜单已接入**(Batch C Worker C):delete-trash、rename、new-file、new-folder、paste-into 携带 `when: '!workspace.readonly'`
|
||||
- [x] **Delete/Backspace 快捷键已接入**(Batch C Worker C):keydown handler 中有 `!workspace.readonly && tree.selectionCount` 守卫
|
||||
- [ ] **未完成**:下载、多选删除、粘贴、只读阻断的 smoke 保持回退兼容
|
||||
- [x] 下载、多选删除、粘贴、只读阻断的主路径 smoke 已由 `task471` / `task476` 在 Batch H 复跑覆盖;`task431` 作为更旧 DnD readonly smoke 仍保留为可选回归项。
|
||||
- [x] **已完成**:AI 写入能力 (`ai.canWrite`) 已接入 hermes_tools 写入守卫;`mnote.block.*`、`mnote.doc.markdown_edit`、`mnote.page.*`、`mnote.artifact.*`、`mnote.mindmap.apply_ops` 写入链共享 `ensure_write_authorized`,只读 scope、`workspace.readonly=true` 或 `ai.canWrite=false` 均会拒绝写入。
|
||||
|
||||
### 3.5 下一步
|
||||
@@ -157,6 +160,7 @@
|
||||
- `layout.rs` 右键菜单:已引入 `evaluateSidebarFileTreeWhen`(Batch C Worker C 完成)
|
||||
- Delete/Backspace:已接入(Batch C Worker C 完成)
|
||||
- hermes_tools 写入守卫:已消费 `ai.canWrite` / `workspace.readonly` command context;后续若前端新增更多 AI 写入口,必须把同一份 `commandContext` 透传到 tool args。
|
||||
- 归档前可选补跑 `task431-vscode-explorer-dnd-readonly-conflict-smoke.js`,但它不再代表 A7 主路径缺失。
|
||||
|
||||
### 3.6 Batch F 复核记录
|
||||
|
||||
@@ -187,13 +191,18 @@
|
||||
|
||||
- [x] `tree_resource_rename_does_not_emit_document_command` — 被现有测试覆盖:
|
||||
- `resource_rename_uses_resource_command_not_document_command` ✅(验证 canonicalCommand 为 `tree.resource.rename` 而非 `tree.node.rename`)
|
||||
- [ ] `mindmap_resource_trash_restore_keeps_markdown_body` — 部分覆盖:
|
||||
- [x] `mindmap_resource_trash_restore_keeps_markdown_body` — 已覆盖:
|
||||
- `mindmap_delete_restore_keeps_markdown_reference_out_of_lifecycle_command` ✅(验证 mindmap 生命周期不携带 documentId 从而不影响 markdown 页面)
|
||||
- 尚未验证文档正文在 trash/restore 后是否完整
|
||||
- `local_folder_mindmap_delete_moves_resource_file_to_trash` ✅(2026-05-22 补断言:archive / restore 后 `Page.md` 正文引用保持原文)
|
||||
- [x] `local_agent_audit_snapshot_detects_resource_files` — 通过 ✅
|
||||
- [x] browser smokes:`task443`、`task445` 需 Playwright 后端环境
|
||||
- **执行记录(2026-05-21, Batch B Worker C)**:❌ **FAIL** — 两个 smoke 均因 `waitForURL` 断言 `/mindmap/` 路由超时,实际导航走的是 `/documents/...?resourceTab=...`。归因:**脚本过时**(mindmap 路由改为 resourceTab,smoke 未同步)。结果文件:`tmp/task443-filetree-mindmap-click-active-row-smoke/result.json`、`tmp/task445-filetree-mindmap-switch-no-flicker-smoke/result.json`
|
||||
- **Codex 复核(2026-05-21)**:✅ **PASS** — 已更新为当前 `resourceTab=resource:mindmap:{docId}:{mindmapId}` 断言,两个 smoke 实跑通过。`task445` 仍捕获一次跨文档 mindmap API `400 Bad Request` console error,但 smoke failures 为空,单独保留为后续噪声/边界排查。
|
||||
- **Codex 复核(2026-05-22)**:✅ **代码级修复** — `task445` 的 400 不是纯噪声,根因是跨文档后 asset row 打开时沿用了当前 URL 的 document,而不是资源 owner document。已补 `sidebar_filetree_asset_open_uses_owner_document_id` 回归测试,并让 filetree asset row 输出/消费 `data-owner-document-id`;`task445` 同步增加 owner URL 断言。浏览器复跑当前被旧 Convex fixture 阻塞:`workspaces:ensureDefaultWorkspace` function 已退役,返回 502。
|
||||
- **Codex 复核(2026-05-22 续)**:✅ **PASS** — `task443` / `task445` 已迁移为 local-first fixture(临时 root + `.mnote/workspace.json` + 本地 `.md` / `.mindmap.json`),不再依赖退役 Convex function。复跑命令:
|
||||
- `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task443-filetree-mindmap-click-active-row-smoke.js`
|
||||
- `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task445-filetree-mindmap-switch-no-flicker-smoke.js`
|
||||
- 两者结果均为 PASS,结果文件分别为 `tmp/task443-filetree-mindmap-click-active-row-smoke/result.json`、`tmp/task445-filetree-mindmap-switch-no-flicker-smoke/result.json`。
|
||||
|
||||
### 4.4 完成标准
|
||||
|
||||
@@ -265,19 +274,19 @@
|
||||
- 结果:`tmp/task452-local-search-index-browser-smoke/result.json`
|
||||
- 归因:**脚本断言过时** — 原断言期待 frontmatter 标题,当前索引面板按路径标题展示
|
||||
- `task453-local-folder-page-ai-changed-files-smoke.js`
|
||||
- **PASS** ✅ — capturedKinds: session/run/events 全部捕获
|
||||
- **PASS** ✅ — clean AI 写入后 Page Aggregate / ProseMirror 前台同步,dirty AI 写入触发 conflict UI 并显示 BufferStore 信封
|
||||
- 命令:同上
|
||||
- 结果:`tmp/task453-local-folder-page-ai-changed-files-smoke/result.json`
|
||||
- `task443-filetree-mindmap-click-active-row-smoke.js`
|
||||
- **PASS** ✅ — mindmap 资源行点击后 URL 使用 `/documents/...&resourceTab=resource:mindmap:...`,且对应 asset row 选中
|
||||
- **PASS** ✅ — local-first fixture 下,mindmap 资源行点击后 URL 使用 `/documents/...&resourceTab=resource:mindmap:...`,且对应 asset row 选中
|
||||
- 命令:`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task443-filetree-mindmap-click-active-row-smoke.js`
|
||||
- 结果:`tmp/task443-filetree-mindmap-click-active-row-smoke/result.json`
|
||||
- 归因:**脚本过时后已更新** — mindmap 已改为 resourceTab 路由
|
||||
- 归因:**脚本过时后已更新并去 Convex fixture 化** — mindmap 已改为 resourceTab 路由,fixture 改为 local_folder
|
||||
- `task445-filetree-mindmap-switch-no-flicker-smoke.js`
|
||||
- **PASS** ✅ — 页面和 mindmap 来回切换时 file tree root 稳定,选中态正确
|
||||
- **PASS** ✅ — local-first fixture 下,页面和 mindmap 来回切换时 file tree root 稳定,选中态正确;跨文档资源重新打开时 URL pathname 使用资源 owner document
|
||||
- 命令:`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/google-chrome-stable node scripts/task445-filetree-mindmap-switch-no-flicker-smoke.js`
|
||||
- 结果:`tmp/task445-filetree-mindmap-switch-no-flicker-smoke/result.json`
|
||||
- 归因:**脚本过时后已更新** — 仍有一次 400 console noise,后续单独排查
|
||||
- 归因:**脚本过时后已更新并去 Convex fixture 化** — 400 根因已收口到 ResourceTab owner document URL 同步
|
||||
|
||||
### 6.2.2 本次执行汇总
|
||||
|
||||
@@ -285,9 +294,9 @@
|
||||
|-------|------|------|
|
||||
| task451 | ✅ PASS | — |
|
||||
| task452 | ✅ PASS | 脚本断言过时,已按当前路径标题和链接片段语义更新 |
|
||||
| task453 | ✅ PASS | — |
|
||||
| task443 | ✅ PASS | 脚本过时,已改为 resourceTab 路由断言 |
|
||||
| task445 | ✅ PASS | 脚本过时,已改为 resourceTab 路由断言;仍有 400 console noise |
|
||||
| task453 | ✅ PASS | 已扩展为 AI clean/dirty 前台同步与 conflict UI 证据 |
|
||||
| task443 | ✅ PASS | 已改为 resourceTab 路由断言,并迁移到 local-first fixture |
|
||||
| task445 | ✅ PASS | 已改为 resourceTab 路由断言,并迁移到 local-first fixture;ResourceTab owner document URL 已修复 |
|
||||
|
||||
---
|
||||
|
||||
@@ -295,8 +304,10 @@
|
||||
|
||||
| Gap | 状态 | 关键产出 | 剩余 |
|
||||
|-----|------|----------|------|
|
||||
| A0 | 🔄 部分 | guard、task451、task452、task453 通过 | task431 等剩余 browser smokes 未复跑 |
|
||||
| A2 | 🔄 运行时接入 + 浏览器读回完成 | `BufferStore` 实现 + 11 tests;task484 验证保存后磁盘、PageAggregate、刷新后 ProseMirror 全部读回 | conflict UI 未接 buffer state |
|
||||
| A7 | 🔄 基础设施完成 | `CommandContext` + when evaluator + 7 tests | UI/快捷键/菜单接入 |
|
||||
| B2 | 🔄 部分 | 资源生命周期 + agent audit 测试通过;task443/task445 通过 | mindmap body 测试、跨文档 ResourceTab 400 噪声排查 |
|
||||
| A0 | ✅ 主路径完成 | guard、task451、task452、task453 通过 | task431 可选复跑 |
|
||||
| A2 | ✅ 完成 | `BufferStore` 实现 + 11 tests;task451 覆盖 BufferStore 冲突信封;task484 验证保存后磁盘、PageAggregate、刷新后 ProseMirror 全部读回 | — |
|
||||
| A7 | ✅ 主路径完成 | `CommandContext` + when evaluator + 7 tests;FileTree 菜单、Delete/Backspace、AI 写入守卫接入 | task431 可选复跑 |
|
||||
| B2 | ✅ 主路径完成 | 资源生命周期 + agent audit 测试通过;mindmap 正文引用保持、ResourceTab owner 修复与 task443/task445 local-first browser 证据已补齐 | Resource Tree projection 产品形态深化另列后续项 |
|
||||
| C1 | ✅ 完成 | export/rollback/dry-run 脚本 + task444/task455 smoke 全部通过 | — |
|
||||
|
||||
归档结论:本清单原本承担的 gap closure 已完成。后续活跃执行转入 `1-8` 的顺序表与 `7-14` 的 AI Markdown 编辑路径收敛,不再把本文件作为 active process。
|
||||
+38
-26
@@ -4,6 +4,8 @@
|
||||
>
|
||||
> 当前状态:`PROCESS`
|
||||
>
|
||||
> 2026-05-22 归档治理补充:当前 active `process/` 只保留可直接执行的少量收口稿;长期架构、愿景、参考矩阵、Wolai 对标基线、Mindmap 总设计、旧 AI 块级执行稿和 Convex Web 迁移入口已分别移动到 `reference/`、`done/` 或 `old/`。本文是后续调度入口,不代表下列所有历史入口仍在 `process/`。
|
||||
>
|
||||
> 目标:把本轮 design governance 后剩余的主线 `process/` 文档排成可执行顺序,避免后续 worker 在 active process 中自行猜优先级。
|
||||
>
|
||||
> 上位依据:
|
||||
@@ -115,9 +117,9 @@
|
||||
|
||||
入口文档:
|
||||
|
||||
- `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/01-tree-first-graph-kernel/reference/1-4-next-phase-execution-roadmap-v1.md`
|
||||
- `design/01-tree-first-graph-kernel/reference/1-5-next-phase-sequential-execution-checklist-v1.md`
|
||||
- `design/01-tree-first-graph-kernel/done/1-6-next-phase-gap-closure-checklist-v1.md`
|
||||
- `design/05-editor-mainline/done/5-26-resource-open-resolver-convergence-checklist-v1.md`
|
||||
|
||||
执行目标:
|
||||
@@ -141,9 +143,9 @@
|
||||
|
||||
入口文档:
|
||||
|
||||
- `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`
|
||||
- `design/01-tree-first-graph-kernel/reference/1-5-next-phase-sequential-execution-checklist-v1.md`
|
||||
- `design/01-tree-first-graph-kernel/done/1-6-next-phase-gap-closure-checklist-v1.md`
|
||||
- `design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
|
||||
执行目标:
|
||||
|
||||
@@ -165,7 +167,7 @@
|
||||
|
||||
入口文档:
|
||||
|
||||
- `design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
- `design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
- `design/05-editor-mainline/reference/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
- `design/03-rust-web/done/3-13-rust-web-local-markdown-gfm-ast-parser-migration-v1.md`
|
||||
|
||||
@@ -193,7 +195,7 @@
|
||||
入口文档:
|
||||
|
||||
- `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`
|
||||
- `design/03-rust-web/reference/3-1-rust-web-long-term-checklist-v2.md`
|
||||
|
||||
执行目标:
|
||||
|
||||
@@ -235,7 +237,7 @@
|
||||
|
||||
入口文档:
|
||||
|
||||
- `design/01-tree-first-graph-kernel/process/1-5-next-phase-sequential-execution-checklist-v1.md`
|
||||
- `design/01-tree-first-graph-kernel/reference/1-5-next-phase-sequential-execution-checklist-v1.md`
|
||||
- `design/04-tree-domain/done/4-44-filetree-action-layer-paste-drop-delete-v1.md`
|
||||
|
||||
执行目标:
|
||||
@@ -261,28 +263,36 @@
|
||||
入口文档:
|
||||
|
||||
- `design/07-ai/done/7-27-online-markdown-writeback-final-content-truth-v2.md`
|
||||
- `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md`
|
||||
|
||||
执行目标:
|
||||
|
||||
1. 修正 online / compat `mnote.doc.markdown_edit` 的 final markdown 写回真相。
|
||||
2. 避免 full_content 和多 operation 场景仍从旧 blocks 二次推导。
|
||||
3. 保持 local-first 默认编辑仍走 agent 原生 patch/diff,不把 `markdown_edit` 升回默认主路径。
|
||||
1. 将 `7-27` 限定为历史 online / compat 回归证据,不再把 `mnote.doc.markdown_edit` 推回 active 设计。
|
||||
2. 以 `7-18` 为当前 AI 编辑执行入口:MNote 只提供文件定位、授权边界、selection/context、审计和前台同步。
|
||||
3. Hermes / Reasonix 用自身文件 read / write / patch / diff 能力在 allowed roots 内修改 `.md`。
|
||||
4. 先实现 Agent Target Resolver / target chip / target picker,解决多工作区、多 tab、多资源时到底把哪个对象发送给 agent 的问题。
|
||||
|
||||
验收:
|
||||
|
||||
- `cargo test -p mnote-web hermes_tools -- --test-threads=1`
|
||||
- 覆盖 full_content、多 op、inline content、selection scope 的回归测试。
|
||||
- Page AI composer 可见 target chip,能展示 workspace、target title、resourceKind、scope、dirty / readonly 状态。
|
||||
- target picker 能在当前焦点、打开 tabs、树选中项之间选择;默认 target 来自 last focused editor / resource tab。
|
||||
- local-first agent 编辑 smoke 断言不调用 `mnote.doc.markdown_edit`。
|
||||
- Hermes / Reasonix run body 包含 `currentFile`、`allowedRoots`、`allowedFiles`、selection 和 dirty / readonly context。
|
||||
- Hermes / Reasonix run body 包含 `targetPackage`,且切 tab 后不影响已冻结 run target。
|
||||
- agent 写入后 watcher -> BufferStore -> Page Aggregate -> ProseMirror 可见更新。
|
||||
- dirty buffer 下 agent 写入不会静默覆盖。
|
||||
|
||||
归档条件:
|
||||
|
||||
- `7-27` 的 6 步实施计划已完成并归档;后续只保留 `revisionRef` 注释可见性、复杂 GFM fallback、多余退役函数体清理等增强项,不阻塞 Batch E。
|
||||
- `7-18` Phase A0/A/B/C 完成后归档到 `design/07-ai/done/`。
|
||||
|
||||
### 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`
|
||||
- `design/07-ai/done/7-15-page-ai-acp-agent-runtime-unified-layer-v1.md`
|
||||
- `design/07-ai/reference/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
|
||||
- `design/07-ai/done/7-34-acp-runtime-cleanup-availability-stability-tail-v1.md`
|
||||
|
||||
执行目标:
|
||||
@@ -307,8 +317,8 @@
|
||||
|
||||
入口文档:
|
||||
|
||||
- `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/reference/6-mindmap-kernel-phase6-projection-editor-v1.md`
|
||||
- `design/06-mindmap/done/6-mindmap-phase6-leptos-ui-shell-reuse-checklist-v1.md`
|
||||
- `design/06-mindmap/reference/6-mindmap-leptos-adapter-reference-notes-v1.md`
|
||||
|
||||
执行目标:
|
||||
@@ -326,7 +336,7 @@
|
||||
|
||||
入口文档:
|
||||
|
||||
- `design/05-editor-mainline/process/5-9-wolai-aline-continuous-checklist-v1.md`
|
||||
- `design/05-editor-mainline/reference/5-9-wolai-aline-continuous-checklist-v1.md`
|
||||
- `design/08-wolai-aline-test-flow/reference/wolai-aline-test-flow-v1.md`
|
||||
- `design/05-editor-mainline/reference/5-7-wolai-page-tree-main-editor-experience-restoration-v1.md`
|
||||
|
||||
@@ -344,8 +354,8 @@
|
||||
|
||||
这些文档不进入当前直接执行序列,只有当产品优先级明确时再拆 checklist:
|
||||
|
||||
- `design/03-rust-web/reference/3-17-convex-export-web-entry-v1.md`
|
||||
- CLI 已有迁移闭环;Web 管理入口未排期。
|
||||
- `design/old/03-rust-web/process/3-17-convex-export-web-entry-v1.md`
|
||||
- CLI 已有迁移闭环;Web 管理入口在当前 Convex 默认退役后已降级为历史设想,不作为 active 或 reference 执行入口。
|
||||
- `design/04-tree-domain/reference/4-23-local-cloud-explicit-bridge-p3-candidate-v1.md`
|
||||
- P3 候选,不能进入 local-first 默认主链。
|
||||
- `design/07-ai/reference/7-17-acp-session-convex-sharing-contract-v1.md`
|
||||
@@ -410,16 +420,18 @@
|
||||
|
||||
### Batch E:AI 与资源工具
|
||||
|
||||
1. `7-27`(已归档到 `design/07-ai/done/`)
|
||||
2. `7-15` 后续拆分(下一批拆 `7-34`)
|
||||
3. `7-12` Phase B-F 拆分(下一批拆 `7-34`)
|
||||
4. `7-28` 仅在 mindmap / office AI 明确排期后进入
|
||||
1. `7-18`
|
||||
2. `7-27`(已归档到 `design/07-ai/done/`,只作为历史 online / compat 回归证据)
|
||||
3. `7-15` 后续拆分(下一批拆 `7-34`)
|
||||
4. `7-12` Phase B-F 拆分(下一批拆 `7-34`)
|
||||
5. `7-28` 仅在 mindmap / office AI 明确排期后进入
|
||||
|
||||
完成后预期:local-first AI 主路径稳定,cloud/compat 工具不再误导默认路径。
|
||||
完成后预期:local-first AI 主路径稳定,普通 Markdown 编辑不再依赖 MNote 专用编辑工具。
|
||||
|
||||
当前批次状态:
|
||||
|
||||
- `7-27` 已由 Batch I 复核归档;`mnote.doc.markdown_edit` 在线写回以最终 markdown 为真源,`markdown_edit` 相关 targeted tests 与 manifest write contract 测试已通过。
|
||||
- `7-18` 已成为当前 active AI 编辑控制面稿;后续实现不再保留 `mnote.doc.markdown_edit` 作为 local-first 默认、fallback 或 remote fallback。
|
||||
- `7-29` 已完成 Worker A/B/C 审查,结论是 `7-15` Step 15-17 与 `7-12` Phase B/F 不应继续压在一个大文档里推进。
|
||||
- `7-34` 已完成并归档到 `design/07-ai/done/`:旧 HTTP proxy 默认关闭、`page_ai_workflow` 受共享 tool executor 守卫保护;profile disabled list 三处同源;`capabilityScope` 中心校验已完成;ACP abort API 改为 best-effort cancellation 并主动推送 `run.aborted`;`task488` 已在当前 3000 真实通过,覆盖 3 个 Reasonix ACP session、并发 run、abort 与 SSE terminal event。cache benchmark 仍是 P2 度量;Phase C Review Mode 继续冻结。
|
||||
|
||||
|
||||
+5
-3
@@ -1,11 +1,13 @@
|
||||
# 1-1 [process] Tree-First Graph 内核实施清单 v2
|
||||
# 1-1 [reference] Tree-First Graph 内核实施清单 v2
|
||||
|
||||
> 更新时间:2026-04-16
|
||||
>
|
||||
> 当前状态:`reference`。本文保留 kernel 分阶段全景与历史状态口径;当前执行顺序以 `design/01-tree-first-graph-kernel/process/1-8-mvp-post-process-execution-order-v1.md` 为准。
|
||||
>
|
||||
> 基于以下实际状态重写:
|
||||
> - 当前未提交代码
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/reference/3-1-rust-web-long-term-checklist-v2.md`
|
||||
> - `/mnt/Data1T/mnote/harness-tasks.json`
|
||||
|
||||
## 1. 这版为什么要重写
|
||||
+5
-3
@@ -1,7 +1,9 @@
|
||||
# 1-2 [process] Tree-First Graph 产品愿景进展评估 v1
|
||||
# 1-2 [reference] Tree-First Graph 产品愿景进展评估 v1
|
||||
|
||||
> 更新时间:2026-05-19
|
||||
>
|
||||
> 当前状态:`reference`。本文保留产品愿景进展评估,不作为当前 active implementation checklist。
|
||||
>
|
||||
> **目的**:对照原始产品愿景(Wolai 编辑体验 + VSCode 文件树 + simple-mindmap + OnlyOffice + AI 操作一切),评估当前代码在 1.0 MVP 后的实际进展和剩余的缺口。
|
||||
>
|
||||
> 关联:
|
||||
@@ -30,7 +32,7 @@
|
||||
| 前端壳已切换到 Rust SSR(`:3000`)| DONE | `CURRENT_ARCHITECTURE.md` §3.1 |
|
||||
| Page Aggregate 写链(标题/正文/设置)smoke 通过 | DONE | `scripts/task110-*`, `task-page-aggregate-*-smoke.js` |
|
||||
| 页面设置运行时语义 / 页头回流 / AI 设置面未完全闭环 | PROCESS | `design/05-editor-mainline/reference/5-5-page-aggregate-single-truth-alignment-v1.md` |
|
||||
| Wolai UI 行为持续对齐 | PROCESS | `design/05-editor-mainline/process/5-9-wolai-aline-continuous-checklist-v1.md` |
|
||||
| Wolai UI 行为持续对齐 | PROCESS | `design/05-editor-mainline/reference/5-9-wolai-aline-continuous-checklist-v1.md` |
|
||||
| `documents.content` 仍是兼容后备而非唯一真源 | PARTIAL | `CURRENT_ARCHITECTURE.md` §3.2 |
|
||||
|
||||
#### 基础块类型覆盖(对照 Wolai 帮助中心)
|
||||
@@ -101,7 +103,7 @@
|
||||
|
||||
| 子能力 | 状态 | 关键证据 |
|
||||
|--------|------|----------|
|
||||
| Phase 6 口径修正(Rust kernel + simple-mind-map adapter)| DONE | `design/06-mindmap/process/6-mindmap-kernel-phase6-projection-editor-v1.md` §2 |
|
||||
| Phase 6 口径修正(Rust kernel + simple-mind-map adapter)| DONE | `design/06-mindmap/reference/6-mindmap-kernel-phase6-projection-editor-v1.md` §2 |
|
||||
| Leptos UI shell | DONE | git `c64487c6` |
|
||||
| 即时显示修复 | DONE | git `3be102a4` |
|
||||
| Filetree 切换稳定 | DONE | git `b300562a` |
|
||||
+5
-3
@@ -1,11 +1,13 @@
|
||||
# 1-4 [process] 下一阶段执行路线图 v1
|
||||
# 1-4 [reference] 下一阶段执行路线图 v1
|
||||
|
||||
> 创建时间:2026-05-19
|
||||
>
|
||||
> 当前状态:`PROCESS`
|
||||
> 当前状态:`reference`
|
||||
>
|
||||
> 归档说明(2026-05-22):本文保留下一阶段路线推导;当前可执行入口已收敛到 `design/01-tree-first-graph-kernel/process/1-8-mvp-post-process-execution-order-v1.md`,不再从本文直接派发实现。
|
||||
>
|
||||
> 上位依据:
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-3-current-priority-execution-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/done/1-3-current-priority-execution-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference/5-14-zed-lapce-vscode-reference-adoption-matrix-v1.md`
|
||||
> - `/mnt/Data1T/mnote/CURRENT_ARCHITECTURE.md`
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
+6
-4
@@ -1,12 +1,14 @@
|
||||
# 1-5 [process] 下一阶段顺序执行 checklist v1
|
||||
# 1-5 [reference] 下一阶段顺序执行 checklist v1
|
||||
|
||||
> 创建时间:2026-05-19
|
||||
>
|
||||
> 当前状态:`PROCESS`
|
||||
> 当前状态:`reference`
|
||||
>
|
||||
> 归档说明(2026-05-22):本文是历史顺序清单,部分条目已被后续 Batch / done 文档覆盖;当前只作为参考,不再作为 active process 队列。
|
||||
>
|
||||
> 上位依据:
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-4-next-phase-execution-roadmap-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-3-current-priority-execution-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-4-next-phase-execution-roadmap-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/done/1-3-current-priority-execution-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference/5-14-zed-lapce-vscode-reference-adoption-matrix-v1.md`
|
||||
>
|
||||
> 目标:给出一份可以顺序执行、每一步都有完成验收、且标明参考源码位置的下一阶段执行清单。
|
||||
+5
-3
@@ -1,13 +1,15 @@
|
||||
# 1 [process] Tree-First Graph 内核方案 v1
|
||||
# 1 [reference] Tree-First Graph 内核方案 v1
|
||||
|
||||
> 更新时间:2026-04-22
|
||||
>
|
||||
> 当前状态:`reference`。本文是 tree-first graph kernel 的长期架构边界,不是当前可直接执行的 checklist;执行入口见 `design/01-tree-first-graph-kernel/process/1-8-mvp-post-process-execution-order-v1.md`。
|
||||
>
|
||||
> 当前优先级入口:
|
||||
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/reference/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/reference/3-1-rust-web-long-term-checklist-v2.md`
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
|
||||
## 1. 文档目的
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
> - `/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/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/design/old/07-ai/process/7-14-local-first-ai-markdown-editing-convergence-v1.md`
|
||||
> - `/mnt/Data1T/mnote/bugs/03-rust-web/done/3-16-local-markdown-upload-uses-convex-media-asset-v1.md`
|
||||
|
||||
---
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@
|
||||
|
||||
> - 当前 shell parity checklist:`/mnt/Data1T/mnote/design/03-rust-web/done/3-8-rust-web-3000-wolai-ui-parity-checklist-v1.md`
|
||||
|
||||
> - Page Aggregate 对齐主线:`/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - Page Aggregate 对齐主线:`/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
|
||||
## 1. 当前结论
|
||||
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
>
|
||||
> 参考主线:
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-11-tree-rust-family-final-renderer-and-host-thinning-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/done/3-10-rust-web-3000-tree-editor-runtime-integration-checklist-v1.md`
|
||||
|
||||
## 1. 根因确认
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
> - 下文第 2 节保留的是实现前问题基线;当前真实完成状态与验证证据以第 15-16 节为准
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/reference/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-13-rust-web-local-markdown-gfm-ast-parser-migration-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
|
||||
## 1. 结论
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
> 关联:
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/reference/3-1-rust-web-long-term-checklist-v2.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-3-page-ai-hermes-panel-and-mnote-plugin-v1.md`
|
||||
>
|
||||
> 2026-05-13 口径修正:
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
> - `3104` 已按 `/mnt/Data1T/mnote/design/03-rust-web/done/3-4-mnote-web-3104-boundary-retirement-plan-v1.md` 退役为默认/公开边界
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-1-tree-first-graph-kernel-checklist-v2.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-1-tree-first-graph-kernel-checklist-v2.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/reference/3-1-rust-web-long-term-checklist-v2.md`
|
||||
|
||||
## 0. 本轮判断
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
> 关联文档:
|
||||
> - `/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/old/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md`(历史过渡背景)
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/reference/3-rust-web-long-term-architecture-v1.md`
|
||||
|
||||
## 1. 目标
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/reference/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
|
||||
|
||||
@@ -514,7 +514,7 @@ Expected:
|
||||
**Files:**
|
||||
|
||||
- Modify: `ARCHITECTURE.md`
|
||||
- Modify: `design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md`
|
||||
- Modify: `design/03-rust-web/reference/3-1-rust-web-long-term-checklist-v2.md`
|
||||
- Modify: `design/03-rust-web/process/3-6-rust-web-main-execution-plane-checklist-v1.md`
|
||||
- Move when complete: `design/03-rust-web/process/3-5-rust-web-main-execution-plane-cutover-plan-v1.md`
|
||||
- Move when complete: `design/03-rust-web/process/3-6-rust-web-main-execution-plane-checklist-v1.md`
|
||||
|
||||
+5
-3
@@ -1,13 +1,15 @@
|
||||
# 3-1 [process] Rust Web 长期架构实施清单 v2
|
||||
# 3-1 [reference] Rust Web 长期架构实施清单 v2
|
||||
|
||||
> 更新时间:2026-04-29
|
||||
>
|
||||
> 当前状态:`reference`。本文保留 Rust Web 分阶段全景,不再作为 active implementation checklist。
|
||||
>
|
||||
> 基于以下实际状态重写:
|
||||
> - 当前未提交代码
|
||||
> - `/mnt/Data1T/mnote/harness-tasks.json` 中 `task-059` ~ `task-069`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/reference/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/03-rust-web/process/rust-web-long-term-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-tree-first-graph-kernel-v1.md`
|
||||
|
||||
## 1. 这版为什么要重写
|
||||
|
||||
+4
-2
@@ -1,7 +1,9 @@
|
||||
# 3 [process] Rust Web 长期架构方案 v1
|
||||
# 3 [reference] Rust Web 长期架构方案 v1
|
||||
|
||||
> 更新时间:2026-05-09
|
||||
>
|
||||
> 当前状态:`reference`。本文保留 Rust Web 长期架构边界;当前实现队列以 `design/01-tree-first-graph-kernel/process/1-8-mvp-post-process-execution-order-v1.md` 和对应 `done/` 证据为准。
|
||||
>
|
||||
> 相关文档:
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/08-legacy-rust-kernel/done/rust-kernel-cutover-v1.md`
|
||||
@@ -352,7 +354,7 @@ Leptos Islands 很适合承接下面这类长期目标:
|
||||
- Rust Web 已能承接主 API、页面壳和主流式链路
|
||||
- `BlockNote` 已降级为最后的重交互孤岛
|
||||
|
||||
对应的执行细节、逐阶段清单和可验证项,当前应先以 `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md` 确认优先级;`/mnt/Data1T/mnote/design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md` 保留为 Rust Web 分阶段全景参考。
|
||||
对应的执行细节、逐阶段清单和可验证项,当前应先以 `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md` 确认优先级;`/mnt/Data1T/mnote/design/03-rust-web/reference/3-1-rust-web-long-term-checklist-v2.md` 保留为 Rust Web 分阶段全景参考。
|
||||
|
||||
### 8.4 当前已落地的最小里程碑(2026-05-09)
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/04-tree-domain/process/4-1-sidebar-pagetree-filetree-product-gap-analysis-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-tree-first-graph-kernel-v1.md`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
> 更新时间:2026-04-16
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-1-tree-first-graph-kernel-checklist-v2.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-1-tree-first-graph-kernel-checklist-v2.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/process/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
>
|
||||
> 关联:
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/process/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-tree-first-graph-kernel-v1.md`
|
||||
|
||||
## 1. 目的
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/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/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-tree-first-graph-kernel-v1.md`
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-1-tree-first-graph-kernel-checklist-v2.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-1-tree-first-graph-kernel-checklist-v2.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/done/3-2-tree-first-graph-kernel-phase3-task-breakdown-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/90-reference/90-2-yemianshu.md`
|
||||
> - `/mnt/Data1T/mnote/design/90-reference/90-1-filetree.md`
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
这份方案必须完全服从:
|
||||
|
||||
- [tree-first-graph-kernel-v1.md](/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md)
|
||||
- [tree-first-graph-kernel-v1.md](/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-tree-first-graph-kernel-v1.md)
|
||||
|
||||
里面已经固定的几条原则。
|
||||
|
||||
|
||||
+2
-2
@@ -11,9 +11,9 @@
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-7-wolai-page-tree-main-editor-experience-restoration-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-9-wolai-aline-continuous-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference/5-9-wolai-aline-continuous-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-3-page-ai-hermes-panel-and-mnote-plugin-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/08-wolai-aline-test-flow/process/wolai-aline-test-flow-v1.md`
|
||||
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
> - `/mnt/Data1T/mnote/bugs/05-editor-mainline/done/5-10-mindmap-filetree-index-single-truth-split-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-24-resource-tree-filetree-pagetree-source-contract-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/06-mindmap/process/6-mindmap-kernel-phase6-projection-editor-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/06-mindmap/reference/6-mindmap-kernel-phase6-projection-editor-v1.md`
|
||||
|
||||
## 1. 目标
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/done/2-1-page-block-storage-projection-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-9-page-block-ai-tooling-roadmap-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||||
>
|
||||
> 代码依据:
|
||||
> - `/mnt/Data1T/mnote/rust/crates/core-protocol/src/editor/model.rs`
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
> - `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/5-1-main-editor-cutover-entry-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-2-tiptap-notion-like-template-adoption-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/5-3-tiptap-leptos-rust-migration-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/reference/3-rust-web-long-term-architecture-v1.md`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
|
||||
+35
-28
@@ -11,6 +11,13 @@
|
||||
> - 写侧必须退出 `/api/documents/save` 长期主路径,收口到带 `expectedFileVersion` 的 `page.body.write` / LocalFS executor。
|
||||
> - AI 直改文件、tiptap autosave、外部编辑器修改必须共用 VSCode-like 文件版本冲突模型。
|
||||
>
|
||||
> 2026-05-22 复核补充:
|
||||
> - local_folder 文档页、Hermes `mnote.page.save`、历史 Hermes `mnote.doc.markdown_edit` 的本地写入链已回到 `write_local_markdown_page_body` / `page.body.write`,并携带 `expectedFileVersion` 或聚合快照中的 file version。2026-05-22 之后,`mnote.doc.markdown_edit` 不再作为 local-first 默认、fallback 或 remote fallback;当前 AI 编辑口径以 `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md` 为准。
|
||||
> - BufferStore 已进入保存、watcher、Hermes 写入与冲突信封;`task451` 已覆盖冲突信封中的 `externalActor / dirtyState / bufferFileVersion`,`task484` 已覆盖保存后刷新读回。
|
||||
> - cloud / compat `/api/documents/save` 边界已固定为非 local-first 正文主写链;local-first 默认写侧走 `/api/page-body/write` / LocalFS executor。
|
||||
> - 2026-05-22 继续补证:`acp_runtime_env_limits_*`、`hermes_client_run_body_local_source_uses_file_scope_not_full_page_context`、`ensure_write_authorized_*`、`local_path_read_access_rejects_root_escape`、`local_file_open_rejects_root_escape` 均通过;ResourceTab 跨文档 owner 错配已由 `sidebar_filetree_asset_open_uses_owner_document_id` 修复并覆盖;旧 cloud `task443` / `task445` browser smoke 已迁移为 local-first fixture 并复跑通过。
|
||||
> - 2026-05-22 收尾补证:`task453-local-folder-page-ai-changed-files-smoke.js` 已扩展为 AI 写入前台同步 smoke;clean path 验证 Hermes `run.completed.agentAudit.changedFiles` 后 changed-files 工具卡、磁盘 `.md`、Page Aggregate、ProseMirror 和 clean editor 非冲突态;dirty path 验证同一路径触发 `external-change-conflict`、冲突面板显示 agent run、冲突信封包含 `externalActor / dirtyState / bufferFileVersion`,diff 同时显示本地未保存 token 与 AI 写盘 token。
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-4-leptos-tiptap-mainline-correction-v1.md`
|
||||
@@ -47,12 +54,12 @@
|
||||
|
||||
### 2.2 还没有成立的事实
|
||||
|
||||
- [ ] 页面树 / 文件树 / 页面头部 / 页面设置 / 主编辑区还没有消费同一份 page aggregate projection。
|
||||
- [x] 页面树 / 文件树 / 页面头部 / 页面设置 / 主编辑区已经在主路径上消费同一份 page aggregate / preferred sidebar projection;后续新增字段仍可能需要补 loader -> state -> consumer 链,但不再作为 P1 阻塞。
|
||||
- [x] 标题 / 正文 / 页面设置已经开始统一到同一组 page aggregate command family。
|
||||
- [ ] 写侧还没有完全退出 `/api/documents/save` 兼容面;`page.body.write` / LocalFS executor 与文件版本冲突模型仍待落地。
|
||||
- [ ] tiptap dirty 状态与 AI / 外部文件变更之间还没有完整 VSCode-like conflict UI。
|
||||
- [ ] `pageOptions` 还没有整体收口到 `leptos-tiptap` island 的正式运行时语义层。
|
||||
- [ ] AI 写入口还没有完整对齐“授权文件引用 + 白名单目录 + 后台文件写入 + 前台同步”模型。
|
||||
- [x] local-first 写侧已退出 `/api/documents/save` 长期主路径;文档页保存使用 `/api/page-body/write`,Hermes 本地正文写入回到 `write_local_markdown_page_body`,并复用文件版本冲突模型。
|
||||
- [x] tiptap dirty 状态与 AI / 外部文件变更已具备 VSCode-like conflict UI 与 BufferStore 信封;`task451` 覆盖 accept-disk / keep-current / merge-save / agent-conflict-source。
|
||||
- [x] `pageOptions` 已整体收口到 `leptos-tiptap` island 的正式运行时语义层;`protectEditing / showBlockRefCount` 仍是 planned / 降级展示项,不再冒充正式完成。
|
||||
- [x] AI 写入口已经补齐前台同步回归证明:local-first 下 allowedRoots / path escape / commandContext 有 targeted tests;`task453` 覆盖 AI 写入后 changed files、Page Aggregate、ProseMirror、clean 非冲突态和 dirty conflict UI。
|
||||
|
||||
补充:本轮已新增前端统一 `page-command-client`,并把 `DocumentContent` 的标题 / 页面设置写入、AI 正文写回、以及 `BlockNote` / `leptos-tiptap` 各 host 的正文保存统一到 `page.head.updateTitle / page.layout.updateOptions / page.body.save`。同时,Next route 侧已新增统一 `page-write-command-adapter`,`/api/documents/title`、`/api/documents/options`、`/api/documents/save` 三条页面写链路已开始共用同一层执行器。这里勾选的是“命令执行面开始收口”,不等于页面域真相链已经完全统一。
|
||||
|
||||
@@ -137,7 +144,7 @@
|
||||
### 5.3 退出标准
|
||||
|
||||
- [x] 页面加载时不再能明显看出“这是几份子结果拼出来的页面”。
|
||||
- [ ] 后续页面新增字段时,不再需要继续向外层 props 链同时塞多种局部真相。
|
||||
- [x] 后续页面新增字段时不再需要继续向外层 props 链同时塞多种局部真相;现有模式是补 Page Aggregate -> client state -> island/consumer 链。新增字段仍要改 consumer,但不再扩第二份页面事实源。
|
||||
|
||||
补充:当前首屏 SSR 与客户端内容重试补拉都已经直接走 `/api/page-aggregate/:id -> PageAggregateProjection`,因此“页面明显由 `meta + content` 两次查询拼起来”的入口级痕迹已经消失;但新增字段仍可能需要继续补 loader / route / island 消费链,所以第二项继续保留未完成。
|
||||
|
||||
@@ -156,7 +163,7 @@
|
||||
### 6.1 必须先分类
|
||||
|
||||
- [x] 把当前页面设置分成:页面壳布局类、阅读视图类、编辑器 runtime 语义类。
|
||||
- [ ] 明确哪些项应继续保留在页面设置面板中,哪些项应降级或隐藏,哪些项必须进入 island。
|
||||
- [x] 明确哪些项应继续保留在页面设置面板中,哪些项应降级或隐藏,哪些项必须进入 island。
|
||||
|
||||
补充:本轮已新增 `page-option-semantics.ts`,把 `pageOptions` 的运行时语义正式收口为代码契约,不再让这套规则继续散落在 `page-options-sidebar.tsx`、`editor-host-types.ts` 与 `leptos-tiptap-island-editor-host.tsx` 里各写一份。当前至少已经明确:
|
||||
|
||||
@@ -272,7 +279,7 @@
|
||||
|
||||
- [x] 人类编辑与 AI 编辑共享同一块语义边界。
|
||||
- [x] AI 改写结果能通过主编辑区 island 正式回显。
|
||||
- [ ] AI 改写后树标题 / 页面头部 / 页面设置不再走各自独立副作用链。
|
||||
- [x] AI 改写后树标题 / 页面头部 / 页面设置已回到同一组 page aggregate command family;剩余验证重点转为前台同步 smoke 的 local-first fixture 化。
|
||||
|
||||
补充:进入这一步时,最大的真实 blocker 已经明确下来;以下为历史 blocker 记录,不再作为 2026-05-14 之后的当前状态描述:
|
||||
|
||||
@@ -291,7 +298,7 @@
|
||||
### 8.3 退出标准
|
||||
|
||||
- [x] AI 写入口已经可以被明确描述为“必须经过授权 scope 与页面写入仲裁”,而不是“绕过系统写编辑器”。
|
||||
- [ ] local-first 下,普通正文 AI 写入必须进一步落到“授权文件 patch 或带 `expectedFileVersion` 的 `page.body.write`”,不能继续以 `/api/documents/save` 兼容面作为长期验收终点。
|
||||
- [x] local-first 下,普通正文 AI 写入已落到“授权文件 patch 或带 `expectedFileVersion` 的 `page.body.write`”;`mnote.page.save` 与历史 `mnote.doc.markdown_edit` 在 local_folder 分支均回到 `write_local_markdown_page_body`。当前 active 口径进一步收口为 `7-18`:普通 Markdown 编辑不再保留 MNote 专用编辑工具。
|
||||
|
||||
注:历史 `/api/ai-agent/run` 在 `Hermes tool.completed` 后,会优先尝试把 `slash_run / doc_insert_blocks / doc_replace_range` 恢复成 `mnote-web bridge-runtime` 的结构化 `tool_result`,不再只把 Hermes 事件当作薄日志。2026-05-14 起这只保留为过渡证据;新的主线已由 Hermes 发起 mnote plugin tool call,再由 Rust runtime / kernel 返回 tool result。其后:
|
||||
|
||||
@@ -316,10 +323,10 @@
|
||||
约束如下:
|
||||
|
||||
- [x] 没有完成 `Phase F` 之前,不再继续零散补标题 / 宽度 / 页面设置。
|
||||
- [ ] 没有完成 `Phase G` 之前,不把当前文档页描述成“已经统一聚合完成”。
|
||||
- [ ] 没有完成 `Phase H` 之前,不把页面设置大量打钩为“正式可用”。
|
||||
- [ ] 没有完成 `Phase I` 之前,不把标题问题视为已从底层解决。
|
||||
- [ ] 没有完成 `Phase J` 之前,不把 AI 直接写主编辑区描述成已经具备正式稳定入口。
|
||||
- [x] 没有完成 `Phase G` 之前,不把当前文档页描述成“已经统一聚合完成”。
|
||||
- [x] 没有完成 `Phase H` 之前,不把页面设置大量打钩为“正式可用”。
|
||||
- [x] 没有完成 `Phase I` 之前,不把标题问题视为已从底层解决。
|
||||
- [x] Phase J 的 local-first browser 回归已补齐;AI 写入主编辑区可按 local-first 主路径描述为稳定入口,仍需遵守 allowed roots、文件版本和冲突 UI 约束。
|
||||
|
||||
---
|
||||
|
||||
@@ -330,16 +337,16 @@
|
||||
> **“主编辑区与树域已经基本统一到 Rust 主导的 page aggregate 单一真源架构”。**
|
||||
|
||||
- [x] 文档页消费统一 `page aggregate projection`
|
||||
- [ ] 标题 / 正文 / 页面设置不再是三条分裂真相链
|
||||
- [x] 标题 / 正文 / 页面设置不再是三条分裂真相链
|
||||
- [x] `leptos-tiptap` island 真正消费 editor-related `page_layout`
|
||||
- [x] 树标题与页头标题来自同一份 projection
|
||||
- [x] AI 写入口开始围绕 page aggregate command family 实现
|
||||
|
||||
补充:当前未勾选“标题 / 正文 / 页面设置不再是三条分裂真相链”的原因,不再只是命令名或 route 分裂。虽然前端页面写入口、Next route 执行器、`DocumentContent` 内部的 `body/layout/tree` client state、以及 `pageOptions` 的代码级运行时分类都已经开始收口,但 projection 回流与 AI 对页面设置面的正式写入口仍未完全统一。也就是说,命令执行面、页面本地状态面、页面设置语义面都已开始统一,但页面域单一真源仍未闭环。
|
||||
补充:2026-05-22 复核后,标题 / 正文 / 页面设置三条链已经在 local-first 主路径上回到 Page Aggregate / page command family / client aggregate state;`documents.save` 只作为 cloud / compat 边界保留。AI 写入后的前台同步 browser 证据、dirty conflict UI 真实 Hermes changed-files 路径均已由 `task453` 补齐,因此本文件可从 `process/` 归档到 `done/`。
|
||||
|
||||
在此之前,正确口径都应保持为:
|
||||
归档后的正确口径为:
|
||||
|
||||
> **`leptos-tiptap` 主编辑区已基本可用,但页面聚合仍未收口,当前仍处于从混合态向 Rust 单一真源过渡的过程中。**
|
||||
> **local-first 主编辑区、页面聚合、BufferStore 冲突模型与 AI 写入同步已经在 Rust 主导的 Page Aggregate 主路径上基本闭环;后续新增字段仍按 Page Aggregate -> client state -> island/consumer 链扩展,不再新增第二份页面事实源。**
|
||||
|
||||
---
|
||||
|
||||
@@ -353,25 +360,25 @@
|
||||
| G(主文档页消费统一聚合) | ✅ 基本完成 | 加载链路、散落 props、退出标准 1 已满足;exit 2(新增字段不扩容 props 链)为 P2 方向目标 |
|
||||
| H(pageOptions 进入 island) | ✅ 基本完成 | 优先级项已 wired,Inspector 口径统一,AI 写回白名单限制;`protectEditing/showBlockRefCount` 仍为 planned |
|
||||
| I(树域与页面域标题统一) | ✅ 全部完成 | task110 smoke 验证全部标题同步场景 |
|
||||
| J(AI 写入口对齐 Page Aggregate) | ⚠️ 命令面完成,权限面待验 | 命令名已收口;`allowedRoots` / 路径 escape 检查不在本审计范围内 |
|
||||
| J(AI 写入口对齐 Page Aggregate) | ✅ 完成 | local_folder 写入已回 `page.body.write`;`allowedRoots` / 路径 escape / commandContext targeted tests 已覆盖;`task453` 覆盖 AI clean 前台同步和 dirty conflict UI |
|
||||
|
||||
### 11.2 2.2 节未成立事实的状态更新
|
||||
|
||||
| 项 | 审计状态 | 说明 |
|
||||
|---|---------|------|
|
||||
| 页面树/文件树/页面头部/页面设置/主编辑区尚未消费同一份 projection | ⚠️ 部分解决 | 主编辑区和页头已统一;页面树/文件树的标题已同源,但页面设置面板与主编辑区的 `pageOptions` 运行时消费仍有 gap |
|
||||
| 写侧未完全退出 `/api/documents/save` | ⚠️ 部分解决 | `local_folder` 已切到 `/api/page-body/write`;Convex workspace 仍走 `/api/documents/save`(作为正式路径而非 compat) |
|
||||
| tiptap dirty + AI/外部变更的 VSCode-like conflict UI | ❌ 未完成 | DOM 侧已经插入 conflict 面板元素(`web_shell.rs:4990-5015`),但前端测试/Playwright smoke 未覆盖 |
|
||||
| `pageOptions` 收口到 island 运行时语义 | ⚠️ 部分完成 | `page-option-semantics.ts` 分类已落地;`protectEditing/showBlockRefCount` 为 planned |
|
||||
| AI 写入口对齐授权/白名单/后台写入/前台同步 | ⚠️ 命令面已收口,权限面待验 | 本审计未确认 Hermes 侧的 `allowedRoots` 和路径 escape 实现 |
|
||||
| 页面树/文件树/页面头部/页面设置/主编辑区尚未消费同一份 projection | ✅ 主路径完成 | 主编辑区、页头、标题投影、pageOptions runtime 语义已统一;新增字段仍需补 consumer 链但不扩第二事实源 |
|
||||
| 写侧未完全退出 `/api/documents/save` | ✅ local-first 完成 | `local_folder` 已切到 `/api/page-body/write`;`/api/documents/save` 仅保留 cloud / compat 边界 |
|
||||
| tiptap dirty + AI/外部变更的 VSCode-like conflict UI | ✅ 完成 | `task451` 覆盖冲突 UI 四路径,并断言 BufferStore 字段进入冲突信封 |
|
||||
| `pageOptions` 收口到 island 运行时语义 | ✅ 基本完成 | `page-option-semantics.ts` 分类、Inspector 口径、AI 写回白名单和可见效果 smoke 已覆盖;`protectEditing/showBlockRefCount` 为 planned / 降级展示 |
|
||||
| AI 写入口对齐授权/白名单/后台写入/前台同步 | ✅ 完成 | targeted tests 已覆盖 ACP env、file scope、CommandContext 写守卫和 LocalFS root escape;`task453` 汇总 AI 写入后 Page Aggregate / ProseMirror / changed files / clean 非冲突态 / dirty conflict UI |
|
||||
|
||||
### 11.3 退出标准状态
|
||||
|
||||
- Section 9:"当前推荐实施顺序" 中的约束:Phase G 和 H 的 [ ] 项可以部分追认——Phase G 和 H 的主体工作已完成,但 exit criterion 的严格表述("已经统一聚合完成" / "大量打钩为正式可用")仍不完全满足。建议保持现状不勾选,等 J 完成后统一收尾。
|
||||
- Section 10 总退出标准:"标题/正文/页面设置不再是三条分裂真相链" 仍 [ ]——命令执行面已统一,但 projection 回流和 AI 页面设置写入口未完全统一,符合当前描述。
|
||||
- Section 9:Phase G/H/I/J 约束均已可追认完成。
|
||||
- Section 10:标题 / 正文 / 页面设置三条分裂真相链已在 local-first 主路径收口;AI 前台同步浏览器证据已补齐。
|
||||
|
||||
### 11.4 建议
|
||||
|
||||
1. **Phase G exit 2 降级为 P2**:新增字段仍需改 loader→state→consumer 链是当前架构的自然限制,不应作为 P1 block。
|
||||
2. **补充 Conflict UI Playwright smoke**:DOM 元素已经存在,只差自动化验收流程。
|
||||
3. **明确 `/api/documents/save` 的正式定位**:如果它是 Convex workspace 的正式路径,则在文档中调整口径;如果它是要移除的 compat,则排期删除。
|
||||
1. **归档到 `done/`**:本清单不再占用 active `process/`。
|
||||
2. **下一执行包转向**:当前新稿为 `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md`,直接围绕 allowed roots、file refs、agent 原生 patch/diff、watcher、BufferStore、Page Aggregate 前台同步和审计链设计;`design/old/07-ai/process/7-14-local-first-ai-markdown-editing-convergence-v1.md` 仅作为反例 / 历史材料。
|
||||
3. **归档前复跑文档检查**:`git diff --check -- design rust/crates/mnote-web/src/ssr/pages/layout.rs rust/crates/mnote-web/src/routes/web_shell.rs scripts/task443-filetree-mindmap-click-active-row-smoke.js scripts/task445-filetree-mindmap-switch-no-flicker-smoke.js scripts/task453-local-folder-page-ai-changed-files-smoke.js`,并同步 CodeGraph。
|
||||
+2
-2
@@ -7,8 +7,8 @@
|
||||
> 关联背景:
|
||||
> - `/mnt/Data1T/mnote/CURRENT_ARCHITECTURE.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/07-ai/process/7-14-local-first-ai-markdown-editing-convergence-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-24-resource-tree-filetree-pagetree-source-contract-checklist-v1.md`
|
||||
>
|
||||
> 本地参考源码:
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
> - `/mnt/Data1T/mnote/design/old/05-editor-mainline/process/5-3-tiptap-leptos-rust-migration-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-4-tree-projection-protocol-contract-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
+5
-5
@@ -9,18 +9,18 @@
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-tree-first-graph-kernel-v1.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/old/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-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/reference/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-2-sidebar-pagetree-filetree-product-interaction-contract-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference/5-2-tiptap-notion-like-template-adoption-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-4-leptos-tiptap-mainline-correction-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-9-wolai-aline-continuous-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference/5-9-wolai-aline-continuous-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
|
||||
|
||||
## 1. 文档目的
|
||||
@@ -852,7 +852,7 @@ P2 做复杂但可逐步推进的部分:
|
||||
- 若截图肉眼可见差异但 smoke 未捕获,先增强 smoke 或 checklist 断言,再继续实现。
|
||||
- 后续可加入截图 diff,但不能用截图 diff 替代真实交互烟测。
|
||||
- 每次改 Sidebar、topbar、document canvas、editor runtime 后都要重新截首屏,并同步更新连续 checklist 的状态。
|
||||
- 连续执行清单见 `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-9-wolai-aline-continuous-checklist-v1.md`。
|
||||
- 连续执行清单见 `/mnt/Data1T/mnote/design/05-editor-mainline/reference/5-9-wolai-aline-continuous-checklist-v1.md`。
|
||||
|
||||
owner 态交互验收清单:
|
||||
|
||||
|
||||
+3
-1
@@ -1,7 +1,9 @@
|
||||
# 5-9 [process] Wolai-aline 连续执行 checklist v1
|
||||
# 5-9 [reference] Wolai-aline 连续执行 checklist v1
|
||||
|
||||
> 更新时间:2026-05-18
|
||||
>
|
||||
> 当前状态:`reference`。本文保留 Wolai-aline 连续项基线;新对标任务必须按 `wolai-aline` skill 和 `design/08-wolai-aline-test-flow/reference/wolai-aline-test-flow-v1.md` 另拆具体 checklist。
|
||||
>
|
||||
> 2026-05-18 口径补充:
|
||||
> - 本 checklist 的 Wolai 体验对标仍有效,但数据真相口径跟随 local-first workspace:本地 `.md` 是默认正文真相,Convex-backed 路径只作为兼容 / cloud source。
|
||||
> - 历史条目中“写回 Convex-backed 持久化底座”的表述只代表当时 smoke 的在线路径,不再作为新增编辑能力默认目标。
|
||||
+5
-5
@@ -36,7 +36,7 @@ Rust kernel truth
|
||||
|
||||
### 当前主线文档
|
||||
|
||||
- `design/06-mindmap/process/6-mindmap-kernel-phase6-projection-editor-v1.md`
|
||||
- `design/06-mindmap/reference/6-mindmap-kernel-phase6-projection-editor-v1.md`
|
||||
- `design/06-mindmap/reference/6-mindmap-leptos-adapter-reference-notes-v1.md`
|
||||
- `docs/superpowers/plans/2026-05-10-mindmap-phase6-projection-editor-checklist.md`
|
||||
|
||||
@@ -110,9 +110,9 @@ Rust kernel truth
|
||||
### Task 1: 冻结新阶段边界
|
||||
|
||||
**Files:**
|
||||
- Modify: `design/06-mindmap/process/6-mindmap-kernel-phase6-projection-editor-v1.md`
|
||||
- Modify: `design/06-mindmap/reference/6-mindmap-kernel-phase6-projection-editor-v1.md`
|
||||
- Modify: `docs/superpowers/plans/2026-05-10-mindmap-phase6-projection-editor-checklist.md`
|
||||
- Create/Modify: `design/06-mindmap/process/6-mindmap-phase6-leptos-ui-shell-reuse-checklist-v1.md`
|
||||
- Create/Modify: `design/06-mindmap/done/6-mindmap-phase6-leptos-ui-shell-reuse-checklist-v1.md`
|
||||
|
||||
- [x] 明确当前 Phase 6 不是“Rust 重写 lx-doc/mind-map”,也不是“直接嵌入 lx-doc Vue app”。
|
||||
- 2026-05-11 记录:本文件“当前判断”和总设计稿第 3 节均保留三路线定位,Phase 6 主线为 `leptos-mindmap`。
|
||||
@@ -449,9 +449,9 @@ Rust kernel truth
|
||||
### Task 14: 文档与执行记录
|
||||
|
||||
**Files:**
|
||||
- Modify: `design/06-mindmap/process/6-mindmap-kernel-phase6-projection-editor-v1.md`
|
||||
- Modify: `design/06-mindmap/reference/6-mindmap-kernel-phase6-projection-editor-v1.md`
|
||||
- Modify: `design/06-mindmap/reference/6-mindmap-leptos-adapter-reference-notes-v1.md`
|
||||
- Modify: `design/06-mindmap/process/6-mindmap-phase6-leptos-ui-shell-reuse-checklist-v1.md`
|
||||
- Modify: `design/06-mindmap/done/6-mindmap-phase6-leptos-ui-shell-reuse-checklist-v1.md`
|
||||
- Modify: `docs/superpowers/plans/2026-05-10-mindmap-phase6-projection-editor-checklist.md`
|
||||
|
||||
- [x] 在总设计稿中明确:UI shell reuse 是 `leptos-mindmap` 的 Phase 6 后续收口,不改变 kernel-first 主合同。
|
||||
+6
-4
@@ -1,7 +1,9 @@
|
||||
# 6 [process] Mindmap Kernel Phase 6:leptos-mindmap Projection Editor v1
|
||||
# 6 [reference] Mindmap Kernel Phase 6:leptos-mindmap Projection Editor v1
|
||||
|
||||
> 更新时间:2026-05-10
|
||||
>
|
||||
> 当前状态:`reference`。本文保留 mindmap Phase 6 总设计;已完成 UI shell reuse 归档到 `design/06-mindmap/done/`,后续新工作需另拆小 checklist。
|
||||
>
|
||||
> 当前口径:`leptos-mindmap`
|
||||
>
|
||||
> 旧稿归档:
|
||||
@@ -9,8 +11,8 @@
|
||||
>
|
||||
> 当前主线依据:
|
||||
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/reference/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
>
|
||||
> 参考实现:
|
||||
@@ -394,7 +396,7 @@ Phase 6.1 的硬性完成判定:
|
||||
|
||||
Phase 6 后续 UI 可用度收口转入:
|
||||
|
||||
- `/mnt/Data1T/mnote/design/06-mindmap/process/6-mindmap-phase6-leptos-ui-shell-reuse-checklist-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/06-mindmap/done/6-mindmap-phase6-leptos-ui-shell-reuse-checklist-v1.md`
|
||||
|
||||
本轮已经把默认宿主修正为 floating overlay canvas shell,并把 toolbar、sidebar、count、navigator 迁到 Leptos/Rust shell 边界:
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
> 上位依据:
|
||||
> - `/mnt/Data1T/mnote/design/old/07-ai/process/7-phase7-ai-kernel-projection-plan-v2.md`
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
>
|
||||
> 状态说明:
|
||||
> - 本稿对应 `Phase 7 v2` 的“文档页 AI 最小闭环”已完成,故迁入 `done/`
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
> 本稿目的:在 7-12 已排除第二套 AI runtime 的前提下,补上 Hermes tool execution → Convex 持久化之间缺失的 Rust 编辑运行时中继层,实现「内存态 apply → 编辑器就地 patch → Convex 异步持久化 → 事件增量通知」的四步闭环。
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/reference/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/07-ai/process/7-14-local-first-ai-markdown-editing-convergence-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-9-page-block-ai-tooling-roadmap-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-13-page-block-identity-and-command-contract-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
@@ -395,7 +395,7 @@ mnote-web 重启
|
||||
| 5-13 块身份合同 | EditorBlockDocument 就是 blockDocument 的内存态 |
|
||||
| 4-6 tree command cutover | EditorRuntimeActor 不碰 tree 命令;page 级和 block 级命令保持独立 |
|
||||
| 3-3 tree realtime event stream | Phase C 新增 `block.delta` event,扩展而非替代 resync_required |
|
||||
| 7-14 markdown 编辑收敛 | EditorRuntimeActor 后续需适配 markdown_edit:内部做 markdown diff 后复用 BlockDelta 通道推送编辑器更新。当前先走完整 markdown → blocks → apply 路径 |
|
||||
| 7-14 markdown 编辑收敛 | 历史口径。`7-14` 已移入 old;当前 `7-18` 口径下,普通 Markdown 编辑不再要求 EditorRuntimeActor 适配 `markdown_edit`,而是由 agent 原生文件 patch/diff 后经 watcher / BufferStore / Page Aggregate 回流 |
|
||||
|
||||
---
|
||||
|
||||
@@ -407,8 +407,8 @@ mnote-web 重启
|
||||
- 不要求编辑器同步等待 Convex 写入完成才展示 AI 编辑结果。
|
||||
- 不改变已有的 `ensure_write_contract` 校验链。
|
||||
- 不新增写工具;Phase A/B/C 只加速已有工具的落地速度。
|
||||
- **按 7-14**:`mnote.doc.markdown_edit` 的新增不违反本禁止项——它是新增工具,但复用 EditorRuntimeActor 的 delta 通道,属于 Phase D 适配范围。
|
||||
- delta channel 不改写 Tiptap 的协作/undo/redo 栈;仅新增 AI 编辑的增量入口。markdown_edit 的 delta 推送同样遵守此约束。
|
||||
- **按 7-18**:local-first 普通 Markdown 编辑不再新增或依赖 `mnote.doc.markdown_edit`,EditorRuntimeActor 不承担普通 Markdown 工具写入适配。
|
||||
- delta channel 不改写 Tiptap 的协作/undo/redo 栈;若后续只用于结构性辅助,也必须遵守此约束。
|
||||
|
||||
---
|
||||
|
||||
|
||||
+2
-2
@@ -17,8 +17,8 @@
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/recycle/design/07-ai/retired-http-hermes/7-5-hermes-client-proxy-contract-v1.md`(已退役历史背景)
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/07-ai/process/7-14-local-first-ai-markdown-editing-convergence-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/reference/7-17-acp-session-convex-sharing-contract-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/hermes-vscode-main/`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/DeepSeek-Reasonix-main/`
|
||||
@@ -7,8 +7,8 @@
|
||||
> 归档说明(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`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/reference/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/10-review/process/08-kernel-architecture-next-priority-review-and-checklist.md`
|
||||
|
||||
---
|
||||
@@ -19,8 +19,8 @@
|
||||
|
||||
本文只承接这些剩余 smoke,不新增 AI 功能面,不改变当前主路径:
|
||||
|
||||
- local-first 普通 Markdown 编辑主路径仍是“授权文件引用 + agent 原生 patch/diff + watcher 同步”;本文只验证 mnote tools 的结构性辅助和 compat fallback。
|
||||
- `mnote.doc.markdown_edit` 只作为 cloud / remote agent / compat fallback 的 smoke 对象。
|
||||
- local-first 普通 Markdown 编辑主路径仍是“授权文件引用 + agent 原生 patch/diff + watcher 同步”;本文只验证 mnote tools 的历史结构性辅助和兼容证据。
|
||||
- 2026-05-22 之后,`mnote.doc.markdown_edit` 不再作为 local-first 默认、fallback 或 remote fallback;本文中的 smoke 只代表历史在线 / compat 回归证据。
|
||||
- `mnote.block.*` 仍只作为结构性辅助。
|
||||
- `mnote.page.save` 仍只作为页面级粗粒度兜底。
|
||||
- Phase C review / streaming apply 仍冻结,当前只验证基础合同和安全边界。
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
>
|
||||
> 上位依据:
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/07-ai/process/7-phase7-ai-kernel-projection-plan-v4.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-1-phase7-document-ai-minimum-loop-checklist-v1.md`
|
||||
>
|
||||
|
||||
@@ -4,16 +4,20 @@
|
||||
>
|
||||
> 2026-05-19 local-first 口径补充:
|
||||
> - 本文仍适用于 `convex_workspace` / 在线文档的 `mnote.doc.markdown_edit` 修复,但当前默认产品形态已切到 local-first workspace。
|
||||
> - 本地 `.md` 路径的默认 AI 编辑主路径是“授权文件引用 + agent 原生 patch/diff + watcher 同步”;`mnote.doc.markdown_edit` 只作为本地受控代理 fallback、cloud / remote agent 或 compat 路径。
|
||||
> - 本地 `.md` 路径的默认 AI 编辑主路径是“授权文件引用 + agent 原生 patch/diff + watcher 同步”。
|
||||
> - 后续新增 AI 编辑能力默认先保证本地 `.md` 与 `{mdBase}.assets/` 相对路径不被改写为 Convex media asset;在线 Convex 文档路径只作为可选 cloud / sync / share source。
|
||||
>
|
||||
> 2026-05-22 口径修正:`mnote.doc.markdown_edit` 不再作为 local-first 默认、fallback 或 remote fallback。本文只保留在线 / compat 写回修复的历史证据;当前 active 设计以 `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md` 为准。
|
||||
>
|
||||
> 当前状态:`DONE`(已归档,代码验证通过 2026-05-21)
|
||||
>
|
||||
> 关联缺陷:`bugs/07-ai/done/7-24-markdown-edit-online-write-does-not-use-final-markdown-v1.md`
|
||||
> `bugs/07-ai/done/7-25-reasonix-block-edit-workflow-empty-block-ops-after-markdown-match-v1.md`
|
||||
> `bugs/07-ai/done/7-17-markdown-edit-same-block-multi-op-overwrite-v1.md`
|
||||
>
|
||||
> 上位设计:`design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md`
|
||||
> 历史上位设计:`design/old/07-ai/process/7-14-local-first-ai-markdown-editing-convergence-v1.md`
|
||||
>
|
||||
> 当前 active 设计:`design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md`
|
||||
>
|
||||
> 参考实现:`design/05-editor-mainline/reference-code/cli-main/shortcuts/doc/`(str_replace skill)
|
||||
> `rust/crates/mnote-web/src/routes/local_markdown_parser.rs`(已有 GFM→blocks 解析器)
|
||||
|
||||
@@ -82,8 +82,8 @@ Owner:
|
||||
|
||||
只读参考:
|
||||
|
||||
- `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`
|
||||
- `design/07-ai/done/7-15-page-ai-acp-agent-runtime-unified-layer-v1.md`
|
||||
- `design/07-ai/reference/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
|
||||
- `rust/crates/mnote-web/src/acp_client.rs`
|
||||
- `rust/crates/mnote-web/src/acp_session_manager.rs`
|
||||
- `rust/crates/mnote-web/src/acp_runtime.rs`
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
> 上位依据:
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/reference/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-10-wolai-page-settings-and-ai-surface-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/hermes-web-ui-0.5.18`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-2-phase7-structured-artifact-write-chain-v1.md`
|
||||
|
||||
@@ -94,7 +94,7 @@ Owner:
|
||||
|
||||
只读范围:
|
||||
|
||||
- `design/07-ai/process/7-15-page-ai-acp-agent-runtime-unified-layer-v1.md`
|
||||
- `design/07-ai/done/7-15-page-ai-acp-agent-runtime-unified-layer-v1.md`
|
||||
- `design/07-ai/done/7-25-acp-session-runtime-enhancement-plan-v1.md`
|
||||
- `scripts/`
|
||||
- `rust/crates/mnote-web/src/acp_*.rs`
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
> 上位依据:
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/reference/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-15-runtime-fallback-retirement-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-10-wolai-page-settings-and-ai-surface-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-11-wolai-page-settings-and-ai-surface-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/hermes-web-ui-0.5.18`
|
||||
@@ -564,7 +564,7 @@ MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task-hermes-page-ai-tool-sm
|
||||
|
||||
- `rust/crates/mnote-web/src/hermes_tools/page.rs`
|
||||
- `rust/crates/bridge-runtime/src/lib.rs`
|
||||
- `design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md` 仅回填完成证据
|
||||
- `design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md` 仅回填完成证据
|
||||
- smoke:
|
||||
- `scripts/task-hermes-page-ai-title-options-smoke.js`
|
||||
|
||||
@@ -789,10 +789,10 @@ MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task-hermes-page-ai-tool-sm
|
||||
|
||||
- `design/07-ai/done/7-3-page-ai-hermes-panel-and-mnote-plugin-v1.md`
|
||||
- `design/07-ai/done/7-4-page-ai-hermes-panel-execution-checklist-v1.md`
|
||||
- `design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
|
||||
- `design/03-rust-web/reference/3-rust-web-long-term-architecture-v1.md`
|
||||
- `design/03-rust-web/process/3-15-runtime-fallback-retirement-checklist-v1.md`
|
||||
- `design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
- `design/05-editor-mainline/process/5-9-wolai-aline-continuous-checklist-v1.md`
|
||||
- `design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
- `design/05-editor-mainline/reference/5-9-wolai-aline-continuous-checklist-v1.md`
|
||||
- `design/old/07-ai/process/*`
|
||||
|
||||
执行步骤:
|
||||
@@ -1010,7 +1010,7 @@ MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task-hermes-page-ai-tool-sm
|
||||
2026-05-14 K1 执行证据:
|
||||
|
||||
- [x] rg 命中已归类:`design/old/**` 与 `design/07-ai/done/7-1**` 为历史证据;`scripts/task-hermes-page-ai-retirement-guard.js` 与 `rust/crates/mnote-web/src/routes/compat.rs` 为 legacy guard;旧 E27/phase7/page-ai smoke 已默认退役;`wolai-frontend` 中 Mindmap / OnlyOffice / generic AI 面板调用点归为非当前页面 AI 主链的 legacy domain 调用点,后续按各自 domain 迁移。
|
||||
- [x] 修改文件:`rust/crates/mnote-web/src/routes/compat.rs`、`scripts/task-hermes-page-ai-retirement-guard.js`、历史退役 smoke 当前本机归档于 gitignored `recycle/scripts/retired-ai-agent-run-smokes/`、`design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`、`design/05-editor-mainline/process/5-9-wolai-aline-continuous-checklist-v1.md`、`design/10-review/04-secondary-domains-and-design-governance-review.md`。
|
||||
- [x] 修改文件:`rust/crates/mnote-web/src/routes/compat.rs`、`scripts/task-hermes-page-ai-retirement-guard.js`、历史退役 smoke 当前本机归档于 gitignored `recycle/scripts/retired-ai-agent-run-smokes/`、`design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`、`design/05-editor-mainline/reference/5-9-wolai-aline-continuous-checklist-v1.md`、`design/10-review/04-secondary-domains-and-design-governance-review.md`。
|
||||
- [x] 旧 smoke 默认退役验证:上述 6 个历史 smoke 均返回 `{ ok: true, retired: true }`。
|
||||
|
||||
**验收标准:**
|
||||
@@ -1094,8 +1094,8 @@ MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task-hermes-page-ai-tool-sm
|
||||
|
||||
| 参考目的 | 具体文件 | 具体位置 / 搜索点 | 处理方式 |
|
||||
| --- | --- | --- | --- |
|
||||
| Page Aggregate 口径 | `design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md` | 搜索 `/api/ai-agent/run`、`mnote-cli host`、`页面 AI` | 改成历史残留 / 已覆盖 / 待迁移证据,不写成长期主线 |
|
||||
| Wolai-aline 过渡基线 | `design/05-editor-mainline/process/5-9-wolai-aline-continuous-checklist-v1.md` | 搜索 `E27`、`mnote-cli`、`页面 AI` | 保留为对标基线时必须标明过渡状态 |
|
||||
| Page Aggregate 口径 | `design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md` | 搜索 `/api/ai-agent/run`、`mnote-cli host`、`页面 AI` | 改成历史残留 / 已覆盖 / 待迁移证据,不写成长期主线 |
|
||||
| Wolai-aline 过渡基线 | `design/05-editor-mainline/reference/5-9-wolai-aline-continuous-checklist-v1.md` | 搜索 `E27`、`mnote-cli`、`页面 AI` | 保留为对标基线时必须标明过渡状态 |
|
||||
| 二级域 review 建议 | `design/10-review/04-secondary-domains-and-design-governance-review.md` | 搜索 `Hermes`、`plugin`、`mnote-cli`、`CLI-first` | 指向 Hermes plugin / Rust bridge 的当前口径 |
|
||||
| 07-ai 历史稿 | `design/old/07-ai/process/` | 搜索 `CLI-first`、`mnote-cli` | 继续标 `[recycle]`,不迁回活跃 process |
|
||||
|
||||
@@ -1111,7 +1111,7 @@ MNOTE_UI_BASE_URL=http://127.0.0.1:3000 node scripts/task-hermes-page-ai-tool-sm
|
||||
2026-05-14 N1 执行证据:
|
||||
|
||||
- [x] `rg -n "mnote-cli.*唯一|唯一长期 agent|CLI-first|/api/ai-agent/run|provider=hermes" design --glob '*.md'` 已跑完,活跃命中已逐项归类。
|
||||
- [x] 活跃命中主要落在 `design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`、`design/05-editor-mainline/process/5-9-wolai-aline-continuous-checklist-v1.md`、`design/10-review/04-secondary-domains-and-design-governance-review.md`、`design/10-review/06-execution-checklist-and-acceptance.md`、`design/01-tree-first-graph-kernel/process/1-1-tree-first-graph-kernel-checklist-v2.md`、`design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md`、`design/07-ai/done/7-3-page-ai-hermes-panel-and-mnote-plugin-v1.md`、`design/07-ai/done/7-4-page-ai-hermes-panel-execution-checklist-v1.md`、`design/07-ai/done/7-5-hermes-client-proxy-contract-v1.md`、`design/07-ai/done/7-6-mnote-hermes-plugin-tool-contract-v1.md`。
|
||||
- [x] 活跃命中主要落在 `design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`、`design/05-editor-mainline/reference/5-9-wolai-aline-continuous-checklist-v1.md`、`design/10-review/04-secondary-domains-and-design-governance-review.md`、`design/10-review/06-execution-checklist-and-acceptance.md`、`design/01-tree-first-graph-kernel/reference/1-1-tree-first-graph-kernel-checklist-v2.md`、`design/03-rust-web/reference/3-1-rust-web-long-term-checklist-v2.md`、`design/07-ai/done/7-3-page-ai-hermes-panel-and-mnote-plugin-v1.md`、`design/07-ai/done/7-4-page-ai-hermes-panel-execution-checklist-v1.md`、`design/07-ai/done/7-5-hermes-client-proxy-contract-v1.md`、`design/07-ai/done/7-6-mnote-hermes-plugin-tool-contract-v1.md`。
|
||||
- [x] `design/old/07-ai/process/7-phase7-ai-kernel-projection-plan-v4.md` 与同目录旧稿继续保留 `[recycle]`,只作为历史证据。
|
||||
- [x] 本轮对 `5-6`、`5-9`、`10-review/04`、`10-review/06`、`1-1`、`3-1` 的旧口径改写已完成;活跃文档现已统一为 Hermes 页面内客户端 + mnote Hermes plugin/tool 口径。
|
||||
- [x] 本文件仍保留在 `process/`,原因是它是执行清单而不是代码实现文件;若后续需要归档,可按 `N1.5` 的规则移动并同步更新引用。
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
>
|
||||
> Hermes Web UI 参考:`packages/client/src/api/hermes/plugins.ts`、`packages/client/src/api/hermes/skills.ts`、`packages/server/src/services/hermes/plugins.ts`、`packages/server/src/services/hermes/chat-run-socket.ts`
|
||||
>
|
||||
> 2026-05-16 口径补充:本文定义的是页面 AI 最小可用工具合同。`mnote.page.save` 只作为页面级兜底写入工具,不再代表长期精确块编辑方案;页面/块级 AI 工具体系路线图已归档到 `design/07-ai/done/7-9-page-block-ai-tooling-roadmap-v1.md`,执行验收继续以 `design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md` 为准。
|
||||
> 2026-05-16 口径补充:本文定义的是页面 AI 最小可用工具合同。`mnote.page.save` 只作为页面级兜底写入工具,不再代表长期精确块编辑方案;页面/块级 AI 工具体系路线图已归档到 `design/07-ai/done/7-9-page-block-ai-tooling-roadmap-v1.md`。
|
||||
>
|
||||
> 2026-05-22 归档治理补充:旧 `7-10` 已移入 `design/old/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`,不再作为当前执行验收入口;`7-14` 也已移入 `design/old/07-ai/process/7-14-local-first-ai-markdown-editing-convergence-v1.md`。当前 AI 编辑 active 口径以 `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md` 为准,MNote 不再提供普通 Markdown 编辑工具。
|
||||
|
||||
## 1. 总边界
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
> 上位依据:
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/reference/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/hermes-web-ui-0.5.18`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-3-page-ai-hermes-panel-and-mnote-plugin-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-4-page-ai-hermes-panel-execution-checklist-v1.md`
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-13-page-block-identity-and-command-contract-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/done/2-1-page-block-storage-projection-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-6-mnote-hermes-plugin-tool-contract-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-8-page-ai-hermes-runtime-bff-next-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||||
|
||||
---
|
||||
|
||||
@@ -964,7 +964,7 @@ Rust 内部结构化文档模型,是 PageXML/PageMarkdown 到 Page Aggregate /
|
||||
|
||||
本文作为页面/块 AI 工具体系路线图与合同已经归档为 `DONE`;执行验收不再由本文继续承接,而是转入:
|
||||
|
||||
- `/mnt/Data1T/mnote/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||||
- `/mnt/Data1T/mnote/design/old/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||||
|
||||
当前已成立的 done 边界:
|
||||
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
# 7-18 [process] local-first agent 文件编辑控制面 v1
|
||||
|
||||
> 创建时间:2026-05-22
|
||||
>
|
||||
> 当前状态:`PROCESS`
|
||||
>
|
||||
> 上位依据:
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-8-mvp-post-process-execution-order-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-15-page-ai-acp-agent-runtime-unified-layer-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/reference/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
|
||||
>
|
||||
> 覆盖旧稿:`/mnt/Data1T/mnote/design/old/07-ai/process/7-14-local-first-ai-markdown-editing-convergence-v1.md`
|
||||
>
|
||||
> 口径说明:旧 `7-14` 仍受 CLI Main / Lark Doc / tool-first 写入模型影响。CLI Main 可以参考 agent workflow 纪律,但不作为 MNote local-first Markdown 编辑架构主参考。
|
||||
|
||||
## 1. 第一结论
|
||||
|
||||
MNote 不提供普通 Markdown 正文编辑工具。
|
||||
|
||||
MNote 在 local-first 下只提供:
|
||||
|
||||
- 页面和文件定位。
|
||||
- agent target 选择与冻结。
|
||||
- `AiAccessScope`、allowed roots、allowed files、readonly / dirty / permission context。
|
||||
- selection、当前光标附近上下文、相关资源引用。
|
||||
- agent runtime session、审计、changed files / diff 回收。
|
||||
- watcher、BufferStore、Page Aggregate、ProseMirror 前台同步。
|
||||
|
||||
Hermes / Reasonix 是已知 agent,使用自身文件 read / write / patch / diff 能力在授权目录内完成普通 `.md` 编辑。
|
||||
|
||||
## 2. 明确退役项
|
||||
|
||||
### 2.1 `mnote.doc.markdown_edit`
|
||||
|
||||
`mnote.doc.markdown_edit` 不再作为:
|
||||
|
||||
- local-first 普通 Markdown 编辑默认入口。
|
||||
- local-first 普通 Markdown 编辑 fallback。
|
||||
- remote fallback。
|
||||
- 新 agent runtime 的推荐工具。
|
||||
- 新 checklist 的验收目标。
|
||||
|
||||
已有代码和 smoke 若仍覆盖 `markdown_edit`,只代表历史在线 / compat 路径的回归证据;不得再由它反推出“local-first 应继续提供 Markdown 编辑工具”。
|
||||
|
||||
### 2.2 `mnote.doc.fetch`
|
||||
|
||||
`mnote.doc.fetch` 不作为普通正文读取主入口。
|
||||
|
||||
local-first 默认路径中,agent 直接读取授权文件。MNote 可以把 selection、文件摘要、block projection 或结构化上下文放进 run input,但不要求 agent 通过 `mnote.doc.fetch` 再读正文。
|
||||
|
||||
### 2.3 `page_ai_workflow` / `mnote.block.*` / `mnote.page.save`
|
||||
|
||||
- `page_ai_workflow` 只保留为 debug / 历史兼容门面,不进入 local-first 普通编辑主路径。
|
||||
- `mnote.block.*` 只保留为复杂结构辅助,例如拖拽排序、精确块删除、非 Markdown 结构资源辅助。
|
||||
- `mnote.page.save` 只保留为页面级兜底写入工具,不作为精确 Markdown 编辑入口。
|
||||
|
||||
## 3. 主路径数据流
|
||||
|
||||
```text
|
||||
Page / active editor
|
||||
-> Agent Target Resolver freezes selected target package
|
||||
-> resolve canonical .md file / resource file
|
||||
-> build AiAccessScope
|
||||
-> freeze allowed roots / allowed files / readonly / dirty / selection
|
||||
-> start Hermes / Reasonix run with file refs and context
|
||||
-> agent native patch / diff / write inside allowed roots
|
||||
-> MNote collects changed files / diff / audit
|
||||
-> watcher observes file changes
|
||||
-> BufferStore arbitrates clean / dirty / conflict state
|
||||
-> Page Aggregate rebuilds projection
|
||||
-> ProseMirror / tiptap island refreshes visible document
|
||||
```
|
||||
|
||||
关键点:
|
||||
|
||||
- MNote 只给位置、权限和上下文,不给普通 Markdown 编辑工具。
|
||||
- 多工作区、多 tab、多资源同时打开时,默认 target 只能来自最后获得编辑焦点的 editor / resource tab;不允许用“第一个 tab”“树选中项”或 URL documentId 静默猜测。
|
||||
- 写入动作发生在 agent 自身文件能力内,并受 allowed roots / allowed files 限制。
|
||||
- 前台同步由 watcher / BufferStore / Page Aggregate 接回,不由 agent 直接推 UI 状态。
|
||||
- dirty buffer 时不得静默覆盖,必须进入冲突或 review 状态。
|
||||
|
||||
## 4. Agent Target Resolver
|
||||
|
||||
Agent Target Resolver 是 MNote 发送给 agent 前的唯一目标解析层。它的职责是把当前 UI 状态冻结为一份明确的 `targetPackage`,而不是让 agent 或 API 自行猜“当前页”。
|
||||
|
||||
### 4.1 选择原则
|
||||
|
||||
- **workspace 是权限边界**:默认一次 run 只属于一个 workspace;跨 workspace 必须由用户显式多选,并生成多个 `allowedRoots`。
|
||||
- **tab 是视图,不是真相**:tab 只帮助判断用户正在看的对象;最终 target 必须落到 `objectIdentity`、`resourceKind` 和 canonical path。
|
||||
- **last focused editor 优先**:默认 target 是最后获得编辑焦点的 editor / resource tab。
|
||||
- **selection 覆盖 whole file**:存在真实选区时,默认 scope 为 selection;没有选区时才是 whole file / current resource。
|
||||
- **树选中项只作为候选**:FileTree / PageTree selection 不自动成为 agent target,除非用户在 target picker 中选择“树选中项”。
|
||||
- **歧义时不发送**:如果焦点不明确、target dirty 且会被写入、或跨 workspace 未显式确认,发送按钮进入需要选择状态。
|
||||
|
||||
### 4.2 targetPackage 合同
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "mnote.agent_target_package.v1",
|
||||
"primaryTargetId": "target-1",
|
||||
"targets": [
|
||||
{
|
||||
"targetId": "target-1",
|
||||
"workspaceId": "local:design",
|
||||
"workspaceSource": "local_folder",
|
||||
"workspaceRoot": "/workspace/design",
|
||||
"editorGroupId": "main",
|
||||
"tabId": "tab-design-md",
|
||||
"focusRank": 1,
|
||||
"objectIdentity": "local_folder:/workspace/design/design.md",
|
||||
"resourceKind": "markdown_page",
|
||||
"title": "design.md",
|
||||
"canonicalPath": "/workspace/design/design.md",
|
||||
"uri": "file:///workspace/design/design.md",
|
||||
"version": "mtime:size:hash",
|
||||
"scope": "selection",
|
||||
"selection": {
|
||||
"text": "当前选区文本",
|
||||
"anchor": "pm:block:p_1:0..12"
|
||||
},
|
||||
"dirty": false,
|
||||
"readonly": false,
|
||||
"ownerPage": null,
|
||||
"relatedResources": []
|
||||
}
|
||||
],
|
||||
"allowedRoots": ["/workspace/design"],
|
||||
"allowedFiles": ["/workspace/design/design.md"],
|
||||
"policy": {
|
||||
"writeMode": "agent_native_file_patch",
|
||||
"requiresConfirmation": false,
|
||||
"crossWorkspace": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 resourceKind
|
||||
|
||||
`resourceKind` 至少需要区分:
|
||||
|
||||
- `markdown_page`:普通 `.md` 页面正文。
|
||||
- `mindmap`:mindmap 资源文件;可带 `ownerPage`,但不伪装成正文。
|
||||
- `office`:OnlyOffice 资源;默认只读上下文,除非明确进入 office 编辑 agent flow。
|
||||
- `raw_file`:普通文件。
|
||||
- `folder`:目录范围;只在用户显式选择“把这个文件夹作为上下文”时出现。
|
||||
|
||||
## 5. UI 设计
|
||||
|
||||
当前右侧页面 AI 抽屉已有 runtime / profile / context scope,但“上下文:当前页/选区/块/页面设置”不够表达多工作区、多 tab 和资源 tab。需要把它升级为明确的 target UI。
|
||||
|
||||
### 5.1 默认展示
|
||||
|
||||
AI composer 上方固定显示 target chip,不放到设置页深处:
|
||||
|
||||
```text
|
||||
[工作区: design] [design.md] [当前选区] [可写]
|
||||
```
|
||||
|
||||
如果当前焦点是截图中的右侧 mindmap,则显示:
|
||||
|
||||
```text
|
||||
[工作区: local] [新页面165037 / KMIND] [mindmap] [可写]
|
||||
```
|
||||
|
||||
chip 必须可点击,打开 target picker。chip 内容优先显示用户能识别的名称,不优先显示内部 id;hover / detail 再显示 path、workspace root、object identity。
|
||||
|
||||
### 5.2 target picker
|
||||
|
||||
点击 target chip 后打开轻量 popover,不跳转页面。结构为:
|
||||
|
||||
```text
|
||||
发送给 Agent
|
||||
|
||||
当前焦点
|
||||
● design.md markdown_page 当前选区 /workspace/design/design.md
|
||||
|
||||
打开的 Tab
|
||||
○ 新页面223322 markdown_page 全文
|
||||
○ design.md markdown_page 当前选区
|
||||
○ 新页面165037 / KMIND mindmap 当前资源
|
||||
|
||||
树选中项
|
||||
○ 3 个文件 只读上下文
|
||||
|
||||
范围
|
||||
( ) 当前选区 ( ) 当前文件 ( ) 当前资源 ( ) 多选 tab ( ) 文件夹
|
||||
|
||||
[取消] [使用这个目标]
|
||||
```
|
||||
|
||||
交互规则:
|
||||
|
||||
- 单选是默认;多选必须用户点击“多选 tab”或勾选多个候选。
|
||||
- 多 workspace 候选默认折叠在各自 workspace 组下,跨 workspace 多选时出现权限提示。
|
||||
- dirty target 显示 `未保存` 标记;如果 agent 可能写入,发送前需要确认。
|
||||
- readonly target 显示 `只读`,发送按钮文案变成“只读提问”或禁用写入型 intent。
|
||||
- resource target 不能显示成“当前页”;必须显示 `mindmap` / `office` / `raw file`。
|
||||
|
||||
### 5.3 composer 中的 context scope
|
||||
|
||||
原 `context scope` select 不应继续承担 target 选择职责。它只控制 primary target 内部范围:
|
||||
|
||||
- `selection`:当前选区。
|
||||
- `file`:当前文件。
|
||||
- `resource`:当前资源文件。
|
||||
- `related`:当前页面引用的相关资源,只读附加上下文。
|
||||
|
||||
不再把 `page / block / options` 混成同一层 target 选择。页面设置属于 target 的辅助上下文,不是普通 Markdown 编辑范围。
|
||||
|
||||
### 5.4 发送前状态
|
||||
|
||||
发送按钮按 targetPackage 状态变化:
|
||||
|
||||
- `发送`:单 target、clean、可写或只读问答。
|
||||
- `选择目标`:没有明确 last focused target,或当前焦点和 URL / active tab 冲突。
|
||||
- `确认发送`:跨 workspace、多 target、dirty write、folder scope。
|
||||
- `只读提问`:readonly target 或用户选择只读上下文。
|
||||
|
||||
发送后第一条用户消息旁边显示冻结的 target 摘要,避免 run 过程中用户切 tab 后误以为 agent 改了新 tab:
|
||||
|
||||
```text
|
||||
目标:design.md / 当前选区 / local_folder / 12:31:05 冻结
|
||||
```
|
||||
|
||||
## 6. SQLite control-plane 职责
|
||||
|
||||
Rust SQLite control-plane 是默认控制面,负责:
|
||||
|
||||
- auth / session / membership。
|
||||
- share grants / sync state。
|
||||
- AI policy、workspace policy、allowed roots / allowed files。
|
||||
- ACP / Hermes / Reasonix runtime session。
|
||||
- targetPackage 审计与用户确认记录。
|
||||
- permission decision、approval decision、tool / file access audit。
|
||||
- changed files / diff / command context 记录。
|
||||
|
||||
它不持有普通 Markdown 正文真相。本地 `.md` 文件仍是正文真相。
|
||||
|
||||
## 7. 运行时输入合同
|
||||
|
||||
每次从页面触发 agent 编辑时,run input 至少应包含:
|
||||
|
||||
```json
|
||||
{
|
||||
"workspaceSource": "local_folder",
|
||||
"targetPackage": {
|
||||
"schema": "mnote.agent_target_package.v1",
|
||||
"primaryTargetId": "target-1",
|
||||
"targets": []
|
||||
},
|
||||
"currentFile": {
|
||||
"path": "/workspace/page.md",
|
||||
"uri": "file:///workspace/page.md",
|
||||
"version": "mtime:size:hash"
|
||||
},
|
||||
"allowedRoots": ["/workspace"],
|
||||
"allowedFiles": ["/workspace/page.md"],
|
||||
"selection": {
|
||||
"text": "当前选区文本",
|
||||
"anchor": "可选的 editor/block/offset 引用"
|
||||
},
|
||||
"context": {
|
||||
"readonly": false,
|
||||
"dirty": false,
|
||||
"resourceRefs": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
后续可以补充 page id、object identity、resource refs、outline、nearby context,但不得把普通正文写入重新包装成 `mnote.doc.markdown_edit`。
|
||||
|
||||
## 8. 验收清单
|
||||
|
||||
### Phase A0:target selection 与 UI
|
||||
|
||||
- [ ] Page AI composer 可见 target chip,显示 workspace、target title、resourceKind、scope、readonly / dirty 状态。
|
||||
- [ ] 点击 target chip 打开 target picker,可在当前焦点、打开 tabs、树选中项之间选择。
|
||||
- [ ] 默认 target 来自 last focused editor / resource tab,不来自第一个 tab、URL documentId 或树选中项。
|
||||
- [ ] mindmap / office / raw file target 显示真实 resourceKind,不伪装成 markdown page。
|
||||
- [ ] 多 tab / 跨 workspace / dirty write 时发送前需要显式确认。
|
||||
- [ ] 发送后消息记录冻结 target 摘要,切换 tab 不影响已启动 run。
|
||||
|
||||
### Phase A:运行时输入与权限边界
|
||||
|
||||
- [ ] 从文档页发起 Hermes / Reasonix run 时,request body 包含 `currentFile`、`allowedRoots`、`allowedFiles`、selection 和 dirty / readonly context。
|
||||
- [ ] run body 包含 `targetPackage`,且 `allowedRoots` / `allowedFiles` 从 targetPackage 推导。
|
||||
- [ ] local-first 普通 Markdown 编辑 run 不包含 `mnote.doc.markdown_edit` 推荐工具。
|
||||
- [ ] allowed roots 外文件写入被 agent runtime 或 MNote 审计层拒绝。
|
||||
- [ ] readonly 页面不会启动可写 run,或只启动只读问答 run。
|
||||
|
||||
### Phase B:写入回收与前台同步
|
||||
|
||||
- [ ] agent 修改当前 `.md` 后,MNote 能回收 changed files / diff。
|
||||
- [ ] clean buffer 下 watcher -> BufferStore -> Page Aggregate -> ProseMirror 可见更新通过 browser smoke。
|
||||
- [ ] dirty buffer 下外部 agent 写入不会静默覆盖,必须出现冲突或 review 状态。
|
||||
- [ ] changed files / diff 与 session / run / actor 关联写入审计。
|
||||
|
||||
### Phase C:旧工具退役
|
||||
|
||||
- [ ] local-first 普通 Markdown 编辑 smoke 断言未调用 `/api/documents/save`。
|
||||
- [ ] local-first 普通 Markdown 编辑 smoke 断言未调用 `mnote.doc.markdown_edit`。
|
||||
- [ ] Hermes / Reasonix manifest 或 system prompt 不再鼓励普通 Markdown 编辑调用 `mnote.doc.markdown_edit`。
|
||||
- [ ] 旧 `markdown_edit` 测试只保留为历史 online / compat 回归,并在文档中标明不指导新实现。
|
||||
|
||||
### Phase D:结构辅助边界
|
||||
|
||||
- [ ] `mnote.block.*` 只在复杂结构辅助场景出现,例如结构块排序、非 Markdown 资源辅助。
|
||||
- [ ] `mnote.page.save` 只作为页面级兜底写入,不作为默认精确编辑入口。
|
||||
- [ ] `mnote.doc.fetch` 不作为 local-first 正文读取主入口;需要结构化上下文时由 MNote 在 run input 中提供摘要或 projection。
|
||||
|
||||
## 9. 不做事项
|
||||
|
||||
- 不参考 CLI Main 的 Lark Doc 在线文档写入架构来设计 local-first 普通 Markdown 编辑。
|
||||
- 不新增 MNote 普通 Markdown 编辑工具。
|
||||
- 不把 Convex 恢复为默认正文或 AI 会话全文主存储。
|
||||
- 不实现 Phase C Review Mode / StreamApplyController / GhostTextOverlay。
|
||||
- 不让前端维护第二份正文真相。
|
||||
- 不把所有打开 tab 默认发送给 agent。
|
||||
- 不把跨 workspace 自动合并为一个隐式上下文。
|
||||
|
||||
## 10. 归档条件
|
||||
|
||||
满足以下条件后,本稿可归档到 `design/07-ai/done/`:
|
||||
|
||||
- Phase A0 target UI 有真实浏览器 smoke 证据。
|
||||
- Phase A / B / C 至少各有一条真实浏览器或 targeted test 证据。
|
||||
- local-first agent 文件编辑主路径已能在 clean buffer 下完成真实 `.md` 修改并同步回 tiptap。
|
||||
- dirty conflict 不静默覆盖。
|
||||
- 文档和 manifest 不再把 `mnote.doc.markdown_edit` 描述为 local-first fallback。
|
||||
+7
-5
@@ -1,14 +1,16 @@
|
||||
# 7-12 [process] 页面 AI Hermes 工具路由与编辑审阅面设计 v1
|
||||
# 7-12 [reference] 页面 AI Hermes 工具路由与编辑审阅面设计 v1
|
||||
|
||||
> 更新时间:2026-05-19
|
||||
>
|
||||
> 当前状态:`PROCESS`
|
||||
> 当前状态:`reference / frozen`
|
||||
>
|
||||
> 归档说明(2026-05-22):本文保留 Hermes 工具路由、manifest 和 review surface 的设计边界;Phase C Review Mode 继续冻结。`7-14` 已移入 `design/old/07-ai/process/7-14-local-first-ai-markdown-editing-convergence-v1.md`,当前 active AI 编辑口径以 `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md` 为准。
|
||||
>
|
||||
> 本稿目的:修正“页面 AI 快速块编辑”后续方向,明确 mnote 不再建设独立 AI agent runtime;mnote 只建设 Hermes 可消费的编辑工具路由、工具 manifest、上下文冻结、dry-run/review 和 Rust 写入安全边界。
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/10-review/done/09-page-ai-fast-block-edit-runtime-review.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-9-page-block-ai-tooling-roadmap-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-6-mnote-hermes-plugin-tool-contract-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/cli-main`
|
||||
@@ -22,7 +24,7 @@
|
||||
本稿仍作为 Hermes 工具路由与审阅面设计保留在 `process/`,但以下口径已经更新:
|
||||
|
||||
- `/api/page-ai/block-edit-workflow` 当前不再以 `local_rule -> apply_block_ops` 作为主路径;local-first 普通 Markdown 编辑默认给 Hermes / Reasonix 授权文件引用,由 agent 使用自身 patch / diff / 文件编辑能力写本地 `.md`。
|
||||
- `mnote.doc.markdown_edit` 保留为 cloud / remote agent / compat fallback;模型生成 markdown `search/replace` 或 `full_content` 后调用该工具,只适用于 agent 不能直接访问授权文件或需要受控代理写入的场景。
|
||||
- `mnote.doc.markdown_edit` 不再作为 local-first 默认、fallback 或 remote fallback;本文后续出现的 `markdown_edit` 只代表历史 tool / review surface 证据,不指导新 agent 文件编辑主路径。当前 active 口径见 `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md`。
|
||||
- `page_ai_workflow` 已复用统一 mnote tool executor,不再绕过 Hermes tool toggle / audit / write contract。
|
||||
- `mnote.doc.apply_block_ops` / `mnote.block.*` 保留为结构性块操作辅助,不再作为普通正文 search/replace 的优先入口。
|
||||
- 本稿中的 `PageAIReviewSession` 只定义 Phase C 的安全合同和状态机边界;当前 Phase C 仍冻结,不实施流式 apply 或新的审阅 UI。
|
||||
@@ -653,7 +655,7 @@ profile disabled mnote.block.fetch
|
||||
`7-10` 是页面块 AI 工具执行 checklist,包含真实代码和 smoke 证据。它仍然有效,继续保留在:
|
||||
|
||||
```text
|
||||
design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md
|
||||
design/old/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md
|
||||
```
|
||||
|
||||
但后续执行必须按本文修正口径:
|
||||
@@ -15,9 +15,9 @@
|
||||
> 4. 为后续项目基本完成后扩展共享能力预留 schema、API 和验证边界。
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/process/7-15-page-ai-acp-agent-runtime-unified-layer-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-15-page-ai-acp-agent-runtime-unified-layer-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-25-acp-session-runtime-enhancement-plan-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/07-ai/process/7-14-local-first-ai-markdown-editing-convergence-v1.md`
|
||||
> - `/mnt/Data1T/mnote/recycle/design/07-ai/retired-http-hermes/`
|
||||
|
||||
---
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
> 上位依据:
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/reference/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/reference/3-rust-web-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/reference/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/06-mindmap/process/6-mindmap-kernel-phase6-projection-editor-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/06-mindmap/reference/6-mindmap-kernel-phase6-projection-editor-v1.md`
|
||||
>
|
||||
> 外部参考:
|
||||
> - `https://github.com/siyuan-note/siyuan`
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
|
||||
- `ARCHITECTURE.md`
|
||||
- `design/01-05-current-priority-overview.md`
|
||||
- `design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
- `design/01-tree-first-graph-kernel/process/1-1-tree-first-graph-kernel-checklist-v2.md`
|
||||
- `design/01-tree-first-graph-kernel/reference/1-tree-first-graph-kernel-v1.md`
|
||||
- `design/01-tree-first-graph-kernel/reference/1-1-tree-first-graph-kernel-checklist-v2.md`
|
||||
- `design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md`
|
||||
- `design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md`
|
||||
- `design/03-rust-web/process/3-1-rust-web-long-term-checklist-v2.md`
|
||||
- `design/03-rust-web/reference/3-rust-web-long-term-architecture-v1.md`
|
||||
- `design/03-rust-web/reference/3-1-rust-web-long-term-checklist-v2.md`
|
||||
- `design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
|
||||
- `design/03-rust-web/process/3-15-runtime-fallback-retirement-checklist-v1.md`
|
||||
- `design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md`
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
- `design/04-tree-domain/process/4-23-local-cloud-explicit-bridge-p3-candidate-v1.md`
|
||||
- `design/05-editor-mainline/done/5-4-leptos-tiptap-mainline-correction-v1.md`
|
||||
- `design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
- `design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
- `design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
- `design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
|
||||
|
||||
重点读取代码:
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
- `design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md`
|
||||
- `design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
|
||||
- `design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
- `design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
- `design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
- `design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
|
||||
- `recycle/20260522-convex-runtime-retirement/infra/convex/README.md`
|
||||
- `wolai-frontend/convex/`
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
### Mindmap
|
||||
|
||||
- 设计口径清晰:`design/06-mindmap/process/6-mindmap-kernel-phase6-projection-editor-v1.md` 明确 Phase 6 主线是 `Rust kernel truth -> mindmap.simple_mind_map_scene.v1 -> leptos-mindmap editor island -> simple-mind-map runtime -> command bridge`,并禁止把 runtime data 当唯一事实源。
|
||||
- 设计口径清晰:`design/06-mindmap/reference/6-mindmap-kernel-phase6-projection-editor-v1.md` 明确 Phase 6 主线是 `Rust kernel truth -> mindmap.simple_mind_map_scene.v1 -> leptos-mindmap editor island -> simple-mind-map runtime -> command bridge`,并禁止把 runtime data 当唯一事实源。
|
||||
- 实现已对齐主线壳:`rust/crates/mnote-web/src/routes/mindmap_shell.rs:105-123` 输出 `mnote.mindmap_shell.v1`、`mindmap.simple_mind_map_scene.get` 与 `mindmap.command.apply`;`rust/crates/mnote-web/src/ssr/pages/mindmap.rs:50-56` 提供 standalone island 挂载点。
|
||||
- 旧 React 块已自我标注为 legacy/compat/reference:`wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx:3-5` 明确 3000 文档页默认主链是 `leptos-tiptap NodeView + leptos-mindmap adapter`。
|
||||
- 仍有 compat 数据层:`wolai-frontend/convex/schema.ts:164-184` 的 `mindmaps.data: v.any()` 仍承载导图数据;这与当前过渡态兼容,但不应被描述为长期 canonical truth。
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
- `rust/crates/mnote-web/src/routes/web_shell.rs`
|
||||
- `wolai-frontend/src/lib/documents/page-aggregate-loader.ts`
|
||||
- `design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
- `design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
- `design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
|
||||
执行项:
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
- `rust/crates/bridge-runtime/src/lib.rs`:`page.aggregate.get` 根据 query data 是否包含 `meta + content` 选择 `CompatMetaContentJoin`,否则保留 `KernelProjection`。
|
||||
- `rust/crates/mnote-web/src/routes/web_shell.rs`:`/api/page-aggregate/:id` 测试断言 `x-mnote-page-aggregate-owner=compat-join` 且 `result.source=CompatMetaContentJoin`。
|
||||
- `design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`:补充 Rust route 主读链与完整 kernel-native projection 的边界。
|
||||
- `design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`:补充 `CompatMetaContentJoin` / `KernelProjection` 验收口径。
|
||||
- `design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`:补充 `CompatMetaContentJoin` / `KernelProjection` 验收口径。
|
||||
- `design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`:新增 `source` / provenance 契约说明。
|
||||
- 已通过:`cargo test -p bridge-runtime page_aggregate_get_query_executes_into_core_projection -- --nocapture`。
|
||||
- 已通过:`cargo test -p mnote-web page_aggregate_endpoint_returns_snapshot_contract -- --nocapture`。
|
||||
@@ -421,7 +421,7 @@
|
||||
- `design/03-rust-web/done/3-15-runtime-fallback-retirement-checklist-v1.md`:从 `process/` 迁入 `done/`,文件自身 checkbox 已全部完成,且与第 2 / 第 8 项 fallback、compat 退场证据一致。
|
||||
- `design/06-mindmap/done/6-mindmap-phase6-kmind-parity-detail-checklist-v1.md`:从 `process/` 迁入 `done/`,文件自身 checkbox 已全部完成,并已有 `task167-mindmap-kmind-parity-smoke.js` 等 Phase 6 KMind parity 证据。
|
||||
- 已验证:`rg -n "需要我给你|我可以|你要不要|是否需要|请告诉我|如果你愿意|要我|我来" design/90-reference` 无匹配。
|
||||
- 已完成只读状态审计:`design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`、`design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`、`design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`、`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` 仍有未完成项或长期主线尾项,继续保留在 `process/`;`design/07-ai/done/7-3-page-ai-hermes-panel-and-mnote-plugin-v1.md`、`design/07-ai/done/7-4-page-ai-hermes-panel-execution-checklist-v1.md`、`design/07-ai/done/7-5-hermes-client-proxy-contract-v1.md`、`design/07-ai/done/7-6-mnote-hermes-plugin-tool-contract-v1.md` 已迁入 `done/`,后续活跃 AI 体验面由 `design/07-ai/process/7-7-page-ai-mini-hermes-control-surface-v1.md` 承接;`design/old/**` 不纳入活跃 process/done 迁移判断。
|
||||
- 已完成只读状态审计:`design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`、`design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`、`design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`、`design/06-mindmap/reference/6-mindmap-kernel-phase6-projection-editor-v1.md`、`design/06-mindmap/done/6-mindmap-phase6-leptos-ui-shell-reuse-checklist-v1.md` 仍有未完成项或长期主线尾项,继续保留在 `process/`;`design/07-ai/done/7-3-page-ai-hermes-panel-and-mnote-plugin-v1.md`、`design/07-ai/done/7-4-page-ai-hermes-panel-execution-checklist-v1.md`、`design/07-ai/done/7-5-hermes-client-proxy-contract-v1.md`、`design/07-ai/done/7-6-mnote-hermes-plugin-tool-contract-v1.md` 已迁入 `done/`,后续活跃 AI 体验面由 `design/07-ai/process/7-7-page-ai-mini-hermes-control-surface-v1.md` 承接;`design/old/**` 不纳入活跃 process/done 迁移判断。
|
||||
|
||||
## 11. 全局 Done Gate
|
||||
|
||||
|
||||
+11
-11
@@ -9,10 +9,10 @@
|
||||
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/reference/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/07-ai/process/7-14-local-first-ai-markdown-editing-convergence-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
- [x] 修改页面设置后检查 Page Aggregate 与 island runtime page options 同步。
|
||||
- [x] 刷新页面后检查标题、正文、页面设置不回退到旧快照。
|
||||
- [x] 破坏或暂停 Convex query,检查响应是 degraded/error,不返回伪 fixture。
|
||||
- [x] 在 `design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md` 勾选已验证项,并补证据路径。
|
||||
- [x] 在 `design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md` 勾选已验证项,并补证据路径。
|
||||
|
||||
2026-05-16 静态审计证据(历史 Next 前端退役前快照):
|
||||
|
||||
@@ -234,8 +234,8 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web document_shell_errors_wi
|
||||
- [x] 给写工具补重复 idempotencyKey 的端到端用例。
|
||||
- [x] 检查 `mnote.page.save` 在 manifest / UI 中继续标为页面级兜底,不显示为精确块编辑主入口。
|
||||
- [x] 检查任何新增 AI surface 是否只是基础 review/context/tooling 验收,不是新功能扩展。
|
||||
- [x] 更新 `design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md` 的每个 phase 证据。
|
||||
- [x] 更新 `design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md` 的 Hermes 工具路由与审阅面 checklist。
|
||||
- [x] 更新 `design/old/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md` 的每个 phase 证据。
|
||||
- [x] 更新 `design/07-ai/reference/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md` 的 Hermes 工具路由与审阅面 checklist。
|
||||
|
||||
2026-05-18 `mnote.block.insert_after` 多块插入证据:
|
||||
|
||||
@@ -269,7 +269,7 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web document_shell_errors_wi
|
||||
|
||||
2026-05-18 Review Session 合同定义:
|
||||
|
||||
- 已在 `design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md` 定义 `mnote.page_ai_review_session.v1` 最小 schema。
|
||||
- 已在 `design/07-ai/reference/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md` 定义 `mnote.page_ai_review_session.v1` 最小 schema。
|
||||
- 已明确状态:`draft/planning/previewing/awaiting_user/accepted/rejected/applying/applied/failed/aborted/stale`。
|
||||
- 已明确动作约束:`accept` 必须重新读 Page Aggregate 并校验 revision / conflictDetectionKey / revisionRef,`reject/abort` 不产生写入,`retry` 必须生成新 proposal 或 dry-run preview,yolo 模式也应生成同构 audit 数据。
|
||||
- 边界:这里只完成合同定义,不实施 Phase C 流式 apply 或新的审阅 UI。
|
||||
@@ -502,7 +502,7 @@ node scripts/task449-tree-sse-reconnect-snapshot-recovery-smoke.js
|
||||
- 已进入 `3.2 Page Block AI Tooling` 的只读审计阶段,但尚未勾选 3.2 的任何新 checkbox。
|
||||
- 已确认现有代码中 `mnote.doc.fetch` 已支持 `scope=selection`、`format=page_xml/text/markdown/json`、`schema=mnote.page_ai_context.v1`、`allowedTargetBlockIds`、truncation/warnings/continuation 等基础字段;`mnote.block.fetch` 已支持 `format=page_xml/text`;manifest 已包含 `readonly/destructive/idempotent/requiresApproval/approvalMode/runtimeOwner/writeOwner/selectionEffect` 等 annotations。
|
||||
- 已确认 `scripts/task-page-block-ai-tools-smoke.js` 覆盖基础块工具闭环,但还没有专门覆盖 `scope=selection`、`page_xml/text`、manifest annotations、选区外写入阻断、review session / conflict / idempotency 的完整验收。
|
||||
- 用户目标中提到的 `design/07-ai/process/7-11-blocknote-tiptap-ai-reference-and-mnote-ai-tool-runtime-v1.md` 当前在 `design/07-ai/process/` 下不存在;实际可读历史参考位于 `design/old/07-ai/process/7-11-blocknote-tiptap-ai-reference-and-mnote-ai-tool-runtime-v1.md`,当前执行口径应继续以 `design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md` 为准。
|
||||
- 用户目标中提到的 `design/07-ai/process/7-11-blocknote-tiptap-ai-reference-and-mnote-ai-tool-runtime-v1.md` 当前在 `design/07-ai/process/` 下不存在;实际可读历史参考位于 `design/old/07-ai/process/7-11-blocknote-tiptap-ai-reference-and-mnote-ai-tool-runtime-v1.md`,当前执行口径应继续以 `design/07-ai/reference/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md` 为准。
|
||||
|
||||
恢复时的下一步建议:
|
||||
|
||||
@@ -552,7 +552,7 @@ MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:300
|
||||
|
||||
2026-05-18 `7-12` runtime 口径闭合证据:
|
||||
|
||||
- `design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md` 已明确 `7-11` 旧“自有 AI runtime”口径移入 `design/old/07-ai/process/`,当前执行口径以 `7-12` 为准。
|
||||
- `design/07-ai/reference/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md` 已明确 `7-11` 旧“自有 AI runtime”口径移入 `design/old/07-ai/process/`,当前执行口径以 `7-12` 为准。
|
||||
- `7-12` 已定义 `PageAIContextBuilder`、`MnoteAIToolManifestProvider`、`PageAICommandRouter`、`PageAIReviewSession` 的边界,并明确 Phase C review / streaming apply 只做设计冻结,当前不实施。
|
||||
- `7-12` Phase B 到 Phase F 保留 manifest / router / review session / event / smoke 的后续 checklist;其中 selection 外写入 blocked 已有真实 smoke 证据,其余未实施项继续保留未勾。
|
||||
|
||||
@@ -601,5 +601,5 @@ MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:300
|
||||
## 6. 给后续 /goal 的持续执行 Prompt
|
||||
|
||||
```text
|
||||
/goal objective: 持续核验 /mnt/Data1T/mnote/design/10-review/done/08-kernel-architecture-next-priority-review-and-checklist.md 的已归档结论,按 P0 -> P1 顺序推进 MNOTE 架构防回归、AI 基础底座和定向 bug hunt。每轮开始先读取 /home/lix/.codex/memories/PROFILE.md 与 ACTIVE.md,再读取 AGENTS.md、ARCHITECTURE.md、design/01-05-current-priority-overview.md、本 checklist、design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md、design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md、design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md。必须保护用户已有未提交改动,不回滚、不覆盖、不删除无关文件。优先使用多个 subagent 并行做只读审查和浏览器验证,主线程只整合证据和做小范围实现。执行顺序固定为:1) Page Aggregate 单一真源防回归;2) tree command cutover 防回归;3) tree realtime live cache 防回归;4) AI 基础工具与审阅底座,当前简单正文编辑主路径已是 `mnote.doc.markdown_edit` + `mnote.doc.fetch` + `page_ai_workflow.rs` 统一 tool executor,Phase C(流式 apply + suggest/review)仅设计冻结不实施;继续补 context/selection/page_xml/tool annotations,不扩新 AI 功能;5) 定向 bug hunt。每完成一个小项都要更新对应 design/bugs 文档,补真实验证命令或证据路径。不要优先扩新功能,不要大改架构,不要把 compat/debug/fallback 当主链,不要把 BlockNote/Tiptap AI runtime 作为 mnote runtime 依赖。验证至少包含 scoped git diff --check、相关 cargo test / smoke;如涉及 3000 页面,使用 mnote-tester 或浏览器自动化并保留截图/JSON 证据。
|
||||
/goal objective: 持续核验 /mnt/Data1T/mnote/design/10-review/done/08-kernel-architecture-next-priority-review-and-checklist.md 的已归档结论,按 P0 -> P1 顺序推进 MNOTE 架构防回归、AI 基础底座和定向 bug hunt。每轮开始先读取 /home/lix/.codex/memories/PROFILE.md 与 ACTIVE.md,再读取 AGENTS.md、ARCHITECTURE.md、design/01-05-current-priority-overview.md、本 checklist、design/old/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md、design/07-ai/reference/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md、design/old/07-ai/process/7-14-local-first-ai-markdown-editing-convergence-v1.md。必须保护用户已有未提交改动,不回滚、不覆盖、不删除无关文件。优先使用多个 subagent 并行做只读审查和浏览器验证,主线程只整合证据和做小范围实现。执行顺序固定为:1) Page Aggregate 单一真源防回归;2) tree command cutover 防回归;3) tree realtime live cache 防回归;4) AI 基础工具与审阅底座,当前简单正文编辑主路径已是 `mnote.doc.markdown_edit` + `mnote.doc.fetch` + `page_ai_workflow.rs` 统一 tool executor,Phase C(流式 apply + suggest/review)仅设计冻结不实施;继续补 context/selection/page_xml/tool annotations,不扩新 AI 功能;5) 定向 bug hunt。每完成一个小项都要更新对应 design/bugs 文档,补真实验证命令或证据路径。不要优先扩新功能,不要大改架构,不要把 compat/debug/fallback 当主链,不要把 BlockNote/Tiptap AI runtime 作为 mnote runtime 依赖。验证至少包含 scoped git diff --check、相关 cargo test / smoke;如涉及 3000 页面,使用 mnote-tester 或浏览器自动化并保留截图/JSON 证据。
|
||||
```
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
> 日期:2026-05-16
|
||||
>
|
||||
> 关联主线:
|
||||
> - `design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||||
> - `design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
|
||||
> - `design/old/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||||
> - `design/07-ai/reference/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
|
||||
> - `design/07-ai/done/7-9-page-block-ai-tooling-roadmap-v1.md`
|
||||
> - `design/05-editor-mainline/done/5-13-page-block-identity-and-command-contract-v1.md`
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ Codex 已复核 worker 输出,并额外核对 `3-15` 的真实浏览器 smoke
|
||||
|
||||
以下文档已迁入 `reference/`,不再作为当前直接执行 checklist:
|
||||
|
||||
- `design/03-rust-web/reference/3-17-convex-export-web-entry-v1.md`
|
||||
- `design/old/03-rust-web/process/3-17-convex-export-web-entry-v1.md`
|
||||
- `design/04-tree-domain/reference/4-23-local-cloud-explicit-bridge-p3-candidate-v1.md`
|
||||
- `design/05-editor-mainline/reference/5-2-tiptap-notion-like-template-adoption-v1.md`
|
||||
- `design/05-editor-mainline/reference/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
# [reference] mnote-dev-squad 协作协议 v1
|
||||
|
||||
## 目的
|
||||
|
||||
把 mnote 开发固定为一个轻量协作组:Hermes 主控,多个 Codex worker 在独立 worktree 实现,Reasonix flash 做真实浏览器 smoke,每个环节都用 Markdown 交接。
|
||||
|
||||
本协议不使用 Codex harness;不允许多个写入型 agent 同时改同一个 checkout。
|
||||
|
||||
## 角色
|
||||
|
||||
### Hermes 主控
|
||||
|
||||
- 读取 mnote 当前设计与 AGENTS.md。
|
||||
- 拆分任务 lane。
|
||||
- 创建 run 目录与 task/result/smoke/review Markdown。
|
||||
- 创建或指定 Codex worktree。
|
||||
- 启动/观察 Codex worker。
|
||||
- 启动 Reasonix smoke。
|
||||
- 阅读 RESULT.md、SMOKE.md、git diff 后做合并裁决。
|
||||
|
||||
### Codex worker
|
||||
|
||||
- 一个 worker 只负责一个 lane。
|
||||
- 一个 worker 只在自己的 worktree 中写入。
|
||||
- 默认不提交、不推送、不改允许范围外文件。
|
||||
- 必须输出 RESULT.md。
|
||||
|
||||
### Reasonix smoke tester
|
||||
|
||||
- 默认使用 Reasonix flash。
|
||||
- 负责真实浏览器 smoke、截图、console、final URL、可见文本证据。
|
||||
- 默认不改代码。
|
||||
- 必须输出 SMOKE.md。
|
||||
|
||||
### Reviewer / 仲裁
|
||||
|
||||
- 默认由 Hermes + 主控 Codex完成。
|
||||
- 有争议时升级到 gpt-5.5 或更强模型。
|
||||
|
||||
## 目录约定
|
||||
|
||||
每轮协作创建:
|
||||
|
||||
```text
|
||||
/mnt/Data1T/mnote/tmp/agent-runs/<run-id>/
|
||||
PLAN.md
|
||||
codex-<lane>/TASK.md
|
||||
codex-<lane>/RESULT.md
|
||||
smoke/SMOKE.md
|
||||
REVIEW.md
|
||||
```
|
||||
|
||||
模板位于:
|
||||
|
||||
```text
|
||||
/mnt/Data1T/mnote/.agent-templates/task.md
|
||||
/mnt/Data1T/mnote/.agent-templates/result.md
|
||||
/mnt/Data1T/mnote/.agent-templates/smoke.md
|
||||
/mnt/Data1T/mnote/.agent-templates/review.md
|
||||
```
|
||||
|
||||
## Worktree 约定
|
||||
|
||||
Codex worker 使用独立 worktree,例如:
|
||||
|
||||
```text
|
||||
/mnt/Data1T/mnote-wt-<lane>-<YYYYMMDD-HHMM>
|
||||
```
|
||||
|
||||
推荐 lane:
|
||||
|
||||
- `rust-kernel`
|
||||
- `mnote-web`
|
||||
- `tree-domain`
|
||||
- `editor-interaction`
|
||||
- `onlyoffice`
|
||||
- `ai-runtime`
|
||||
- `docs-review`
|
||||
- `qa-smoke`
|
||||
|
||||
## Ownership 规则
|
||||
|
||||
每个 TASK.md 必须写清:
|
||||
|
||||
- 允许修改范围
|
||||
- 禁止修改范围
|
||||
- 验收命令
|
||||
- 交付物路径
|
||||
|
||||
若两个 lane 需要改同一个核心文件,必须停下来由 Hermes 重新拆分或指定唯一 owner。
|
||||
|
||||
## 交付标准
|
||||
|
||||
Codex worker 完成不等于可合并。可合并必须满足:
|
||||
|
||||
1. RESULT.md 存在。
|
||||
2. 修改文件符合 ownership。
|
||||
3. 验证命令已执行或明确说明阻塞原因。
|
||||
4. Reasonix smoke 已完成,或明确说明该任务不需要浏览器 smoke。
|
||||
5. Hermes / reviewer 已读 diff。
|
||||
|
||||
## Smoke 标准
|
||||
|
||||
涉及 UI、文档页、编辑器、树、拖拽、选择、hover、click、快捷键等,SMOKE.md 必须包含:
|
||||
|
||||
- 测试 URL / 环境
|
||||
- 浏览器动作
|
||||
- final URL
|
||||
- screenshot 路径
|
||||
- console error 数量
|
||||
- PASS / FAIL
|
||||
- 问题与复现步骤
|
||||
|
||||
块编辑器验收中,hover / click / drag / select 是强制步骤;缺任一步只能算未完成验收。
|
||||
|
||||
## 升级规则
|
||||
|
||||
升级到 gpt-5.5 / 主控 Codex 深度复核的情况:
|
||||
|
||||
- Reasonix smoke 与 Codex 结论冲突。
|
||||
- smoke evidence 不足。
|
||||
- 失败原因涉及架构边界。
|
||||
- 涉及 AGENTS.md 中的长期方向判断。
|
||||
- 需要合并多个 worktree 的重叠改动。
|
||||
|
||||
## 回收规则
|
||||
|
||||
旧 worktree、失败 worktree、临时实验目录优先软删除到 recycle,不直接硬删。
|
||||
|
||||
推荐:
|
||||
|
||||
```text
|
||||
/mnt/Data1T/recycle/<reason>-<timestamp>/
|
||||
```
|
||||
|
||||
保留 MANIFEST 或 README,说明来源与恢复方法。
|
||||
+4
-2
@@ -1,4 +1,4 @@
|
||||
# 1-9 [process] Batch B P1 Identity / BufferStore / Page Aggregate Execution Checklist v1
|
||||
# 1-9 [recycle] Batch B P1 Identity / BufferStore / Page Aggregate Execution Checklist v1
|
||||
|
||||
> 创建时间:2026-05-21
|
||||
>
|
||||
@@ -6,7 +6,9 @@
|
||||
>
|
||||
> 阶段:Batch B / P1 工作区身份、BufferStore、Page Aggregate 收口
|
||||
>
|
||||
> 当前状态:`PROCESS`
|
||||
> 当前状态:`recycle / old`
|
||||
>
|
||||
> 归档说明(2026-05-22):本文的 Batch B 尾项已被后续 Batch / `done/` 文档和 `1-6`、`5-6` 的当前缺口口径覆盖;保留为历史执行记录,不再作为 active process。
|
||||
|
||||
## 1. 目标
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
# 3-17 Convex Export Web Entry v1
|
||||
# 3-17 [recycle] Convex Export Web Entry v1
|
||||
|
||||
状态:reference
|
||||
状态:`recycle / old`
|
||||
|
||||
归档说明(2026-05-21):CLI 迁移闭环已具备并有 smoke 证据;本文只保留为后续 Web 管理入口设计参考,当前不作为 active implementation checklist。若决定短期落地 Web UI,应新建执行 checklist。
|
||||
归档说明(2026-05-22):CLI 迁移闭环已具备并有 smoke 证据;但当前 Convex 默认运行时、根 functions 与自托管入口已退役到历史 / compat / sync replica 边界,本文的 Web 管理入口设想不再作为 active 或 reference 执行入口。若未来重新做 cloud/import UI,应按当前 SQLite control-plane 和 local-first 口径新建设计稿。
|
||||
|
||||
## 目标
|
||||
|
||||
+4
-2
@@ -1,8 +1,10 @@
|
||||
# 7-10 [process] 页面块 AI 工具执行 checklist v1
|
||||
# 7-10 [recycle] 页面块 AI 工具执行 checklist v1
|
||||
|
||||
> 更新时间:2026-05-16
|
||||
>
|
||||
> 当前状态:`PROCESS`。
|
||||
> 当前状态:`recycle / old`。
|
||||
>
|
||||
> 归档说明(2026-05-22):本文的块级 AI 执行路线已被 local-first Markdown 编辑收敛口径覆盖;当前 active AI 编辑入口以 `design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md` 为准。
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-13-page-block-identity-and-command-contract-v1.md`
|
||||
+68
-61
@@ -1,13 +1,20 @@
|
||||
# 7-14 [process] 在线 / 本地文档 AI Markdown 编辑路径收敛 v2
|
||||
# 7-14 [recycle][process] Local-first AI Markdown 编辑路径收敛 v2
|
||||
|
||||
> 创建时间:2026-05-16
|
||||
>
|
||||
> 更新时间:2026-05-16(v3:深度参考 CLI Main skill 系统,补全成熟度采纳清单)
|
||||
> 更新时间:2026-05-22(recycle:本文仍受 CLI Main / tool-first 旧口径影响,已移入 old,不再作为当前 AI 编辑实现依据)
|
||||
>
|
||||
> 归档说明(2026-05-22):本文试图从旧在线 / 本地收敛稿修正为 local-first,但仍把 CLI Main 与 `mnote.doc.*` 兼容工具放在过高位置,不符合当前 SQLite control-plane + local-first `.md` + agent 原生文件 patch/diff 主线。后续应新建更小的 active 设计稿,直接基于 SQLite、allowed roots、file refs、watcher、BufferStore、Page Aggregate 前台同步和审计链设计。
|
||||
>
|
||||
> 2026-05-18 口径补充:
|
||||
> - local-first workspace 已成为早期产品默认形态;本地 `.md` 是默认 AI 编辑目标,在线 Convex 文档降级为可选 cloud / sync / share source。
|
||||
> - 本文早期把 `mnote.doc.markdown_edit` 描述为统一主路径;最新口径改为:local-first 普通 Markdown 编辑优先给 agent 授权文件引用,由 agent 使用自身成熟的 diff / apply_patch / 文件编辑能力完成;`mnote.doc.markdown_edit` 保留为 cloud / remote agent / compat fallback。
|
||||
> - 页面内图片 / 附件上传已在 local source 下写入 `{mdBase}.assets/` 并保存相对 Markdown 路径,AI 后续处理附件引用时也应保留相对路径,不改写为 Convex media asset。
|
||||
> - local-first workspace 已成为早期产品默认形态;本地 `.md` 是默认 AI 编辑目标;旧 Convex 文档只作为历史迁移源或显式 compat/source replica 边界。
|
||||
> - 本文早期把 `mnote.doc.markdown_edit` 描述为统一主路径;最新口径改为:local-first 普通 Markdown 编辑优先给 agent 授权文件引用,由 agent 使用自身成熟的 diff / apply_patch / 文件编辑能力完成;`mnote.doc.markdown_edit` 保留为显式 remote agent / compat fallback。
|
||||
> - 页面内图片 / 附件上传已在 local source 下写入 `{mdBase}.assets/` 并保存相对 Markdown 路径,AI 后续处理附件引用时也应保留相对路径,不改写为历史 media asset。
|
||||
>
|
||||
> 2026-05-22 口径纠正:
|
||||
> - 当前 Convex runtime / functions 已退役,不再作为当前正文主链、默认控制面、AI 会话主存储或新增能力目标。
|
||||
> - 本文中的“在线文档”仅可理解为历史迁移源、显式 cloud compat / remote agent 边界或未来 sync replica,不再与 local-first `.md` 并列为当前主路径。
|
||||
> - 当前主线应写成:本地 `.md` 文件 + Rust SQLite control-plane + allowed roots / file references + agent 原生 patch/diff + watcher / BufferStore / Page Aggregate 前台同步。
|
||||
>
|
||||
> 当前状态:`PROCESS`
|
||||
>
|
||||
@@ -15,12 +22,12 @@
|
||||
> 1. 纠正 7-9 / 7-10 / 7-12 / 7-13 中隐含的「块级编辑是 AI 唯一写入路径」假设
|
||||
> 2. 基于 CLI Main 参考实现,确立 mnote 的「agent 原生文件 patch/diff 为 local-first 默认路径,MNote 文本级兼容工具 + 块级结构性操作为 fallback / 辅助」两层模型
|
||||
> 3. 规划 BlockNote AI 流式/review 能力的远期方向(当前不实施)
|
||||
> 4. 统一 cloud / remote / compat 文档和本地 `.md` 文件的 AI 读取、权限、冲突与回读口径
|
||||
> 4. 明确历史 cloud / remote / compat 文档边界,并把当前 AI 读取、权限、冲突与回读口径收口到 local-first `.md` 文件
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-9-page-block-ai-tooling-roadmap-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/reference/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/07-ai/done/7-13-page-block-editor-runtime-actor-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-13-rust-web-local-markdown-gfm-ast-parser-migration-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-22-local-folder-convex-unified-tree-execution-checklist-v1.md`
|
||||
@@ -42,13 +49,13 @@
|
||||
|
||||
## 1. 结论
|
||||
|
||||
**在线 Convex 文档和本地 `.md` 文件本质上是同一个东西:一段 markdown 文本,Tiptap 只是块级 UI 表现层。** 进一步切到 local-first 后,本地 `.md` 已经是普通文件,因此不需要再为常规正文编辑发明一套 MNote 专用工具。当前 07-ai 设计把 AI 编辑契约钉死在 `EditorBlockDocument` 的块级操作上,导致三个连锁问题:
|
||||
**当前事实源是 local-first `.md` 文件,Tiptap 只是块级 UI 表现层。** Convex 相关正文链已经退役为历史 / compat 边界,不能再和本地 `.md` 并列描述为当前主路径。既然默认对象已经是普通 Markdown 文件,就不需要再为常规正文编辑发明一套 MNote 专用块级工具。当前 07-ai 设计把 AI 编辑契约钉死在 `EditorBlockDocument` 的块级操作上,导致三个连锁问题:
|
||||
|
||||
1. **AI 被迫在块级操作**:对「改写这段话」「补充一段总结」「调整语气」「把所有 TODO 改成 DONE」等自然请求,AI 必须产出 `{ op: "replace", blockId: "block_1", ... }` 格式的块操作,而不能直接产出修改后的文本或搜索替换对。这增加了 AI 的认知负担和出错概率。
|
||||
|
||||
2. **本地 `.md` 文件没有 AI 能力**:本地文件没有稳定的 `blockId`(每次解析重新分配),因此块级工具无法应用于本地文件。当前 Hermes 的 13 个工具没有一个是面向本地文件的。
|
||||
2. **旧块级工具不适合 local-first `.md`**:本地文件没有稳定的 `blockId`(每次解析重新分配),因此块级工具不能作为普通 Markdown 编辑默认路径。
|
||||
|
||||
3. **两套口径维护**:在线文档(块操作)和本地文档(无 AI 路径)长期走两套路径,维护成本翻倍。
|
||||
3. **历史 compat 容易污染当前主线**:如果继续把 Convex / online 文档写成并列路径,后续实现会误把 `mnote.doc.*` 或 `documents.save` 当成新增能力入口,而不是 local-first 的 fallback / 迁移边界。
|
||||
|
||||
### 参考实现的验证
|
||||
|
||||
@@ -64,11 +71,11 @@
|
||||
|
||||
### 正确方向
|
||||
|
||||
> mnote 的 AI 编辑路线:**本地授权文件 + agent 原生 patch/diff 为主;MNote 兼容工具为 cloud/remote/结构化辅助;BlockNote AI 的流式/review 只作为远期交互参考。**
|
||||
> mnote 的 AI 编辑路线:**本地授权文件 + agent 原生 patch/diff 为主;MNote 兼容工具仅用于显式 cloud/remote compat 或结构化辅助;BlockNote AI 的流式/review 只作为远期交互参考。**
|
||||
|
||||
具体:
|
||||
|
||||
- **当前实施**(Phase A/B):local-first 页面 AI 只传当前文件引用、可选 selection 和用户指令;MNote 校验 `AiAccessScope` 后让 agent 在受限目录中使用原生 patch/diff 编辑 `.md`。`mnote.doc.markdown_edit` 作为兼容 / 远端代理 fallback,`mnote.block.*` 作为复杂结构辅助。
|
||||
- **当前实施**(Phase A/B):local-first 页面 AI 只传当前文件引用、可选 selection 和用户指令;MNote 校验 `AiAccessScope` 后让 agent 在受限目录中使用原生 patch/diff 编辑 `.md`。`mnote.doc.markdown_edit` 只作为显式 cloud / remote agent / compat fallback,`mnote.block.*` 只作为复杂结构辅助。
|
||||
- **远期规划**(Phase C):BlockNote AI 的流式增量 apply + suggest/review 层。当前先设计,不实施。
|
||||
|
||||
---
|
||||
@@ -109,7 +116,7 @@ docs +update --command block_move_after --block-id "block_3" --target-block-id "
|
||||
**对 mnote 的启示**:
|
||||
|
||||
1. **两层操作模型直接适用**。mnote 的 `markdown_edit` = CLI Main 的 `str_replace`(文本级),`apply_block_ops` = CLI Main 的 `block_*`(块级)。
|
||||
2. **但 mnote 的主格式是 markdown 而非 XML**。在线文档的持久化格式(Convex `documents.content`)可以投影为 markdown,本地文件本身就是 markdown。所以我们不需要 XML 这一层——markdown 既是 AI 编辑格式,也是人类可读格式。
|
||||
2. **但 mnote 的主格式是 markdown 而非 XML**。当前 local-first 正文事实源就是本地 `.md`,历史 cloud / compat 内容也只能先投影为 markdown 后进入兼容工具。所以我们不需要 XML 这一层——markdown 既是 AI 编辑格式,也是人类可读格式。
|
||||
3. **CLI Main 的 skill 格式**(YAML frontmatter + Markdown body,定义 tool shortcuts + 上下文格式规范)值得学习。mnote 的 Hermes `mnote` plugin 可以用类似方式组织。
|
||||
|
||||
#### 2.1.1 Skill YAML 前端元数据
|
||||
@@ -268,7 +275,7 @@ CLI Main 的 lark-doc skill 定义了嵌入式资源的显式路由表:
|
||||
```
|
||||
|
||||
**→ mnote 采纳**(远期,当文档内嵌入其他资源类型时):
|
||||
- 在线文档中嵌入的思维导图 → 路由到 `mnote-mindmap` skill
|
||||
- local-first Markdown 中嵌入的思维导图资源 → 路由到 `mnote-mindmap` skill
|
||||
- 嵌入的附件/文件 → 路由到 `mnote-file` skill
|
||||
- 当前 Phase A 不实施,预留路由表字段
|
||||
|
||||
@@ -284,7 +291,7 @@ CLI Main 的格式选择规则:
|
||||
| 整篇导入/导出 | Markdown | 人类可读,和 `.md` 互通 |
|
||||
| AI 对话引用 | Markdown | 模型理解 markdown 远好于 XML |
|
||||
|
||||
**→ mnote 采纳**:CLI Main 用 XML 做默认格式是因为飞书文档本身是 XML 存储。mnote 的在线文档持久化格式(Convex `documents.content`)和本地文件都是 markdown,因此 markdown 是 mnote 的默认和唯一 AI 格式。这是正确的差异化决策。
|
||||
**→ mnote 采纳**:CLI Main 用 XML 做默认格式是因为飞书文档本身是 XML 存储。mnote 当前主存储是 local-first `.md` 文件,历史 cloud / compat 内容也必须先投影成 markdown 后再进入 AI 编辑面,因此 markdown 是 mnote 的默认和唯一 AI 格式。这是正确的差异化决策。
|
||||
|
||||
#### 2.1.7 Skill CI 校验
|
||||
|
||||
@@ -320,8 +327,8 @@ LLM 流式输出 partial JSON
|
||||
|------|------|---------------|
|
||||
| `StreamTool` | 单操作的定义:name + inputSchema + validate + execute | mnote 已有(`mnote.block.*` / `mnote.doc.*`),不需要重构 |
|
||||
| `StreamToolExecutor` | 流式解析 partial JSON → 逐条 enqueue → 按顺序 execute | Phase C 新增:`StreamApplyController` |
|
||||
| `suggestChanges` | ProseMirror suggestion marks:AI 编辑不直接落盘,先标记为待审阅 | Phase C 新增:在线文档的 `ReviewSession` |
|
||||
| `RebaseTool` | 协作场景:用户同时在编辑 → AI 操作 rebase 到最新文档状态 | Phase C 考虑(协作场景依赖 Convex 的 revision 乐观锁) |
|
||||
| `suggestChanges` | ProseMirror suggestion marks:AI 编辑不直接落盘,先标记为待审阅 | Phase C 新增:local-first `ReviewSession` / 临时审阅层 |
|
||||
| `RebaseTool` | 协作场景:用户同时在编辑 → AI 操作 rebase 到最新文档状态 | Phase C 考虑;local-first 下优先基于 file version / BufferStore 冲突模型 |
|
||||
| `delayAgentStep` | 逐条 apply 之间加 50-200ms 延迟,给用户"AI 正在操作"的可见性 | Phase C 可选改善 |
|
||||
|
||||
**BlockNote AI 不适合直接搬的原因**:它的所有操作都通过 `id`(blockId)寻址。`add` 需要 `referenceId`、`update` 需要 `id`、`delete` 需要 `id`。这意味着:
|
||||
@@ -354,23 +361,23 @@ LLM 流式输出 partial JSON
|
||||
|
||||
「把文章中所有 TODO 改成 DONE」这类跨块请求,当前需要逐一产出 N 个 `{ op: "replace", blockId: "..." }`。文本级 search/replace 只需一条:`{ search: "TODO", replace: "DONE" }`。
|
||||
|
||||
### 3.2 本地文件没有 AI 写入路径
|
||||
### 3.2 旧块级工具没有 local-first 默认写入语义
|
||||
|
||||
> 参考对照:CLI Main 的 skill 可以操作任何 `doc-token`(包括本地文件和云端文档),因为它用的是文本级 + XML 级工具,不依赖特定存储。
|
||||
|
||||
mnote 当前:Hermes 只知道 workspace/document 模型,不知道本地文件夹/文件模型。本地 `.md` 文件在 AI 视角完全不可见。
|
||||
mnote 当前主线:页面已能定位 local folder `.md`,但普通 AI 正文编辑不能回到“必须构造完整 page context 或 block ops”的旧模型。AI 视角应看到受控文件引用、selection 和 allowed roots,而不是一份由前端拼出的第二事实源。
|
||||
|
||||
### 3.3 两套体系互不相认
|
||||
|
||||
| | 在线 Convex 文档 | 本地 .md 文件 |
|
||||
| | 历史 cloud / compat 文档 | 当前 local-first `.md` 文件 |
|
||||
|---|---|---|
|
||||
| 存储 | Convex `documents.content` | 文件系统 `*.md` |
|
||||
| AI 读取 | `mnote.doc.fetch`(block projection) | 无 |
|
||||
| AI 写入 | `mnote.block.*` / `mnote.doc.apply_block_ops` | 无 |
|
||||
| 写入粒度 | 块级(需要 blockId) | —(无 AI 路径) |
|
||||
| 设计覆盖 | 07-ai 全部文档 | 仅在 03-rust-web 讨论解析 |
|
||||
| 存储 | 历史迁移源 / 显式 cloud compat / sync replica | 文件系统 `*.md` |
|
||||
| AI 读取 | `mnote.doc.fetch` 兼容投影 | 当前文件引用 / Page Aggregate / local markdown |
|
||||
| AI 写入 | `mnote.doc.*` 或 `mnote.block.*` 兼容 fallback | agent 原生 patch/diff + watcher 同步 |
|
||||
| 写入粒度 | 工具代理,不能作为新增默认入口 | 文件级 patch + selection 辅助 |
|
||||
| 设计定位 | 历史 / compat 边界 | 当前主路径 |
|
||||
|
||||
两份体系在设计中互不相认。3-13 写明"不影响 Convex workspace 链路"——这是主动划清界限。**收敛是必须的,markdown 是共同分母。**
|
||||
旧设计把 compat 文档和本地文件写成两套体系,容易让后续实现继续往旧 cloud / block 工具堆逻辑。当前必须收敛:**local-first `.md` 是默认事实源,markdown 是共同格式,compat 只服务迁移、显式 cloud 或 remote agent 边界。**
|
||||
|
||||
---
|
||||
|
||||
@@ -380,28 +387,28 @@ mnote 当前:Hermes 只知道 workspace/document 模型,不知道本地文
|
||||
|
||||
> 参考对照:CLI Main 用 Markdown 做整篇导入/导出和对话引用,用 XML 做精确块编辑。mnote 直接用 Markdown 做所有 AI 操作,因为 mnote 没有 XML 存储层。
|
||||
|
||||
AI 最自然的编辑方式是对文本进行操作。块是 UI 概念,不是 AI 概念。在线文档的持久化格式和本地文件的持久化格式都可以投影为 markdown。
|
||||
AI 最自然的编辑方式是对文本进行操作。块是 UI 概念,不是 AI 概念。当前 local-first 持久化格式就是 markdown;历史 cloud / compat 内容若需要进入 AI 编辑,也必须先投影成 markdown。
|
||||
|
||||
### 4.2 两层操作模型(兼容层)
|
||||
|
||||
| 层 | 工具 | 寻址方式 | 适用场景 | 占比 |
|
||||
|----|------|---------|---------|------|
|
||||
| **文件级(主)** | agent 原生 `diff/apply_patch/文件编辑` | 授权文件引用 / selection | local-first 普通 Markdown 改写 | 80%+ |
|
||||
| **文本级(兼容)** | `mnote.doc.markdown_edit` | search/replace 文本对 | cloud / remote agent / 兼容旧页面 AI | 次要 |
|
||||
| **文本级(兼容)** | `mnote.doc.markdown_edit` | search/replace 文本对 | 显式 cloud / remote agent / 兼容旧页面 AI | 次要 |
|
||||
| **块级(辅助)** | `mnote.doc.apply_block_ops` | blockId / matchText | "把第三块拖到第一块后面"、"精确删除引用块" | <20% |
|
||||
|
||||
### 4.3 在线和本地的主写入路径
|
||||
### 4.3 local-first 主写入路径
|
||||
|
||||
```
|
||||
页面 AI / ACP
|
||||
→ resolve_source(documentId) → Convex | LocalFS
|
||||
→ local-first: 传授权文件引用给 agent runtime
|
||||
→ resolve_current_file(documentId, sourceKind=local_folder, rootUri)
|
||||
→ 传授权文件引用 / selection / allowed roots 给 agent runtime
|
||||
→ agent 原生 patch/diff 写入文件
|
||||
→ MNote 做权限 / 审计 / refresh
|
||||
→ cloud / remote fallback: mnote.doc.markdown_edit
|
||||
→ MNote 做权限 / 审计 / watcher refresh / BufferStore 冲突
|
||||
→ 仅在显式 cloud / remote compat 时 fallback 到 mnote.doc.markdown_edit
|
||||
```
|
||||
|
||||
差异主要在执行层:local-first 直接让 agent 修改授权文件;cloud 或受限 remote runtime 无法直接访问本地文件时,再走 `mnote.doc.markdown_edit` 代理。这和 CLI Main 的 `docs +update` 可以操作任何 `doc-token` 的设计一致,只是 mnote 进一步把“编辑算法”让渡给 agent runtime。
|
||||
当前执行层不再二选一。默认只有 local-first 授权文件路径;cloud 或受限 remote runtime 无法直接访问本地文件时,才显式走 `mnote.doc.markdown_edit` 代理。这和 CLI Main 的 `docs +update` 可以操作任何 `doc-token` 的设计一致,只是 mnote 进一步把“编辑算法”让渡给 agent runtime。
|
||||
|
||||
### 4.4 Diff 是内部实现细节
|
||||
|
||||
@@ -434,17 +441,17 @@ AI 最自然的编辑方式是对文本进行操作。块是 UI 概念,不是
|
||||
```
|
||||
|
||||
增强点:
|
||||
- `format: "markdown"` — 新增值。在线文档由 Page Aggregate 产出 PageMarkdown;本地文件直接返回 `.md` 原文。
|
||||
- `documentId` — 自动检测 source:Convex workspace 文档 vs 本地文件系统路径。
|
||||
- `format: "markdown"` — 兼容值。local-first 文档直接返回 `.md` 原文;历史 cloud / compat 文档必须经 Page Aggregate 投影成 markdown。
|
||||
- `documentId` — 当前默认解析为 local-first 文档;只有显式 sourceKind / compat 标记允许进入 cloud adapter。
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"source": "convex",
|
||||
"documentId": "tree_xxx",
|
||||
"revision": 3,
|
||||
"source": "local_folder",
|
||||
"documentId": "local-md:README.md",
|
||||
"fileVersion": "local-md:README.md:...",
|
||||
"format": "markdown",
|
||||
"content": "# 标题\n\n段落内容...\n\n## 子标题\n\n...",
|
||||
"truncated": false,
|
||||
@@ -471,7 +478,7 @@ AI 最自然的编辑方式是对文本进行操作。块是 UI 概念,不是
|
||||
服务端处理流程:
|
||||
|
||||
```
|
||||
1. resolve_source(documentId) → ConvexAdapter | LocalFSAdapter
|
||||
1. resolve_source(documentId, sourceKind, rootUri) → LocalFSAdapter | explicit CompatAdapter
|
||||
2. 读取当前 markdown 全文
|
||||
3. 逐条 search_replace(精确匹配 → fuzzy fallback)
|
||||
4. 计算内部 diff(用于 BlockDelta 推送)
|
||||
@@ -484,16 +491,16 @@ AI 最自然的编辑方式是对文本进行操作。块是 UI 概念,不是
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"source": "convex",
|
||||
"documentId": "tree_xxx",
|
||||
"revision": { "before": 3, "after": 4 },
|
||||
"source": "local_folder",
|
||||
"documentId": "local-md:README.md",
|
||||
"fileVersion": { "before": "local-md:README.md:...", "after": "local-md:README.md:..." },
|
||||
"operationsApplied": 2,
|
||||
"operationsFailed": 0,
|
||||
"failedOperations": [],
|
||||
"changedText": "已将「原文片段」替换为「新文本」\n已将「另一段」替换为「改写后的内容」",
|
||||
"blockDelta": {
|
||||
"documentId": "tree_xxx",
|
||||
"revision": 4,
|
||||
"documentId": "local-md:README.md",
|
||||
"fileVersion": "local-md:README.md:...",
|
||||
"operations": [...]
|
||||
}
|
||||
}
|
||||
@@ -518,7 +525,7 @@ AI 最自然的编辑方式是对文本进行操作。块是 UI 概念,不是
|
||||
|
||||
多条 operation 按数组顺序串行执行。前一条的替换结果对后一条可见(和 sed 语义一致)。
|
||||
|
||||
**乐观锁**:执行前用 `revision` 检测。如果写入时 revision 已过期,返回冲突错误,让 AI 重新 fetch + edit。
|
||||
**乐观锁**:local-first 下执行前用 file version / conflict detection key 检测。如果写入时文件版本已过期,返回冲突错误,让 AI 重新 fetch + edit;历史 cloud compat 才能使用旧 revision 语义。
|
||||
|
||||
---
|
||||
|
||||
@@ -534,7 +541,7 @@ AI 最自然的编辑方式是对文本进行操作。块是 UI 概念,不是
|
||||
2. 服务端逐条 apply 并推送 BlockDelta
|
||||
3. 编辑器逐条渲染修改(带延迟,模拟"AI 正在编辑")
|
||||
4. 用户可逐条 accept / reject(suggestion 模式)
|
||||
5. 确认后才最终写入 Convex(review 模式)
|
||||
5. 确认后才最终写入本地 `.md` 或显式 compat 目标(review 模式)
|
||||
|
||||
### 7.2 与 BlockNote AI 的差异
|
||||
|
||||
@@ -558,9 +565,9 @@ StreamApplyController(新增)
|
||||
4. 可选:注入 delay(50-200ms)模拟人类编辑节奏
|
||||
输出:逐条 BlockDelta
|
||||
|
||||
ReviewSession(新增,在线文档)
|
||||
ReviewSession(新增,local-first 审阅层)
|
||||
状态:pending → accepted | rejected
|
||||
存储:Convex review_session 表或 documents 的 review 字段
|
||||
存储:SQLite control-plane / 本地 transient review state;不依赖 Convex
|
||||
生命周期:
|
||||
- AI 编辑 → 创建 ReviewSession → 所有修改标记为 pending
|
||||
- 用户逐条操作 → accept/reject → 更新 session 状态
|
||||
@@ -577,10 +584,10 @@ GhostTextOverlay(新增,编辑器)
|
||||
|------|------|------|
|
||||
| C-1 | `StreamApplyController`:流式 apply + SSE 逐条推送 delta | 7-13 EditorRuntimeActor delta 通道 |
|
||||
| C-2 | `GhostTextOverlay`:编辑器内 diff 展示(红删绿增) | leptos-tiptap 的 decoration 能力 |
|
||||
| C-3 | `ReviewSession`:在线文档的 accept/reject session | Convex review 表设计 |
|
||||
| C-3 | `ReviewSession`:local-first accept/reject session | SQLite control-plane / editor transient review state 设计 |
|
||||
| C-4 | `delayAgentStep`:可选的编辑节奏模拟 | C-1 完成 |
|
||||
|
||||
**当前不做实施决策**。Phase C 的启动时机以 Phase A/B 完成后,编辑器能力和 Convex review 表设计就绪为前置条件。
|
||||
**当前不做实施决策**。Phase C 的启动时机以 Phase A/B 完成后,编辑器 decoration 能力、BufferStore/file version 冲突模型和 SQLite control-plane review state 设计就绪为前置条件。
|
||||
|
||||
---
|
||||
|
||||
@@ -588,19 +595,19 @@ GhostTextOverlay(新增,编辑器)
|
||||
|
||||
### Phase A:文件引用主路径 + `mnote.doc.*` 兼容层
|
||||
|
||||
- [x] `mnote.doc.fetch` 增加 `format: "markdown"`(在线文档 Page Aggregate → PageMarkdown)
|
||||
- [x] `mnote.doc.fetch` 增加本地文件 source 路由(自动检测 Convex vs 文件系统路径)
|
||||
- [x] 实现 `resolve_source(documentId)` — 本地文件路径 `local_fs` vs 其余走 Convex
|
||||
- [x] `mnote.doc.fetch` 增加 `format: "markdown"`(local-first `.md` / Page Aggregate markdown 投影)
|
||||
- [x] `mnote.doc.fetch` 增加本地文件 source 路由(`local_folder` / `rootUri` / `local-md:*`)
|
||||
- [x] 实现 `resolve_source(documentId, sourceKind, rootUri)` — 默认 `local_folder`;历史 cloud / compat 必须显式进入 compat adapter
|
||||
- [ ] 页面 AI / ACP 普通正文编辑默认只传当前文件引用、可选 selection 和用户指令,不再默认构造完整 page context
|
||||
- [ ] 本地 agent runtime 在 `allowed_roots / allowed_file_paths` 内执行 patch/diff,并把 changed files / diff 摘要回传 MNote
|
||||
- [x] 实现 `search_replace(text, operations)` — 四级匹配策略(精确→宽松→段落 fuzzy→失败)
|
||||
- [x] 实现 Convex 写入 adapter(复用 `doc_apply_block_ops` 链路)
|
||||
- [x] 历史 cloud / compat 写入 adapter 已降级为 fallback,不再作为 local-first 默认入口
|
||||
- [x] 实现本地文件写入 adapter(`mnote.doc.markdown_edit` 检测到本地文件路径时直接 `fs::write` 写回,不经过 Convex)
|
||||
- [ ] 内部 diff 生成 + BlockDelta 推送(复用 7-13 delta 基础设施,待 Phase C 实现)
|
||||
- [x] Hermes tool manifest 注册 `mnote.doc.markdown_edit`
|
||||
- [x] Hermes `mnote` plugin 更新:`mnote_doc_fetch` schema + `mnote_doc_markdown_edit` 新增
|
||||
- [x] 乐观锁:`revision` 冲突检测(`doc_apply_block_ops` 已有)
|
||||
- [x] 浏览器 smoke(在线文档 `format: "markdown"` + `markdown_edit` 搜索替换)
|
||||
- [x] 乐观锁:local-first 使用 file version / conflict detection key;compat 路径才保留 revision 语义
|
||||
- [x] 历史 browser smoke(cloud compat `format: "markdown"` + `markdown_edit` 搜索替换)仅作兼容证据,不代表当前主路径
|
||||
- [x] 单元测试(`search_replace` 精确/失败/全文、`blocks_to_markdown` with_ids/heading 共 5 测试通过)
|
||||
- [x] 浏览器 smoke(本地 `.md` 文件读取 `mnote_doc_fetch` + 写入 `mnote_doc_markdown_edit`,`full_content` 创建 + `operations` 搜索替换 + 回读验证全部通过)
|
||||
|
||||
@@ -662,7 +669,7 @@ GhostTextOverlay(新增,编辑器)
|
||||
- 不强迫 local-first agent 产出 MNote 自定义 diff;Hermes / Reasonix 可使用自身成熟 patch / diff / apply_patch 能力,MNote 负责白名单权限、文件版本冲突和审计。
|
||||
- 不要求本地文件有稳定的 `blockId`(本地文件没有 block identity)。
|
||||
- 不在 markdown_edit 内部引入新的 AI 模型调用(diff 是确定性算法)。
|
||||
- 不把 Convex `documents:updateContent` 重新提升为 local-first 正文主存储;`markdown_edit` 在 cloud / compat 场景可复用受控保存路径。
|
||||
- 不把 Convex / `documents:updateContent` 重新提升为当前正文主存储;`markdown_edit` 只在显式 cloud / compat 场景复用受控保存路径。
|
||||
- 不把 `mnote.page.save` 重新描述为精确编辑主入口(它仍是兜底工具)。
|
||||
- **不照搬 BlockNote AI 的纯 blockId 寻址模式**(与 mnote 的 markdown 优先策略冲突)。
|
||||
|
||||
@@ -670,12 +677,12 @@ GhostTextOverlay(新增,编辑器)
|
||||
|
||||
## 11. 成功标准
|
||||
|
||||
- [x] `mnote.doc.fetch(documentId, format: "markdown")` 对在线文档返回正确 markdown
|
||||
- [x] `mnote.doc.fetch(documentId, format: "markdown")` 对历史 cloud / compat 文档返回正确 markdown(兼容证据)
|
||||
- [ ] `mnote.doc.fetch(documentId, format: "markdown")` 对本地 `.md` 文件返回正确 markdown(兼容路径)
|
||||
- [x] `mnote.doc.markdown_edit` 的简单搜索替换(1 条 operation)浏览器 smoke 通过
|
||||
- [ ] `mnote.doc.markdown_edit` 的复杂改写(3+ 条 operations)成功率 > 80%
|
||||
- [ ] 本地 `.md` 文件通过页面 AI 面板可被读取和写入,且默认通过 agent 原生 patch/diff 完成
|
||||
- [x] 在线文档的 markdown_edit 不增加 Convex RTT(和当前块操作持平)
|
||||
- [x] 历史 cloud / compat markdown_edit 不增加额外 RTT(兼容证据,不代表当前主路径)
|
||||
- [x] `direct_block_edit_operations` 已退役(路由跳过,代码保留)
|
||||
- [x] `page_ai_workflow.rs` 的 system prompt 已补全 search/replace schema
|
||||
- [x] Phase C(流式/review)的设计已冻结,不阻塞 A/B 实施
|
||||
@@ -63,6 +63,14 @@ fn capabilities_json(capabilities: &[String]) -> Result<String, ControlPlaneErro
|
||||
serde_json::to_string(capabilities).map_err(ControlPlaneError::from)
|
||||
}
|
||||
|
||||
fn file_uri_to_legacy_path(value: &str) -> String {
|
||||
value
|
||||
.trim()
|
||||
.strip_prefix("file://")
|
||||
.unwrap_or("")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn row_to_user(row: &rusqlite::Row<'_>) -> rusqlite::Result<UserRecord> {
|
||||
Ok(UserRecord {
|
||||
id: row.get(0)?,
|
||||
@@ -793,13 +801,21 @@ impl ControlPlaneStore for SqliteControlPlaneStore {
|
||||
root_uri: &str,
|
||||
) -> Result<ResolvedAccess, ControlPlaneError> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let root_path = file_uri_to_legacy_path(root_uri);
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, user_id, workspace_id, root_uri, root_path, permission, recursive, capabilities_json, source, status, created_by, created_at, updated_at, revision
|
||||
FROM directory_grants
|
||||
WHERE user_id = ?1 AND status = 'active' AND (?2 = root_uri OR (recursive = 1 AND ?2 LIKE root_uri || '%'))",
|
||||
WHERE user_id = ?1
|
||||
AND status = 'active'
|
||||
AND (
|
||||
?2 = root_uri
|
||||
OR (recursive = 1 AND ?2 LIKE root_uri || '%')
|
||||
OR (?3 != '' AND ?3 = root_path)
|
||||
OR (?3 != '' AND recursive = 1 AND ?3 LIKE root_path || '/%')
|
||||
)",
|
||||
)?;
|
||||
let grants = stmt
|
||||
.query_map(params![actor_id, root_uri], row_to_grant)?
|
||||
.query_map(params![actor_id, root_uri, root_path], row_to_grant)?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut permission = "none".to_string();
|
||||
for grant in &grants {
|
||||
@@ -1800,6 +1816,31 @@ mod tests {
|
||||
assert_eq!(access.permission, "none");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_access_supports_legacy_directory_grants_with_plain_path_root_uri() {
|
||||
let store = store();
|
||||
create_user(&store, "legacy_reader");
|
||||
store
|
||||
.grant_directory_access(DirectoryGrantInput {
|
||||
user_id: "legacy_reader".to_string(),
|
||||
workspace_id: None,
|
||||
root_uri: "/tmp/shared".to_string(),
|
||||
root_path: "/tmp/shared".to_string(),
|
||||
permission: "read".to_string(),
|
||||
recursive: true,
|
||||
capabilities: vec![],
|
||||
source: "legacy".to_string(),
|
||||
created_by: None,
|
||||
})
|
||||
.expect("legacy grant");
|
||||
|
||||
let access = store
|
||||
.resolve_access("legacy_reader", "file:///tmp/shared/page.md")
|
||||
.expect("resolve access");
|
||||
assert_eq!(access.permission, "read");
|
||||
assert_eq!(access.grant_ids.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn share_link_token_is_hashed_and_revocable() {
|
||||
let store = store();
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::routes::local_folder_source::{
|
||||
control_plane_db_path_display, create_default_local_workspace_for_actor,
|
||||
ensure_local_workspace_read_access, is_local_access_policy_admin_context,
|
||||
ensure_local_workspace_read_access_with_state, is_local_access_policy_admin_context,
|
||||
load_local_folder_page_tree_snapshot, load_local_trash_entries,
|
||||
};
|
||||
use crate::routes::snapshot_support::load_sidebar_dataset;
|
||||
@@ -311,7 +311,7 @@ pub async fn root_entry(
|
||||
.ok_or_else(|| {
|
||||
WebError::bad_request_code("local_folder_root_required", "缺少本地文件夹 rootUri")
|
||||
})?;
|
||||
ensure_local_workspace_read_access(&context, root_uri)?;
|
||||
ensure_local_workspace_read_access_with_state(&state, &context, root_uri)?;
|
||||
let snapshot = load_local_folder_page_tree_snapshot(root_uri)?;
|
||||
let workspace_id = snapshot
|
||||
.dataset
|
||||
@@ -614,7 +614,7 @@ pub async fn trash_entry(
|
||||
.ok_or_else(|| {
|
||||
WebError::bad_request_code("local_folder_root_required", "缺少本地文件夹 rootUri")
|
||||
})?;
|
||||
ensure_local_workspace_read_access(&context, root_uri)
|
||||
ensure_local_workspace_read_access_with_state(&state, &context, root_uri)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
let workspace_id =
|
||||
crate::routes::local_folder_source::local_workspace_id_from_root_uri(root_uri)?;
|
||||
@@ -1780,6 +1780,7 @@ fn handle_sqlite_auth_action(
|
||||
&resolved.user.id,
|
||||
resolved.user.email.as_deref().unwrap_or_default(),
|
||||
&resolved.user.display_name,
|
||||
&effective_sqlite_auth_actor_type(&resolved.user.id, &resolved.user.role),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1809,18 +1810,20 @@ fn build_sqlite_auth_response(
|
||||
user_id: &str,
|
||||
email: &str,
|
||||
name: &str,
|
||||
actor_type: &str,
|
||||
) -> Response {
|
||||
let mut response = axum::Json(json!({
|
||||
"ok": true,
|
||||
"userId": user_id,
|
||||
"email": email,
|
||||
"name": name,
|
||||
"authMode": "sqliteSession"
|
||||
"authMode": "sqliteSession",
|
||||
"actorType": actor_type
|
||||
}))
|
||||
.into_response();
|
||||
set_literal_cookie(response.headers_mut(), COOKIE_MNOTE_SESSION, session_token);
|
||||
set_literal_cookie(response.headers_mut(), COOKIE_MNOTE_ACTOR_ID, user_id);
|
||||
set_literal_cookie(response.headers_mut(), COOKIE_MNOTE_ACTOR_TYPE, "user");
|
||||
set_literal_cookie(response.headers_mut(), COOKIE_MNOTE_ACTOR_TYPE, actor_type);
|
||||
if !email.trim().is_empty() {
|
||||
set_encoded_cookie(response.headers_mut(), COOKIE_MNOTE_ACTOR_EMAIL, email);
|
||||
}
|
||||
@@ -1835,6 +1838,19 @@ fn build_sqlite_auth_response(
|
||||
response
|
||||
}
|
||||
|
||||
fn effective_sqlite_auth_actor_type(user_id: &str, stored_role: &str) -> String {
|
||||
let role = stored_role.trim();
|
||||
let fallback_role = if role.is_empty() { "user" } else { role };
|
||||
if crate::routes::local_folder_source::is_local_access_policy_admin_actor(
|
||||
user_id,
|
||||
fallback_role,
|
||||
) {
|
||||
"admin".to_string()
|
||||
} else {
|
||||
fallback_role.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn build_sqlite_sign_out_response(state: &AppState, context: &RequestContext) -> Response {
|
||||
if let Some(raw_token) = extract_cookie_value(context, COOKIE_MNOTE_SESSION) {
|
||||
let token_hash = session_token_hash(&raw_token);
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::app::AppState;
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::routes::local_folder_source::{
|
||||
ensure_local_workspace_read_access, load_local_folder_file_tree_snapshot,
|
||||
ensure_local_workspace_read_access_with_state, load_local_folder_file_tree_snapshot,
|
||||
load_local_folder_page_tree_snapshot,
|
||||
};
|
||||
use crate::routes::query_support::resolve_effective_workspace_id;
|
||||
@@ -85,7 +85,7 @@ async fn project_projection(
|
||||
.ok_or_else(|| {
|
||||
WebError::bad_request_code("local_folder_root_required", "缺少本地文件夹 rootUri")
|
||||
})?;
|
||||
ensure_local_workspace_read_access(&context, root_uri)
|
||||
ensure_local_workspace_read_access_with_state(&state, &context, root_uri)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
let snapshot = if projection == KernelProjectionKind::FileTree {
|
||||
load_local_folder_file_tree_snapshot(root_uri)?
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::app::AppState;
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::routes::local_folder_source::{
|
||||
decode_local_id_segment, ensure_local_workspace_read_access,
|
||||
decode_local_id_segment, ensure_local_workspace_read_access_with_state,
|
||||
load_local_folder_file_tree_snapshot, load_local_folder_page_tree_snapshot,
|
||||
local_folder_watch_revision, local_workspace_id_from_root_uri,
|
||||
};
|
||||
@@ -36,7 +36,8 @@ pub async fn local_folder_events(
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Query(query): Query<LocalFolderEventsQuery>,
|
||||
) -> Result<(HeaderMap, Sse<BoxedEventStream>), WebError> {
|
||||
let canonical_root = ensure_local_workspace_read_access(&context, &query.root_uri)
|
||||
let canonical_root =
|
||||
ensure_local_workspace_read_access_with_state(&state, &context, &query.root_uri)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
|
||||
let (mut headers, stream): (HeaderMap, BoxedEventStream) = if query.tree_live.unwrap_or(false) {
|
||||
|
||||
@@ -27,7 +27,7 @@ use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fs;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -132,7 +132,7 @@ struct LocalShareGrant {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum LocalAccessMode {
|
||||
pub(crate) enum LocalAccessMode {
|
||||
Read,
|
||||
Write,
|
||||
}
|
||||
@@ -490,7 +490,7 @@ fn ensure_local_workspace_access_for_actor_with_mode(
|
||||
))
|
||||
}
|
||||
|
||||
fn ensure_local_workspace_access_with_state(
|
||||
pub(crate) fn ensure_local_workspace_access_with_state(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
root_uri: &str,
|
||||
@@ -525,18 +525,7 @@ fn ensure_local_workspace_access_with_state(
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_local_workspace_read_access(
|
||||
context: &RequestContext,
|
||||
root_uri: &str,
|
||||
) -> Result<PathBuf, WebError> {
|
||||
ensure_local_workspace_read_access_for_actor(
|
||||
&context.auth.actor_id,
|
||||
&context.auth.actor_type,
|
||||
root_uri,
|
||||
)
|
||||
}
|
||||
|
||||
fn ensure_local_workspace_read_access_with_state(
|
||||
pub(crate) fn ensure_local_workspace_read_access_with_state(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
root_uri: &str,
|
||||
@@ -544,6 +533,14 @@ fn ensure_local_workspace_read_access_with_state(
|
||||
ensure_local_workspace_access_with_state(state, context, root_uri, LocalAccessMode::Read)
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_local_workspace_write_access_with_state(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
root_uri: &str,
|
||||
) -> Result<PathBuf, WebError> {
|
||||
ensure_local_workspace_access_with_state(state, context, root_uri, LocalAccessMode::Write)
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_local_path_read_access(
|
||||
context: &RequestContext,
|
||||
root_uri: &str,
|
||||
@@ -587,6 +584,17 @@ pub(crate) fn ensure_local_path_read_access(
|
||||
Ok(canonical_target)
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_local_workspace_read_access(
|
||||
context: &RequestContext,
|
||||
root_uri: &str,
|
||||
) -> Result<PathBuf, WebError> {
|
||||
ensure_local_workspace_read_access_for_actor(
|
||||
&context.auth.actor_id,
|
||||
&context.auth.actor_type,
|
||||
root_uri,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_local_workspace_access(
|
||||
context: &RequestContext,
|
||||
root_uri: &str,
|
||||
@@ -598,14 +606,6 @@ pub(crate) fn ensure_local_workspace_access(
|
||||
)
|
||||
}
|
||||
|
||||
fn ensure_local_workspace_write_access_with_state(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
root_uri: &str,
|
||||
) -> Result<PathBuf, WebError> {
|
||||
ensure_local_workspace_access_with_state(state, context, root_uri, LocalAccessMode::Write)
|
||||
}
|
||||
|
||||
fn local_access_policy_path() -> PathBuf {
|
||||
std::env::var(ENV_LOCAL_ACCESS_POLICY_FILE)
|
||||
.ok()
|
||||
@@ -759,6 +759,10 @@ fn require_share_grants_admin(context: &RequestContext) -> Result<(), WebError>
|
||||
pub(crate) fn is_local_access_policy_admin_context(context: &RequestContext) -> bool {
|
||||
let actor_id = context.auth.actor_id.trim();
|
||||
let actor_type = context.auth.actor_type.trim();
|
||||
is_local_access_policy_admin_actor(actor_id, actor_type)
|
||||
}
|
||||
|
||||
pub(crate) fn is_local_access_policy_admin_actor(actor_id: &str, actor_type: &str) -> bool {
|
||||
if actor_id.is_empty()
|
||||
|| actor_id == "anonymous"
|
||||
|| actor_type.is_empty()
|
||||
@@ -1629,6 +1633,7 @@ fn control_plane_grant_payload(grant: &DirectoryGrantRecord) -> Value {
|
||||
json!({
|
||||
"id": grant.id,
|
||||
"userId": grant.user_id,
|
||||
"workspaceId": grant.workspace_id,
|
||||
"rootUri": grant.root_uri,
|
||||
"rootPath": grant.root_path,
|
||||
"permission": grant.permission,
|
||||
@@ -1643,6 +1648,16 @@ fn control_plane_grant_payload(grant: &DirectoryGrantRecord) -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
fn is_default_workspace_auto_grant(grant: &DirectoryGrantRecord) -> bool {
|
||||
grant.source.trim() == "auto"
|
||||
&& grant.permission.trim() == "write"
|
||||
&& grant.recursive
|
||||
&& grant.created_by.as_deref().map(str::trim) == Some(grant.user_id.as_str())
|
||||
&& grant.workspace_id.is_some()
|
||||
&& grant.root_uri.starts_with("local://users/")
|
||||
&& grant.root_uri.ends_with("/workspaces/my-space")
|
||||
}
|
||||
|
||||
fn sqlite_access_policy_payload(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
@@ -1689,7 +1704,10 @@ fn sqlite_user_access_policy_payload(
|
||||
.map_err(|error| WebError::internal(format!("SQLite 控制面授权列表读取失败: {error}")))?;
|
||||
let grant_values = grants
|
||||
.iter()
|
||||
.filter(|grant| grant.created_by.as_deref().map(str::trim) == Some(actor_id))
|
||||
.filter(|grant| {
|
||||
grant.user_id.trim() == actor_id
|
||||
|| grant.created_by.as_deref().map(str::trim) == Some(actor_id)
|
||||
})
|
||||
.map(control_plane_grant_payload)
|
||||
.collect::<Vec<_>>();
|
||||
Ok(json!({
|
||||
@@ -1871,6 +1889,13 @@ fn delete_sqlite_user_access_grant_for_context(
|
||||
"目录授权不存在或不属于当前用户",
|
||||
)
|
||||
})?;
|
||||
if is_default_workspace_auto_grant(grant) {
|
||||
return Err(WebError::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
"local_access_policy_system_grant_readonly",
|
||||
"默认空间的系统授权不能撤销",
|
||||
));
|
||||
}
|
||||
if grant.created_by.as_deref().map(str::trim) != Some(actor_id) {
|
||||
return Err(WebError::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
@@ -1933,6 +1958,26 @@ fn delete_sqlite_local_access_grant_for_context(
|
||||
"必须提供 grantId",
|
||||
));
|
||||
}
|
||||
let grants = state
|
||||
.control_plane()
|
||||
.find_directory_grants(DirectoryGrantLookup {
|
||||
grant_id: Some(grant_id.to_string()),
|
||||
user_id: None,
|
||||
root_uri: None,
|
||||
include_revoked: false,
|
||||
})
|
||||
.map_err(|error| WebError::internal(format!("SQLite 控制面授权查找失败: {error}")))?;
|
||||
if grants
|
||||
.first()
|
||||
.map(is_default_workspace_auto_grant)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Err(WebError::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
"local_access_policy_system_grant_readonly",
|
||||
"默认空间的系统授权不能撤销",
|
||||
));
|
||||
}
|
||||
state
|
||||
.control_plane()
|
||||
.revoke_directory_grant(grant_id, None)
|
||||
@@ -2710,7 +2755,16 @@ pub fn resolve_local_markdown_page_aggregate(
|
||||
})?;
|
||||
let parsed = parse_markdown_page(&markdown, &markdown_file.file_name);
|
||||
let title = parsed.title;
|
||||
let content = crate::routes::local_markdown_parser::markdown_to_blocks(&parsed.body);
|
||||
let attachment_paths = uploaded_asset_markdown_relative_paths_for_document(
|
||||
&canonical_root,
|
||||
&metadata,
|
||||
document_id,
|
||||
&markdown_file.path,
|
||||
);
|
||||
let content = crate::routes::local_markdown_parser::markdown_to_blocks_with_attachment_paths(
|
||||
&parsed.body,
|
||||
&attachment_paths,
|
||||
);
|
||||
let block_count = content.as_array().map(|blocks| blocks.len()).unwrap_or(0) as u64;
|
||||
let page_subtree = markdown_page_subtree(document_id, &title, &content);
|
||||
let workspace_id = local_workspace_id(&canonical_root);
|
||||
@@ -3122,7 +3176,28 @@ pub async fn read_local_resource(
|
||||
.unwrap_or("资源")
|
||||
.to_string();
|
||||
let content = if is_markdown_file(&file_name) {
|
||||
let metadata = load_local_folder_metadata(&root)?;
|
||||
let target_relative_path = normalize_relative_path(&root, &target)?;
|
||||
let attachment_paths = metadata
|
||||
.uploaded_assets
|
||||
.get(&target_relative_path)
|
||||
.map(|entry| {
|
||||
uploaded_asset_markdown_relative_paths_for_document(
|
||||
&root,
|
||||
&metadata,
|
||||
&entry.document_id,
|
||||
&target,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
if attachment_paths.is_empty() {
|
||||
crate::routes::local_markdown_parser::markdown_to_blocks(&text)
|
||||
} else {
|
||||
crate::routes::local_markdown_parser::markdown_to_blocks_with_attachment_paths(
|
||||
&text,
|
||||
&attachment_paths,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
text_to_editor_blocks(&text, &file_name)
|
||||
};
|
||||
@@ -6437,6 +6512,26 @@ fn find_markdown_by_page_id(
|
||||
walk(root, root, metadata, document_id)
|
||||
}
|
||||
|
||||
fn uploaded_asset_markdown_relative_paths_for_document(
|
||||
root: &Path,
|
||||
metadata: &LocalFolderMetadata,
|
||||
document_id: &str,
|
||||
markdown_path: &Path,
|
||||
) -> BTreeSet<String> {
|
||||
let Some(markdown_dir) = markdown_path.parent() else {
|
||||
return BTreeSet::new();
|
||||
};
|
||||
metadata
|
||||
.uploaded_assets
|
||||
.values()
|
||||
.filter(|entry| entry.document_id == document_id)
|
||||
.filter_map(|entry| {
|
||||
let path = root.join(&entry.relative_path);
|
||||
normalize_markdown_relative_asset_path(markdown_dir, &path).ok()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn local_folder_row_to_projection_item(row: &LocalFolderRow) -> Value {
|
||||
let mut item = json!({
|
||||
"rowId": row.row_id,
|
||||
@@ -7125,7 +7220,7 @@ fn editor_blocks_to_markdown_with_rewrite(
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("paragraph");
|
||||
let inline_markdown = block_content_value(&block)
|
||||
.map(inline_nodes_to_markdown)
|
||||
.map(|content| inline_nodes_to_markdown(content, local_file_context))
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_string();
|
||||
@@ -7319,7 +7414,11 @@ fn rewrite_local_open_url_to_markdown_relative(
|
||||
local_file_context: Option<(&Path, &Path)>,
|
||||
) -> Option<String> {
|
||||
let (root, markdown_path) = local_file_context?;
|
||||
let parsed = Url::parse(value).ok()?;
|
||||
let parsed = if value.starts_with('/') {
|
||||
Url::parse(&format!("http://localhost{}", value)).ok()?
|
||||
} else {
|
||||
Url::parse(value).ok()?
|
||||
};
|
||||
if parsed.path() != "/api/local-folder/files/open" {
|
||||
return None;
|
||||
}
|
||||
@@ -7605,7 +7704,7 @@ fn editor_block_table_to_markdown(block: &Value) -> String {
|
||||
.map(|cell| {
|
||||
let cell_text = cell
|
||||
.get("content")
|
||||
.map(inline_nodes_to_markdown)
|
||||
.map(|content| inline_nodes_to_markdown(content, None))
|
||||
.unwrap_or_default()
|
||||
.replace('\n', " ")
|
||||
.replace('|', r"\|")
|
||||
@@ -7715,29 +7814,33 @@ fn block_content_value(block: &Value) -> Option<&Value> {
|
||||
// 过渡实现:手写行内节点→Markdown 回写函数。从 editor block 的 content 数组中
|
||||
// 逐个节点提取 text + styles,按 legacy 样式格式输出为内联 Markdown。
|
||||
// AST + 中间 IR 迁移 complete 后应统一走 MarkdownInline→Markdown 反向映射。
|
||||
fn inline_nodes_to_markdown(value: &Value) -> String {
|
||||
fn inline_nodes_to_markdown(value: &Value, local_file_context: Option<(&Path, &Path)>) -> String {
|
||||
if let Some(text) = value.as_str() {
|
||||
return escape_markdown_inline_text(text);
|
||||
}
|
||||
if let Some(array) = value.as_array() {
|
||||
return array
|
||||
.iter()
|
||||
.map(inline_node_to_markdown)
|
||||
.map(|node| inline_node_to_markdown(node, local_file_context))
|
||||
.collect::<Vec<_>>()
|
||||
.join("");
|
||||
}
|
||||
if let Some(object) = value.as_object() {
|
||||
if let Some(text) = object.get("text").and_then(Value::as_str) {
|
||||
return markdown_text_with_styles(text, &inline_styles_from_object(object));
|
||||
return markdown_text_with_styles(
|
||||
text,
|
||||
&inline_styles_from_object(object),
|
||||
local_file_context,
|
||||
);
|
||||
}
|
||||
if let Some(content) = object.get("content").or_else(|| object.get("contentNodes")) {
|
||||
return inline_nodes_to_markdown(content);
|
||||
return inline_nodes_to_markdown(content, local_file_context);
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn inline_node_to_markdown(node: &Value) -> String {
|
||||
fn inline_node_to_markdown(node: &Value, local_file_context: Option<(&Path, &Path)>) -> String {
|
||||
if let Some(text) = node.as_str() {
|
||||
return escape_markdown_inline_text(text);
|
||||
}
|
||||
@@ -7747,10 +7850,14 @@ fn inline_node_to_markdown(node: &Value) -> String {
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
if !text.is_empty() {
|
||||
return markdown_text_with_styles(text, &inline_styles_from_object(object));
|
||||
return markdown_text_with_styles(
|
||||
text,
|
||||
&inline_styles_from_object(object),
|
||||
local_file_context,
|
||||
);
|
||||
}
|
||||
if let Some(content) = object.get("content").or_else(|| object.get("contentNodes")) {
|
||||
return inline_nodes_to_markdown(content);
|
||||
return inline_nodes_to_markdown(content, local_file_context);
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
@@ -7802,7 +7909,11 @@ fn inline_styles_from_object(object: &Map<String, Value>) -> Value {
|
||||
Value::Object(styles)
|
||||
}
|
||||
|
||||
fn markdown_text_with_styles(text: &str, styles: &Value) -> String {
|
||||
fn markdown_text_with_styles(
|
||||
text: &str,
|
||||
styles: &Value,
|
||||
local_file_context: Option<(&Path, &Path)>,
|
||||
) -> String {
|
||||
let mut value = escape_markdown_inline_text(text);
|
||||
let link = styles
|
||||
.get("link")
|
||||
@@ -7838,6 +7949,8 @@ fn markdown_text_with_styles(text: &str, styles: &Value) -> String {
|
||||
value = format!("~~{value}~~");
|
||||
}
|
||||
if let Some(href) = link {
|
||||
let href =
|
||||
rewrite_local_open_url_to_markdown_relative(&href, local_file_context).unwrap_or(href);
|
||||
value = format!("[{}]({href})", value.replace(']', r"\]"));
|
||||
}
|
||||
value
|
||||
@@ -7995,14 +8108,14 @@ mod tests {
|
||||
initialize_local_workspace_for_actor, load_local_folder_file_tree_snapshot,
|
||||
load_local_folder_page_tree_snapshot, local_folder_watch_revision,
|
||||
local_markdown_path_page_id, local_resource_write_editor_blocks, local_workspace_id,
|
||||
open_local_file, record_shared_cache, record_sync_pending_change,
|
||||
open_local_file, read_local_resource, record_shared_cache, record_sync_pending_change,
|
||||
resolve_local_markdown_page_aggregate, save_local_markdown_page,
|
||||
update_local_markdown_title, validate_local_access_root, write_local_markdown_asset,
|
||||
write_local_markdown_page_body, write_local_mindmap_data, write_local_resource,
|
||||
write_sync_conflict_report, LocalAccessGrantRequest, LocalAccessValidateRootRequest,
|
||||
LocalFileOpenQuery, LocalResourceWriteRequest, LocalShareGrantRequest,
|
||||
LocalShareLinkRequest, LocalUploadFile, ShareLinkListQuery, SharedCacheRecordRequest,
|
||||
SyncConflictReportRequest, SyncPendingChangeRequest,
|
||||
LocalFileOpenQuery, LocalResourceReadQuery, LocalResourceWriteRequest,
|
||||
LocalShareGrantRequest, LocalShareLinkRequest, LocalUploadFile, ShareLinkListQuery,
|
||||
SharedCacheRecordRequest, SyncConflictReportRequest, SyncPendingChangeRequest,
|
||||
};
|
||||
use crate::app::{AppConfig, AppState};
|
||||
use crate::context::RequestContext;
|
||||
@@ -8010,7 +8123,7 @@ mod tests {
|
||||
use axum::http::{HeaderMap, Method, StatusCode};
|
||||
use axum::Json;
|
||||
use control_plane::{DirectoryGrantInput, UpsertUserInput};
|
||||
use serde_json::Value;
|
||||
use serde_json::{json, Value};
|
||||
use std::sync::Mutex;
|
||||
|
||||
fn env_lock() -> &'static Mutex<()> {
|
||||
@@ -8452,6 +8565,193 @@ fn main() {}
|
||||
assert_eq!(media["props"]["sourcePath"], "attachments/spec.pdf");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_markdown_page_aggregate_preserves_uploaded_markdown_assets_as_media_blocks() {
|
||||
let root = temp_root("mnote-local-uploaded-md-assets-media");
|
||||
init_workspace(&root);
|
||||
std::fs::create_dir_all(root.join("Page")).expect("create page dir");
|
||||
std::fs::write(root.join("Page").join("Page.md"), "# Page\n").expect("write page");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
let document_id = "local-md:Page~2FPage.md";
|
||||
|
||||
let first = write_local_markdown_asset(
|
||||
&root_uri,
|
||||
document_id,
|
||||
"attachment",
|
||||
LocalUploadFile {
|
||||
name: "notes.md".to_string(),
|
||||
content_type: "text/markdown".to_string(),
|
||||
bytes: b"# Notes one\n".to_vec(),
|
||||
},
|
||||
)
|
||||
.expect("upload first md asset");
|
||||
let second = write_local_markdown_asset(
|
||||
&root_uri,
|
||||
document_id,
|
||||
"attachment",
|
||||
LocalUploadFile {
|
||||
name: "notes.md".to_string(),
|
||||
content_type: "text/markdown".to_string(),
|
||||
bytes: b"# Notes two\n".to_vec(),
|
||||
},
|
||||
)
|
||||
.expect("upload second md asset");
|
||||
assert_eq!(first["sourcePath"], "notes.md");
|
||||
assert_eq!(second["sourcePath"], "notes-1.md");
|
||||
|
||||
save_local_markdown_page(
|
||||
&root_uri,
|
||||
document_id,
|
||||
None,
|
||||
&json!([
|
||||
{"type":"heading","props":{"level":1},"content":[{"type":"text","text":"Page"}]},
|
||||
{"type":"media","props":{"name":"notes.md","sourcePath":"notes.md"}},
|
||||
{"type":"media","props":{"name":"notes-1.md","sourcePath":"notes-1.md"}}
|
||||
]),
|
||||
)
|
||||
.expect("save uploaded md links");
|
||||
|
||||
let aggregate =
|
||||
resolve_local_markdown_page_aggregate(&root_uri, document_id).expect("aggregate");
|
||||
let media_paths = aggregate
|
||||
.body
|
||||
.content
|
||||
.as_array()
|
||||
.expect("blocks")
|
||||
.iter()
|
||||
.filter(|block| block["type"].as_str() == Some("media"))
|
||||
.map(|block| {
|
||||
block["props"]["sourcePath"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.to_string()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(media_paths, vec!["notes.md", "notes-1.md"]);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_markdown_save_rewrites_uploaded_markdown_inline_link_as_media_path() {
|
||||
let root = temp_root("mnote-local-uploaded-md-inline-link");
|
||||
init_workspace(&root);
|
||||
std::fs::create_dir_all(root.join("Page")).expect("create page dir");
|
||||
std::fs::write(root.join("Page").join("Page.md"), "# Page\n").expect("write page");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
let document_id = "local-md:Page~2FPage.md";
|
||||
let asset = write_local_markdown_asset(
|
||||
&root_uri,
|
||||
document_id,
|
||||
"attachment",
|
||||
LocalUploadFile {
|
||||
name: "notes.md".to_string(),
|
||||
content_type: "text/markdown".to_string(),
|
||||
bytes: b"# Notes\n".to_vec(),
|
||||
},
|
||||
)
|
||||
.expect("upload md asset");
|
||||
let href = format!(
|
||||
"http://127.0.0.1:3000/api/local-folder/files/open?rootUri={}&path=Page%2Fnotes.md",
|
||||
root_uri
|
||||
);
|
||||
|
||||
save_local_markdown_page(
|
||||
&root_uri,
|
||||
document_id,
|
||||
None,
|
||||
&json!([
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": "notes.md",
|
||||
"marks": [{
|
||||
"type": "link",
|
||||
"attrs": {
|
||||
"href": href,
|
||||
"class": "mnote-uploaded-attachment"
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}
|
||||
]),
|
||||
)
|
||||
.expect("save inline attachment link");
|
||||
|
||||
assert_eq!(asset["sourcePath"], "notes.md");
|
||||
let saved = std::fs::read_to_string(root.join("Page").join("Page.md")).expect("read md");
|
||||
assert!(saved.contains("[notes.md](notes.md)"));
|
||||
assert!(!saved.contains("/api/local-folder/files/open"));
|
||||
|
||||
let aggregate =
|
||||
resolve_local_markdown_page_aggregate(&root_uri, document_id).expect("aggregate");
|
||||
let media = aggregate
|
||||
.body
|
||||
.content
|
||||
.as_array()
|
||||
.expect("blocks")
|
||||
.iter()
|
||||
.find(|block| block["type"].as_str() == Some("media"))
|
||||
.expect("uploaded md inline link should reload as media block");
|
||||
assert_eq!(media["props"]["sourcePath"], "notes.md");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_markdown_save_rewrites_uploaded_markdown_relative_open_url_as_media_path() {
|
||||
let root = temp_root("mnote-local-uploaded-md-relative-open-link");
|
||||
init_workspace(&root);
|
||||
std::fs::create_dir_all(root.join("Page")).expect("create page dir");
|
||||
std::fs::write(root.join("Page").join("Page.md"), "# Page\n").expect("write page");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
let document_id = "local-md:Page~2FPage.md";
|
||||
write_local_markdown_asset(
|
||||
&root_uri,
|
||||
document_id,
|
||||
"attachment",
|
||||
LocalUploadFile {
|
||||
name: "notes.md".to_string(),
|
||||
content_type: "text/markdown".to_string(),
|
||||
bytes: b"# Notes\n".to_vec(),
|
||||
},
|
||||
)
|
||||
.expect("upload md asset");
|
||||
let href = format!(
|
||||
"/api/local-folder/files/open?rootUri={}&path=Page%2Fnotes.md",
|
||||
root_uri
|
||||
);
|
||||
|
||||
save_local_markdown_page(
|
||||
&root_uri,
|
||||
document_id,
|
||||
None,
|
||||
&json!([
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": "notes.md",
|
||||
"marks": [{
|
||||
"type": "link",
|
||||
"attrs": {
|
||||
"href": href,
|
||||
"class": "mnote-uploaded-attachment-row"
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}
|
||||
]),
|
||||
)
|
||||
.expect("save relative open url");
|
||||
|
||||
let saved = std::fs::read_to_string(root.join("Page").join("Page.md")).expect("read md");
|
||||
assert!(saved.contains("[notes.md](notes.md)"));
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_markdown_save_preserves_frontmatter_and_writes_basic_blocks() {
|
||||
let root = temp_root("mnote-local-markdown-save-basic-blocks");
|
||||
@@ -9382,6 +9682,16 @@ fn main() {}
|
||||
assert_eq!(listed["grants"][0]["id"], grant_id);
|
||||
assert_eq!(listed["grants"][0]["permission"], "write");
|
||||
|
||||
let (_, Json(target_listed)) = get_user_access_policy(
|
||||
State(state.clone()),
|
||||
Extension(request_context("user_target", "user")),
|
||||
)
|
||||
.await
|
||||
.expect("target can list incoming directory grants");
|
||||
assert_eq!(target_listed["controlPlane"], "sqlite");
|
||||
assert_eq!(target_listed["grants"][0]["id"], grant_id);
|
||||
assert_eq!(target_listed["grants"][0]["userId"], "user_target");
|
||||
|
||||
let target_delete_error = delete_user_access_grant(
|
||||
State(state.clone()),
|
||||
Extension(request_context("user_target", "user")),
|
||||
@@ -9426,6 +9736,44 @@ fn main() {}
|
||||
let _ = std::fs::remove_dir_all(&outside_root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn user_access_policy_cannot_revoke_default_workspace_auto_grant() {
|
||||
let _guard = env_lock().lock().expect("env lock");
|
||||
let state = test_state();
|
||||
state
|
||||
.control_plane()
|
||||
.upsert_user(UpsertUserInput {
|
||||
id: Some("shujuan".into()),
|
||||
email: Some("shujuan@example.com".into()),
|
||||
username: "shujuan".into(),
|
||||
display_name: "shujuan".into(),
|
||||
role: None,
|
||||
password_hash: None,
|
||||
})
|
||||
.expect("upsert user");
|
||||
state
|
||||
.control_plane()
|
||||
.ensure_default_workspace("shujuan")
|
||||
.expect("default workspace");
|
||||
let grant_id = state
|
||||
.control_plane()
|
||||
.list_directory_grants()
|
||||
.expect("list grants")
|
||||
.into_iter()
|
||||
.find(|grant| grant.user_id == "shujuan" && grant.source == "auto")
|
||||
.map(|grant| grant.id)
|
||||
.expect("default auto grant");
|
||||
|
||||
let error = delete_user_access_grant(
|
||||
State(state.clone()),
|
||||
Extension(request_context("shujuan", "user")),
|
||||
AxumPath(grant_id),
|
||||
)
|
||||
.await
|
||||
.expect_err("default grant must be readonly");
|
||||
assert_eq!(error.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn admin_share_grant_allows_any_local_folder_with_minimal_fields() {
|
||||
let _guard = env_lock().lock().expect("env lock");
|
||||
@@ -10012,6 +10360,115 @@ fn main() {}
|
||||
assert!(markdown.contains("资源正文"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_resource_read_preserves_uploaded_markdown_attachment_links() {
|
||||
let root = temp_root("mnote-local-resource-read-uploaded-md-links");
|
||||
init_workspace(&root);
|
||||
std::fs::create_dir_all(root.join("Page")).expect("create page dir");
|
||||
std::fs::write(root.join("Page").join("Page.md"), "# Page\n").expect("write page");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
let document_id = "local-md:Page~2FPage.md";
|
||||
let resource = write_local_markdown_asset(
|
||||
&root_uri,
|
||||
document_id,
|
||||
"attachment",
|
||||
LocalUploadFile {
|
||||
name: "resource.md".to_string(),
|
||||
content_type: "text/markdown".to_string(),
|
||||
bytes: b"# Resource\n".to_vec(),
|
||||
},
|
||||
)
|
||||
.expect("upload resource markdown");
|
||||
let first = write_local_markdown_asset(
|
||||
&root_uri,
|
||||
document_id,
|
||||
"attachment",
|
||||
LocalUploadFile {
|
||||
name: "notes.md".to_string(),
|
||||
content_type: "text/markdown".to_string(),
|
||||
bytes: b"# Notes one\n".to_vec(),
|
||||
},
|
||||
)
|
||||
.expect("upload first linked markdown");
|
||||
let second = write_local_markdown_asset(
|
||||
&root_uri,
|
||||
document_id,
|
||||
"attachment",
|
||||
LocalUploadFile {
|
||||
name: "notes.md".to_string(),
|
||||
content_type: "text/markdown".to_string(),
|
||||
bytes: b"# Notes two\n".to_vec(),
|
||||
},
|
||||
)
|
||||
.expect("upload second linked markdown");
|
||||
assert_eq!(resource["sourcePath"], "resource.md");
|
||||
assert_eq!(first["sourcePath"], "notes.md");
|
||||
assert_eq!(second["sourcePath"], "notes-1.md");
|
||||
std::fs::write(
|
||||
root.join("Page").join("resource.md"),
|
||||
"# Resource\n\n[notes.md](notes.md)\n\n[notes-1.md](notes-1.md)\n",
|
||||
)
|
||||
.expect("write resource links");
|
||||
|
||||
let state = test_state();
|
||||
state
|
||||
.control_plane()
|
||||
.upsert_user(UpsertUserInput {
|
||||
id: Some("resource_reader".into()),
|
||||
email: Some("resource-reader@example.com".into()),
|
||||
username: "resource_reader".into(),
|
||||
display_name: "resource_reader".into(),
|
||||
role: None,
|
||||
password_hash: None,
|
||||
})
|
||||
.expect("upsert resource reader");
|
||||
state
|
||||
.control_plane()
|
||||
.grant_directory_access(DirectoryGrantInput {
|
||||
user_id: "resource_reader".into(),
|
||||
workspace_id: None,
|
||||
root_uri: root_uri.clone(),
|
||||
root_path: root
|
||||
.canonicalize()
|
||||
.expect("canonical root")
|
||||
.display()
|
||||
.to_string(),
|
||||
permission: "read".into(),
|
||||
recursive: true,
|
||||
capabilities: vec![],
|
||||
source: "test".into(),
|
||||
created_by: None,
|
||||
})
|
||||
.expect("grant read");
|
||||
let context = request_context("resource_reader", "user");
|
||||
let (_, _, payload) = read_local_resource(
|
||||
State(state),
|
||||
Extension(context),
|
||||
Query(LocalResourceReadQuery {
|
||||
root_uri: root_uri.clone(),
|
||||
path: "Page/resource.md".into(),
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("read resource markdown");
|
||||
|
||||
let media_paths = payload["result"]["content"]
|
||||
.as_array()
|
||||
.expect("content blocks")
|
||||
.iter()
|
||||
.filter(|block| block["type"].as_str() == Some("media"))
|
||||
.map(|block| {
|
||||
block["props"]["sourcePath"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.to_string()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(media_paths, vec!["notes.md", "notes-1.md"]);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_tree_command_delete_folder_moves_directory_to_trash() {
|
||||
let root = temp_root("mnote-local-delete-folder");
|
||||
@@ -10383,6 +10840,64 @@ fn main() {}
|
||||
let _ = std::fs::remove_dir_all(&policy_root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_file_open_allows_legacy_sqlite_directory_grant_with_path_root_uri() {
|
||||
let _guard = env_lock().lock().expect("env lock");
|
||||
let root = temp_root("mnote-local-file-open-legacy-sqlite-grant-root");
|
||||
let policy_root = temp_root("mnote-local-file-open-legacy-sqlite-grant-config");
|
||||
let policy_file = policy_root.join("missing-access-policy.json");
|
||||
let state = test_state();
|
||||
let canonical_root = root.canonicalize().expect("canonical root");
|
||||
let canonical_root_path = canonical_root.display().to_string();
|
||||
let root_uri = format!("file://{canonical_root_path}");
|
||||
std::fs::write(root.join("README.txt"), "hello legacy sqlite").expect("write file");
|
||||
std::env::set_var("MNOTE_LOCAL_ACCESS_POLICY_FILE", &policy_file);
|
||||
state
|
||||
.control_plane()
|
||||
.upsert_user(UpsertUserInput {
|
||||
id: Some("legacy_sqlite_reader".into()),
|
||||
email: Some("legacy-sqlite-reader@example.com".into()),
|
||||
username: "legacy_sqlite_reader".into(),
|
||||
display_name: "legacy_sqlite_reader".into(),
|
||||
role: None,
|
||||
password_hash: None,
|
||||
})
|
||||
.expect("upsert sqlite reader");
|
||||
state
|
||||
.control_plane()
|
||||
.grant_directory_access(DirectoryGrantInput {
|
||||
user_id: "legacy_sqlite_reader".into(),
|
||||
workspace_id: None,
|
||||
root_uri: canonical_root_path.clone(),
|
||||
root_path: canonical_root_path,
|
||||
permission: "read".into(),
|
||||
recursive: true,
|
||||
capabilities: vec![],
|
||||
source: "legacy-test".into(),
|
||||
created_by: None,
|
||||
})
|
||||
.expect("grant legacy sqlite read");
|
||||
|
||||
let context = request_context("legacy_sqlite_reader", "user");
|
||||
let (_, _, bytes) = open_local_file(
|
||||
State(state),
|
||||
Extension(context),
|
||||
Query(LocalFileOpenQuery {
|
||||
root_uri,
|
||||
path: "README.txt".into(),
|
||||
download: None,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("legacy sqlite read grant can open local file");
|
||||
assert_eq!(bytes, b"hello legacy sqlite");
|
||||
assert!(!policy_file.exists(), "SQLite grant 不应写旧 JSON policy");
|
||||
|
||||
std::env::remove_var("MNOTE_LOCAL_ACCESS_POLICY_FILE");
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
let _ = std::fs::remove_dir_all(&policy_root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_resource_write_allows_sqlite_directory_write_grant_without_json_policy() {
|
||||
let _guard = env_lock().lock().expect("env lock");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use comrak::nodes::{AstNode, ListType, NodeValue, TableAlignment};
|
||||
use comrak::{parse_document, Arena, Options};
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParsedLocalMarkdownPage {
|
||||
@@ -134,7 +135,14 @@ fn find_first_h1(body: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
pub fn markdown_to_blocks(markdown: &str) -> Value {
|
||||
markdown_ast_document_to_blocks(&parse_markdown_ast_document(markdown))
|
||||
markdown_to_blocks_with_attachment_paths(markdown, &BTreeSet::new())
|
||||
}
|
||||
|
||||
pub fn markdown_to_blocks_with_attachment_paths(
|
||||
markdown: &str,
|
||||
attachment_paths: &BTreeSet<String>,
|
||||
) -> Value {
|
||||
markdown_ast_document_to_blocks(&parse_markdown_ast_document(markdown, attachment_paths))
|
||||
}
|
||||
|
||||
fn markdown_options() -> Options<'static> {
|
||||
@@ -149,6 +157,13 @@ fn markdown_options() -> Options<'static> {
|
||||
}
|
||||
|
||||
pub fn parse_markdown_attachment_link(trimmed: &str) -> Option<(String, String)> {
|
||||
parse_markdown_attachment_link_with_paths(trimmed, &BTreeSet::new())
|
||||
}
|
||||
|
||||
fn parse_markdown_attachment_link_with_paths(
|
||||
trimmed: &str,
|
||||
attachment_paths: &BTreeSet<String>,
|
||||
) -> Option<(String, String)> {
|
||||
let value = trimmed
|
||||
.strip_prefix('!')
|
||||
.unwrap_or(trimmed)
|
||||
@@ -170,7 +185,9 @@ pub fn parse_markdown_attachment_link(trimmed: &str) -> Option<(String, String)>
|
||||
}
|
||||
let target_path = std::path::Path::new(target);
|
||||
let extension = target_path.extension().and_then(|value| value.to_str())?;
|
||||
if extension.eq_ignore_ascii_case("md") || extension.eq_ignore_ascii_case("markdown") {
|
||||
if (extension.eq_ignore_ascii_case("md") || extension.eq_ignore_ascii_case("markdown"))
|
||||
&& !attachment_paths.contains(target)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let fallback_name = target_path
|
||||
@@ -186,20 +203,27 @@ pub fn parse_markdown_attachment_link(trimmed: &str) -> Option<(String, String)>
|
||||
Some((name.to_string(), target.to_string()))
|
||||
}
|
||||
|
||||
fn parse_markdown_ast_document(markdown: &str) -> MarkdownAstDocument {
|
||||
fn parse_markdown_ast_document(
|
||||
markdown: &str,
|
||||
attachment_paths: &BTreeSet<String>,
|
||||
) -> MarkdownAstDocument {
|
||||
let arena = Arena::new();
|
||||
let options = markdown_options();
|
||||
let root = parse_document(&arena, markdown, &options);
|
||||
let mut blocks = Vec::new();
|
||||
for node in root.children() {
|
||||
append_ast_block(node, &mut blocks);
|
||||
append_ast_block(node, &mut blocks, attachment_paths);
|
||||
}
|
||||
MarkdownAstDocument { blocks }
|
||||
}
|
||||
|
||||
fn append_ast_block<'a>(node: &'a AstNode<'a>, blocks: &mut Vec<MarkdownBlock>) {
|
||||
fn append_ast_block<'a>(
|
||||
node: &'a AstNode<'a>,
|
||||
blocks: &mut Vec<MarkdownBlock>,
|
||||
attachment_paths: &BTreeSet<String>,
|
||||
) {
|
||||
match node.data.borrow().value.clone() {
|
||||
NodeValue::Paragraph => append_ast_paragraph(node, blocks),
|
||||
NodeValue::Paragraph => append_ast_paragraph(node, blocks, attachment_paths),
|
||||
NodeValue::Heading(heading) => blocks.push(MarkdownBlock::Heading {
|
||||
level: heading.level,
|
||||
content: collect_inline_children(node),
|
||||
@@ -212,7 +236,12 @@ fn append_ast_block<'a>(node: &'a AstNode<'a>, blocks: &mut Vec<MarkdownBlock>)
|
||||
}),
|
||||
NodeValue::List(list) => {
|
||||
for item in node.children() {
|
||||
append_ast_list_item(item, list.list_type == ListType::Ordered, blocks);
|
||||
append_ast_list_item(
|
||||
item,
|
||||
list.list_type == ListType::Ordered,
|
||||
blocks,
|
||||
attachment_paths,
|
||||
);
|
||||
}
|
||||
}
|
||||
NodeValue::Table(table) => blocks.push(ast_table_to_ir(node, table.alignments)),
|
||||
@@ -233,7 +262,11 @@ fn append_ast_block<'a>(node: &'a AstNode<'a>, blocks: &mut Vec<MarkdownBlock>)
|
||||
}
|
||||
}
|
||||
|
||||
fn append_ast_paragraph<'a>(node: &'a AstNode<'a>, blocks: &mut Vec<MarkdownBlock>) {
|
||||
fn append_ast_paragraph<'a>(
|
||||
node: &'a AstNode<'a>,
|
||||
blocks: &mut Vec<MarkdownBlock>,
|
||||
attachment_paths: &BTreeSet<String>,
|
||||
) {
|
||||
if let Some((alt, source_path)) = paragraph_image(node) {
|
||||
blocks.push(MarkdownBlock::Image { alt, source_path });
|
||||
return;
|
||||
@@ -242,11 +275,13 @@ fn append_ast_paragraph<'a>(node: &'a AstNode<'a>, blocks: &mut Vec<MarkdownBloc
|
||||
blocks.push(MarkdownBlock::Mindmap { name, source_path });
|
||||
return;
|
||||
}
|
||||
if let Some((name, source_path)) = paragraph_attachment_media(node) {
|
||||
if let Some((name, source_path)) = paragraph_attachment_media(node, attachment_paths) {
|
||||
blocks.push(MarkdownBlock::Media { name, source_path });
|
||||
return;
|
||||
}
|
||||
if let Some((name, source_path, remaining)) = paragraph_leading_attachment_media(node) {
|
||||
if let Some((name, source_path, remaining)) =
|
||||
paragraph_leading_attachment_media(node, attachment_paths)
|
||||
{
|
||||
blocks.push(MarkdownBlock::Media { name, source_path });
|
||||
let content = merge_adjacent_inline_nodes(remaining);
|
||||
if !content.is_empty() {
|
||||
@@ -257,17 +292,22 @@ fn append_ast_paragraph<'a>(node: &'a AstNode<'a>, blocks: &mut Vec<MarkdownBloc
|
||||
blocks.push(MarkdownBlock::Paragraph(collect_inline_children(node)));
|
||||
}
|
||||
|
||||
fn append_ast_list_item<'a>(node: &'a AstNode<'a>, ordered: bool, blocks: &mut Vec<MarkdownBlock>) {
|
||||
fn append_ast_list_item<'a>(
|
||||
node: &'a AstNode<'a>,
|
||||
ordered: bool,
|
||||
blocks: &mut Vec<MarkdownBlock>,
|
||||
attachment_paths: &BTreeSet<String>,
|
||||
) {
|
||||
let (is_task, checked) = match node.data.borrow().value.clone() {
|
||||
NodeValue::TaskItem(task_item) => (true, task_item.symbol.is_some()),
|
||||
NodeValue::Item(_) => (false, false),
|
||||
_ => return append_ast_block(node, blocks),
|
||||
_ => return append_ast_block(node, blocks, attachment_paths),
|
||||
};
|
||||
let mut content = Vec::new();
|
||||
for child in node.children() {
|
||||
match child.data.borrow().value.clone() {
|
||||
NodeValue::Paragraph => content.extend(collect_inline_children(child)),
|
||||
_ => append_ast_block(child, blocks),
|
||||
_ => append_ast_block(child, blocks, attachment_paths),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,13 +444,16 @@ fn merge_adjacent_inline_nodes(nodes: Vec<MarkdownInline>) -> Vec<MarkdownInline
|
||||
merged
|
||||
}
|
||||
|
||||
fn paragraph_attachment_media<'a>(node: &'a AstNode<'a>) -> Option<(String, String)> {
|
||||
fn paragraph_attachment_media<'a>(
|
||||
node: &'a AstNode<'a>,
|
||||
attachment_paths: &BTreeSet<String>,
|
||||
) -> Option<(String, String)> {
|
||||
let mut children = node.children();
|
||||
let first = children.next()?;
|
||||
if children.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
link_attachment_media(first)
|
||||
link_attachment_media(first, attachment_paths)
|
||||
}
|
||||
|
||||
fn paragraph_mindmap<'a>(node: &'a AstNode<'a>) -> Option<(String, String)> {
|
||||
@@ -437,10 +480,11 @@ fn paragraph_image<'a>(node: &'a AstNode<'a>) -> Option<(String, String)> {
|
||||
|
||||
fn paragraph_leading_attachment_media<'a>(
|
||||
node: &'a AstNode<'a>,
|
||||
attachment_paths: &BTreeSet<String>,
|
||||
) -> Option<(String, String, Vec<MarkdownInline>)> {
|
||||
let mut children = node.children();
|
||||
let first = children.next()?;
|
||||
let (name, source_path) = link_attachment_media(first)?;
|
||||
let (name, source_path) = link_attachment_media(first, attachment_paths)?;
|
||||
let second = children.next()?;
|
||||
if !matches!(
|
||||
second.data.borrow().value,
|
||||
@@ -455,11 +499,17 @@ fn paragraph_leading_attachment_media<'a>(
|
||||
Some((name, source_path, remaining))
|
||||
}
|
||||
|
||||
fn link_attachment_media<'a>(node: &'a AstNode<'a>) -> Option<(String, String)> {
|
||||
fn link_attachment_media<'a>(
|
||||
node: &'a AstNode<'a>,
|
||||
attachment_paths: &BTreeSet<String>,
|
||||
) -> Option<(String, String)> {
|
||||
let NodeValue::Link(link) = &node.data.borrow().value else {
|
||||
return None;
|
||||
};
|
||||
parse_markdown_attachment_link(&format!("[{}]({})", collect_plain_text(node), link.url))
|
||||
parse_markdown_attachment_link_with_paths(
|
||||
&format!("[{}]({})", collect_plain_text(node), link.url),
|
||||
attachment_paths,
|
||||
)
|
||||
}
|
||||
|
||||
fn link_mindmap<'a>(node: &'a AstNode<'a>) -> Option<(String, String)> {
|
||||
|
||||
@@ -3,8 +3,8 @@ use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::routes::command_support::execute_runtime_command_via_convex_with_artifacts;
|
||||
use crate::routes::local_folder_source::{
|
||||
ensure_local_workspace_access, ensure_local_workspace_read_access, read_local_mindmap_data,
|
||||
write_local_mindmap_data,
|
||||
ensure_local_workspace_read_access_with_state, ensure_local_workspace_write_access_with_state,
|
||||
read_local_mindmap_data, write_local_mindmap_data,
|
||||
};
|
||||
use crate::routes::query_support::{
|
||||
execute_runtime_query_against_data, execute_runtime_query_via_convex,
|
||||
@@ -151,7 +151,7 @@ pub async fn get_mindmap(
|
||||
}
|
||||
if is_local_folder_source(params.source_kind.as_deref()) {
|
||||
let root_uri = local_root_uri(params.root_uri.as_deref(), None)?;
|
||||
ensure_local_workspace_read_access(&context, root_uri)
|
||||
ensure_local_workspace_read_access_with_state(&state, &context, root_uri)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
let data = read_local_mindmap_data(root_uri, document_id, mindmap_id)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
@@ -228,7 +228,7 @@ pub async fn apply_mindmap_command(
|
||||
.or(params.source_kind.as_deref());
|
||||
if is_local_folder_source(local_source_kind) {
|
||||
let root_uri = local_root_uri(params.root_uri.as_deref(), body.root_uri.as_deref())?;
|
||||
ensure_local_workspace_access(&context, root_uri)
|
||||
ensure_local_workspace_write_access_with_state(&state, &context, root_uri)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
if command_name == Some("mindmap.command.apply") {
|
||||
let current = read_local_mindmap_data(root_uri, document_id, mindmap_id)
|
||||
|
||||
@@ -1268,7 +1268,9 @@ mod tests {
|
||||
std::env::temp_dir().join(format!("mnote-local-mindmap-trash-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
std::fs::create_dir_all(root.join("Page")).expect("create page");
|
||||
std::fs::write(root.join("Page").join("Page.md"), "# Page\n").expect("write md");
|
||||
let markdown_path = root.join("Page").join("Page.md");
|
||||
let original_markdown = "# Page\n\n[map](map.mindmap.json)\n";
|
||||
std::fs::write(&markdown_path, original_markdown).expect("write md");
|
||||
std::fs::write(
|
||||
root.join("Page").join("map.mindmap.json"),
|
||||
r#"{"data":{"uid":"root","text":"KMIND"},"children":[]}"#,
|
||||
@@ -1313,6 +1315,11 @@ mod tests {
|
||||
.join("trash")
|
||||
.join("map.mindmap.json")
|
||||
.exists());
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(&markdown_path).expect("read markdown after trash"),
|
||||
original_markdown,
|
||||
"mindmap archive 只移动资源文件,不应改写正文引用"
|
||||
);
|
||||
let trash_index = std::fs::read_to_string(root.join(".mnote").join("trash-index.json"))
|
||||
.expect("trash index");
|
||||
assert!(trash_index.contains("local-file:Page/map.mindmap.json"));
|
||||
@@ -1354,6 +1361,11 @@ mod tests {
|
||||
"tree.resource.restore"
|
||||
);
|
||||
assert!(root.join("Page").join("map.mindmap.json").exists());
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(&markdown_path).expect("read markdown after restore"),
|
||||
original_markdown,
|
||||
"mindmap restore 只恢复资源文件,不应改写正文引用"
|
||||
);
|
||||
|
||||
let delete_again = app()
|
||||
.oneshot(
|
||||
|
||||
@@ -179,7 +179,9 @@ pub async fn documents(
|
||||
WebError::bad_request_code("local_search_root_required", "本地搜索缺少 rootUri")
|
||||
.with_context(&context)
|
||||
})?;
|
||||
let root_path = local_folder_source::ensure_local_workspace_read_access(&context, root_uri)
|
||||
let root_path = local_folder_source::ensure_local_workspace_read_access_with_state(
|
||||
&state, &context, root_uri,
|
||||
)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
local_search_index::query_local_search_index(
|
||||
&root_path,
|
||||
@@ -227,6 +229,7 @@ pub async fn documents(
|
||||
}
|
||||
|
||||
pub async fn refresh_local_index(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Json(body): Json<LocalSearchIndexRefreshRequest>,
|
||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||
@@ -241,7 +244,9 @@ pub async fn refresh_local_index(
|
||||
)
|
||||
.with_context(&context));
|
||||
}
|
||||
let root_path = local_folder_source::ensure_local_workspace_read_access(&context, root_uri)
|
||||
let root_path = local_folder_source::ensure_local_workspace_read_access_with_state(
|
||||
&state, &context, root_uri,
|
||||
)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
let refreshed = local_search_index::refresh_local_search_index(
|
||||
&root_path,
|
||||
@@ -268,6 +273,7 @@ pub async fn refresh_local_index(
|
||||
}
|
||||
|
||||
pub async fn local_index_backlinks(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Query(query): Query<LocalSearchIndexQuery>,
|
||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||
@@ -294,7 +300,9 @@ pub async fn local_index_backlinks(
|
||||
)
|
||||
.with_context(&context)
|
||||
})?;
|
||||
let root_path = local_folder_source::ensure_local_workspace_read_access(&context, root_uri)
|
||||
let root_path = local_folder_source::ensure_local_workspace_read_access_with_state(
|
||||
&state, &context, root_uri,
|
||||
)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
let backlinks = local_search_index::query_local_backlinks(
|
||||
&root_path,
|
||||
@@ -322,6 +330,7 @@ pub async fn local_index_backlinks(
|
||||
}
|
||||
|
||||
pub async fn local_index_tags(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Query(query): Query<LocalSearchIndexQuery>,
|
||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||
@@ -336,7 +345,9 @@ pub async fn local_index_tags(
|
||||
)
|
||||
.with_context(&context));
|
||||
}
|
||||
let root_path = local_folder_source::ensure_local_workspace_read_access(&context, root_uri)
|
||||
let root_path = local_folder_source::ensure_local_workspace_read_access_with_state(
|
||||
&state, &context, root_uri,
|
||||
)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
let tags = local_search_index::query_local_tags(&root_path, root_uri, &effective_workspace_id)?;
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
@@ -78,10 +78,10 @@ fn build_session_response(state: &AppState, context: RequestContext) -> SessionR
|
||||
return SessionResponse {
|
||||
ok: true,
|
||||
owner: "mnote-web",
|
||||
user_id: resolved.user.id,
|
||||
user_id: resolved.user.id.clone(),
|
||||
email: resolved.user.email.unwrap_or_default(),
|
||||
name: resolved.user.display_name,
|
||||
actor_type: "user".to_string(),
|
||||
actor_type: effective_actor_type_for_user(&resolved.user.id, &resolved.user.role),
|
||||
auth_mode: "sqliteSession",
|
||||
request_id: context.trace.request_id,
|
||||
trace_id: context.trace.trace_id,
|
||||
@@ -109,7 +109,7 @@ fn build_session_response(state: &AppState, context: RequestContext) -> SessionR
|
||||
state.config().dev_user_id.clone()
|
||||
};
|
||||
let actor_type = if has_forwarded_actor {
|
||||
context.auth.actor_type
|
||||
effective_actor_type_for_user(actor_id, &context.auth.actor_type)
|
||||
} else {
|
||||
"devFallback".to_string()
|
||||
};
|
||||
@@ -139,6 +139,19 @@ fn build_session_response(state: &AppState, context: RequestContext) -> SessionR
|
||||
}
|
||||
}
|
||||
|
||||
fn effective_actor_type_for_user(user_id: &str, stored_role: &str) -> String {
|
||||
let role = stored_role.trim();
|
||||
let fallback_role = if role.is_empty() { "user" } else { role };
|
||||
if crate::routes::local_folder_source::is_local_access_policy_admin_actor(
|
||||
user_id,
|
||||
fallback_role,
|
||||
) {
|
||||
"admin".to_string()
|
||||
} else {
|
||||
fallback_role.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn jwt_cookie_claim(context: &RequestContext, keys: &[&str]) -> Option<String> {
|
||||
let token = context
|
||||
.auth
|
||||
@@ -376,6 +389,155 @@ mod tests {
|
||||
assert_eq!(payload["authMode"], "sqliteSession");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_returns_admin_for_sqlite_admin_user() {
|
||||
let state = AppState::new(AppConfig {
|
||||
service_name: "mnote-web".into(),
|
||||
service_version: "0.1.0".into(),
|
||||
bind_addr: "127.0.0.1:0".into(),
|
||||
public_bind_addr: "127.0.0.1:3000".into(),
|
||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
||||
enable_legacy_next_compat: true,
|
||||
enable_debug_shell_routes: false,
|
||||
enable_editor_actor: true,
|
||||
hermes_base_path: "/api/hermes".into(),
|
||||
compat_next_base_path: "/api/compat/next".into(),
|
||||
convex_url: None,
|
||||
convex_admin_key: None,
|
||||
allow_dev_fixtures: true,
|
||||
query_fixtures_json: None,
|
||||
mutation_fixtures_json: None,
|
||||
dev_user_id: "dev-user".into(),
|
||||
dev_user_name: "开发用户".into(),
|
||||
dev_user_email: "dev@mnote.local".into(),
|
||||
});
|
||||
state
|
||||
.control_plane()
|
||||
.upsert_user(UpsertUserInput {
|
||||
id: Some("liaibo".into()),
|
||||
email: Some("liaibo@yeah.net".into()),
|
||||
username: "liaibo".into(),
|
||||
display_name: "liaibo".into(),
|
||||
role: Some("admin".into()),
|
||||
password_hash: None,
|
||||
})
|
||||
.expect("upsert admin user");
|
||||
state
|
||||
.control_plane()
|
||||
.create_session(CreateSessionInput {
|
||||
id: None,
|
||||
user_id: "liaibo".into(),
|
||||
token_hash: session_token_hash("raw-admin-session-token"),
|
||||
user_agent: None,
|
||||
ip_hash: None,
|
||||
expires_at: None,
|
||||
})
|
||||
.expect("create admin session");
|
||||
|
||||
let response = build_app(state)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/auth/session")
|
||||
.header("cookie", "mnote_session=raw-admin-session-token")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["userId"], "liaibo");
|
||||
assert_eq!(payload["actorType"], "admin");
|
||||
assert_eq!(payload["authMode"], "sqliteSession");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_returns_admin_for_local_access_policy_admin() {
|
||||
let _guard = crate::test_support::hermes_env_lock()
|
||||
.lock()
|
||||
.expect("env lock");
|
||||
let policy_root = std::env::temp_dir().join(format!(
|
||||
"mnote-session-access-policy-admin-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::remove_dir_all(&policy_root);
|
||||
std::fs::create_dir_all(&policy_root).expect("create policy root");
|
||||
let policy_file = policy_root.join("access-policy.json");
|
||||
std::fs::write(&policy_file, r#"{"admins":["liaibo"],"grants":[]}"#)
|
||||
.expect("write access policy");
|
||||
std::env::set_var("MNOTE_LOCAL_ACCESS_POLICY_FILE", &policy_file);
|
||||
|
||||
let state = AppState::new(AppConfig {
|
||||
service_name: "mnote-web".into(),
|
||||
service_version: "0.1.0".into(),
|
||||
bind_addr: "127.0.0.1:0".into(),
|
||||
public_bind_addr: "127.0.0.1:3000".into(),
|
||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
||||
enable_legacy_next_compat: true,
|
||||
enable_debug_shell_routes: false,
|
||||
enable_editor_actor: true,
|
||||
hermes_base_path: "/api/hermes".into(),
|
||||
compat_next_base_path: "/api/compat/next".into(),
|
||||
convex_url: None,
|
||||
convex_admin_key: None,
|
||||
allow_dev_fixtures: true,
|
||||
query_fixtures_json: None,
|
||||
mutation_fixtures_json: None,
|
||||
dev_user_id: "dev-user".into(),
|
||||
dev_user_name: "开发用户".into(),
|
||||
dev_user_email: "dev@mnote.local".into(),
|
||||
});
|
||||
state
|
||||
.control_plane()
|
||||
.upsert_user(UpsertUserInput {
|
||||
id: Some("liaibo".into()),
|
||||
email: Some("liaibo@yeah.net".into()),
|
||||
username: "liaibo".into(),
|
||||
display_name: "liaibo".into(),
|
||||
role: None,
|
||||
password_hash: None,
|
||||
})
|
||||
.expect("upsert policy admin user");
|
||||
state
|
||||
.control_plane()
|
||||
.create_session(CreateSessionInput {
|
||||
id: None,
|
||||
user_id: "liaibo".into(),
|
||||
token_hash: session_token_hash("raw-policy-admin-session-token"),
|
||||
user_agent: None,
|
||||
ip_hash: None,
|
||||
expires_at: None,
|
||||
})
|
||||
.expect("create policy admin session");
|
||||
|
||||
let response = build_app(state)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/auth/session")
|
||||
.header("cookie", "mnote_session=raw-policy-admin-session-token")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
std::env::remove_var("MNOTE_LOCAL_ACCESS_POLICY_FILE");
|
||||
let _ = std::fs::remove_dir_all(&policy_root);
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["userId"], "liaibo");
|
||||
assert_eq!(payload["actorType"], "admin");
|
||||
assert_eq!(payload["authMode"], "sqliteSession");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn legacy_whoami_alias_prefers_forwarded_actor_identity() {
|
||||
let response = app()
|
||||
|
||||
@@ -6,10 +6,10 @@ use crate::routes::command_support::{
|
||||
execute_runtime_command_via_convex_with_artifacts, read_optional_non_empty,
|
||||
};
|
||||
use crate::routes::local_folder_source::{
|
||||
ensure_local_workspace_access, ensure_local_workspace_read_access,
|
||||
ensure_local_workspace_access_with_state, ensure_local_workspace_read_access_with_state,
|
||||
execute_local_tree_command_with_sort, load_local_folder_file_tree_snapshot,
|
||||
load_local_folder_page_tree_snapshot, local_folder_watch_revision,
|
||||
local_workspace_id_from_root_uri,
|
||||
local_workspace_id_from_root_uri, LocalAccessMode,
|
||||
};
|
||||
use crate::routes::query_support::{
|
||||
fetch_documents_meta_via_convex, resolve_effective_workspace_id,
|
||||
@@ -6084,10 +6084,11 @@ fn json_response(context: &RequestContext, result: Value) -> (StatusCode, Json<V
|
||||
}
|
||||
|
||||
pub async fn local_folder_watch(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Query(query): Query<LocalFolderWatchQuery>,
|
||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||
ensure_local_workspace_read_access(&context, &query.root_uri)
|
||||
ensure_local_workspace_read_access_with_state(&state, &context, &query.root_uri)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
let revision = local_folder_watch_revision(&query.root_uri)?;
|
||||
Ok(json_response(
|
||||
@@ -6185,7 +6186,7 @@ pub async fn tree_shell(
|
||||
.ok_or_else(|| {
|
||||
WebError::bad_request_code("local_folder_root_required", "缺少本地文件夹 rootUri")
|
||||
})?;
|
||||
ensure_local_workspace_read_access(&effective_context, root_uri)
|
||||
ensure_local_workspace_read_access_with_state(&state, &effective_context, root_uri)
|
||||
.map_err(|error| error.with_context(&effective_context))?;
|
||||
(
|
||||
local_workspace_id_from_root_uri(root_uri)?,
|
||||
@@ -6808,7 +6809,12 @@ pub async fn tree_command(
|
||||
WebError::bad_request_code("local_folder_root_required", "缺少本地文件夹 rootUri")
|
||||
.with_context(&context)
|
||||
})?;
|
||||
ensure_local_workspace_access(&context, root_uri)
|
||||
ensure_local_workspace_access_with_state(
|
||||
&state,
|
||||
&context,
|
||||
root_uri,
|
||||
LocalAccessMode::Write,
|
||||
)
|
||||
.map_err(|error| error.with_context(&context))?;
|
||||
let execution = execute_local_tree_command_with_sort(
|
||||
root_uri,
|
||||
@@ -6932,6 +6938,7 @@ mod tests {
|
||||
use crate::routes::command_support::build_runtime_command_plan;
|
||||
use axum::body::Body;
|
||||
use axum::http::{HeaderMap, HeaderValue, Method, Request, StatusCode, Uri};
|
||||
use control_plane::{DirectoryGrantInput, UpsertUserInput};
|
||||
use serde_json::Value;
|
||||
use tower::util::ServiceExt;
|
||||
|
||||
@@ -7223,6 +7230,88 @@ mod tests {
|
||||
assert!(html.contains("data-document-id=\"local-md:README.md\""));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tree_shell_local_folder_allows_sqlite_directory_read_grant() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-local-folder-sqlite-read-grant-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
std::fs::create_dir_all(&root).expect("create local root");
|
||||
std::fs::write(root.join("README.md"), "# Shared\n").expect("write local md");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
init_local_workspace(&root, "owner_user");
|
||||
|
||||
let state = AppState::new(AppConfig {
|
||||
service_name: "mnote-web".into(),
|
||||
service_version: "0.1.0".into(),
|
||||
bind_addr: "127.0.0.1:0".into(),
|
||||
public_bind_addr: "127.0.0.1:3000".into(),
|
||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
||||
enable_legacy_next_compat: true,
|
||||
enable_debug_shell_routes: true,
|
||||
enable_editor_actor: true,
|
||||
hermes_base_path: "/api/hermes".into(),
|
||||
compat_next_base_path: "/api/compat/next".into(),
|
||||
convex_url: None,
|
||||
convex_admin_key: None,
|
||||
allow_dev_fixtures: true,
|
||||
query_fixtures_json: None,
|
||||
mutation_fixtures_json: None,
|
||||
dev_user_id: "dev-user".into(),
|
||||
dev_user_name: "开发用户".into(),
|
||||
dev_user_email: "dev@mnote.local".into(),
|
||||
});
|
||||
for (user_id, role) in [("shujuan", None), ("liaibo", Some("admin"))] {
|
||||
state
|
||||
.control_plane()
|
||||
.upsert_user(UpsertUserInput {
|
||||
id: Some(user_id.into()),
|
||||
email: Some(format!("{user_id}@example.com")),
|
||||
username: user_id.into(),
|
||||
display_name: user_id.into(),
|
||||
role: role.map(str::to_string),
|
||||
password_hash: None,
|
||||
})
|
||||
.expect("upsert grant user");
|
||||
}
|
||||
state
|
||||
.control_plane()
|
||||
.grant_directory_access(DirectoryGrantInput {
|
||||
user_id: "shujuan".into(),
|
||||
workspace_id: None,
|
||||
root_uri: root_uri.clone(),
|
||||
root_path: root
|
||||
.canonicalize()
|
||||
.expect("canonical root")
|
||||
.display()
|
||||
.to_string(),
|
||||
permission: "read".into(),
|
||||
recursive: true,
|
||||
capabilities: vec![],
|
||||
source: "admin".into(),
|
||||
created_by: Some("liaibo".into()),
|
||||
})
|
||||
.expect("grant sqlite read");
|
||||
|
||||
let response = build_app(state)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri(format!(
|
||||
"/tree?mode=filetree&sourceKind=local_folder&rootUri={root_uri}"
|
||||
))
|
||||
.header("x-mnote-actor-id", "shujuan")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_folder_tree_shell_does_not_reload_page_for_refresh() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
@@ -7778,6 +7867,89 @@ mod tests {
|
||||
assert_eq!(payload["code"], "local_workspace_access_denied");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tree_command_local_folder_allows_sqlite_directory_write_grant() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-local-tree-sqlite-write-grant-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
std::fs::create_dir_all(&root).expect("create local root");
|
||||
init_local_workspace(&root, "owner_user");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
|
||||
let state = AppState::new(AppConfig {
|
||||
service_name: "mnote-web".into(),
|
||||
service_version: "0.1.0".into(),
|
||||
bind_addr: "127.0.0.1:0".into(),
|
||||
public_bind_addr: "127.0.0.1:3000".into(),
|
||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
||||
enable_legacy_next_compat: true,
|
||||
enable_debug_shell_routes: true,
|
||||
enable_editor_actor: true,
|
||||
hermes_base_path: "/api/hermes".into(),
|
||||
compat_next_base_path: "/api/compat/next".into(),
|
||||
convex_url: None,
|
||||
convex_admin_key: None,
|
||||
allow_dev_fixtures: true,
|
||||
query_fixtures_json: None,
|
||||
mutation_fixtures_json: None,
|
||||
dev_user_id: "dev-user".into(),
|
||||
dev_user_name: "开发用户".into(),
|
||||
dev_user_email: "dev@mnote.local".into(),
|
||||
});
|
||||
for (user_id, role) in [("shujuan", None), ("liaibo", Some("admin"))] {
|
||||
state
|
||||
.control_plane()
|
||||
.upsert_user(UpsertUserInput {
|
||||
id: Some(user_id.into()),
|
||||
email: Some(format!("{user_id}@example.com")),
|
||||
username: user_id.into(),
|
||||
display_name: user_id.into(),
|
||||
role: role.map(str::to_string),
|
||||
password_hash: None,
|
||||
})
|
||||
.expect("upsert grant user");
|
||||
}
|
||||
state
|
||||
.control_plane()
|
||||
.grant_directory_access(DirectoryGrantInput {
|
||||
user_id: "shujuan".into(),
|
||||
workspace_id: None,
|
||||
root_uri: root_uri.clone(),
|
||||
root_path: root
|
||||
.canonicalize()
|
||||
.expect("canonical root")
|
||||
.display()
|
||||
.to_string(),
|
||||
permission: "write".into(),
|
||||
recursive: true,
|
||||
capabilities: vec![],
|
||||
source: "admin".into(),
|
||||
created_by: Some("liaibo".into()),
|
||||
})
|
||||
.expect("grant sqlite write");
|
||||
|
||||
let response = build_app(state)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri("/api/tree/commands")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-mnote-actor-id", "shujuan")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::from(format!(
|
||||
r#"{{"action":"create","sourceKind":"local_folder","rootUri":"{root_uri}","title":"授权新页面"}}"#
|
||||
)))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tree_shell_embeds_renderer_input_contract() {
|
||||
let filetree_response = app()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -111,6 +111,7 @@ pub(crate) const ADMIN_POLICY_SCRIPT: &str = r#"
|
||||
var deleteShareGrantResult = root.querySelector('[data-testid="mnote-admin-delete-share-grant-result"]');
|
||||
var refreshShareGrantsButton = root.querySelector('[data-admin-action="refresh-share-grants"]');
|
||||
var shareGrantsList = root.querySelector('[data-testid="mnote-admin-share-grants-list"]');
|
||||
var currentActorId = document.body && document.body.getAttribute ? String(document.body.getAttribute('data-mnote-actor-id') || '').trim() : '';
|
||||
var pageConfig = (function () {
|
||||
var node = root.querySelector('#__MNOTE_ACCESS_POLICY_PAGE__');
|
||||
try { return JSON.parse(node ? node.textContent || '{}' : '{}'); } catch (_) { return {}; }
|
||||
@@ -147,6 +148,48 @@ pub(crate) const ADMIN_POLICY_SCRIPT: &str = r#"
|
||||
return suffix ? base + suffix : base;
|
||||
}
|
||||
|
||||
function pathToFileRootUri(value) {
|
||||
var trimmed = String(value || '').trim();
|
||||
if (!trimmed) return '';
|
||||
if (/^file:\/\//i.test(trimmed)) return trimmed;
|
||||
if (trimmed.charAt(0) !== '/') return '';
|
||||
return 'file://' + trimmed.split('/').map(function(part, index) {
|
||||
return index === 0 ? '' : encodeURIComponent(part);
|
||||
}).join('/');
|
||||
}
|
||||
|
||||
function localFolderOpenHref(grant) {
|
||||
var rootUri = String(grant.rootUri || '').trim();
|
||||
if (rootUri && !/^file:\/\//i.test(rootUri)) rootUri = '';
|
||||
if (!rootUri) {
|
||||
var rootPath = String(grant.rootPath || '').trim();
|
||||
if (rootPath) rootUri = pathToFileRootUri(rootPath);
|
||||
}
|
||||
if (!rootUri) {
|
||||
rootUri = pathToFileRootUri(String(grant.rootUri || '').trim());
|
||||
}
|
||||
if (!rootUri) return '';
|
||||
var url = new URL('/', window.location.origin);
|
||||
url.searchParams.set('treeView', 'filetree');
|
||||
url.searchParams.set('sourceKind', 'local_folder');
|
||||
url.searchParams.set('rootUri', rootUri);
|
||||
return url.pathname + url.search;
|
||||
}
|
||||
|
||||
function isDefaultWorkspaceAutoGrant(grant) {
|
||||
var source = String(grant && grant.source || '').trim();
|
||||
var workspaceId = String(grant && grant.workspaceId || '').trim();
|
||||
var rootUri = String(grant && grant.rootUri || '').trim();
|
||||
var permission = String(grant && grant.permission || '').trim();
|
||||
var createdBy = String(grant && (grant.createdBy || grant.ownerUserId) || '').trim();
|
||||
var targetUser = String(grant && (grant.userId || grant.targetUserId) || '').trim();
|
||||
return source === 'auto'
|
||||
&& workspaceId
|
||||
&& permission === 'write'
|
||||
&& createdBy === targetUser
|
||||
&& /^local:\/\/users\/.+\/workspaces\/my-space$/.test(rootUri);
|
||||
}
|
||||
|
||||
function renderShareGrants(payload) {
|
||||
if (!shareGrantsList) return;
|
||||
var grants = readDirectoryGrants(payload);
|
||||
@@ -156,12 +199,20 @@ pub(crate) const ADMIN_POLICY_SCRIPT: &str = r#"
|
||||
}
|
||||
shareGrantsList.innerHTML = grants.map(function(grant) {
|
||||
var active = grant.active === false || grant.status === 'revoked' ? '已撤销' : '有效';
|
||||
var revokeButton = grant.active === false ? '' :
|
||||
var openHref = localFolderOpenHref(grant);
|
||||
var rootLabel = escapeHtml(grant.rootPath || grant.rootUri || '');
|
||||
var createdBy = String(grant.createdBy || grant.ownerUserId || '').trim();
|
||||
var systemOwned = isDefaultWorkspaceAutoGrant(grant);
|
||||
var canRevoke = !systemOwned && (isAdmin || (createdBy && currentActorId && createdBy === currentActorId));
|
||||
var rootNode = openHref
|
||||
? '<a class="mnote-admin-policy-root-link" data-testid="mnote-admin-open-granted-root" href="' + escapeHtml(openHref) + '">' + rootLabel + '</a>'
|
||||
: '<strong>' + rootLabel + '</strong>';
|
||||
var revokeButton = grant.active === false || !canRevoke ? '' :
|
||||
'<button type="button" data-admin-action="revoke-access-grant" data-grant-id="' + escapeHtml(grant.id || '') + '">撤销</button>';
|
||||
return '<article class="mnote-admin-policy-row">' +
|
||||
'<div><strong>' + escapeHtml(grant.rootPath || grant.rootUri || '') + '</strong><span>授权用户 ' + escapeHtml(grant.targetUserId || grant.userId || '') + '</span></div>' +
|
||||
'<div>' + rootNode + '<span>授权用户 ' + escapeHtml(grant.targetUserId || grant.userId || '') + '</span></div>' +
|
||||
'<div>' + renderBadge(grant.permission) + renderBadge(active) + '</div>' +
|
||||
'<div><span>创建者 ' + escapeHtml(grant.createdBy || grant.ownerUserId || '') + '</span><span>授权 ID ' + escapeHtml(grant.id || '') + '</span>' + revokeButton + '</div>' +
|
||||
'<div><span>创建者 ' + escapeHtml(createdBy) + '</span><span>授权 ID ' + escapeHtml(grant.id || '') + '</span>' + revokeButton + '</div>' +
|
||||
'</article>';
|
||||
}).join('');
|
||||
}
|
||||
@@ -203,7 +254,8 @@ pub(crate) const ADMIN_POLICY_SCRIPT: &str = r#"
|
||||
|
||||
root.querySelector('[data-admin-form="create-share-grant"]').addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
var values = shareGrantFormValues(event.currentTarget);
|
||||
var form = event.currentTarget;
|
||||
var values = shareGrantFormValues(form);
|
||||
setText(createShareGrantResult, '正在创建...');
|
||||
requestJson(accessPolicyUrl('/grants'), {
|
||||
method: 'POST',
|
||||
@@ -211,7 +263,7 @@ pub(crate) const ADMIN_POLICY_SCRIPT: &str = r#"
|
||||
}).then(function (payload) {
|
||||
setText(createShareGrantResult, payload);
|
||||
setText(shareGrantsMessage, '文件夹授权已创建');
|
||||
event.currentTarget.reset();
|
||||
form.reset();
|
||||
return refreshShareGrants();
|
||||
}).catch(function (error) {
|
||||
setText(createShareGrantResult, { ok: false, error: error.message || '创建失败' });
|
||||
@@ -292,4 +344,20 @@ mod tests {
|
||||
assert!(!html.contains("capabilities"));
|
||||
assert!(!html.contains("mnote-admin-validate-root-submit"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_policy_script_keeps_form_reference_for_async_reset() {
|
||||
assert!(ADMIN_POLICY_SCRIPT.contains("var form = event.currentTarget;"));
|
||||
assert!(ADMIN_POLICY_SCRIPT.contains("form.reset();"));
|
||||
assert!(!ADMIN_POLICY_SCRIPT.contains("event.currentTarget.reset();"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_policy_script_links_grants_to_local_folder_entry() {
|
||||
assert!(ADMIN_POLICY_SCRIPT.contains("function localFolderOpenHref(grant)"));
|
||||
assert!(ADMIN_POLICY_SCRIPT.contains("sourceKind', 'local_folder'"));
|
||||
assert!(ADMIN_POLICY_SCRIPT.contains("data-testid=\"mnote-admin-open-granted-root\""));
|
||||
assert!(ADMIN_POLICY_SCRIPT.contains("function isDefaultWorkspaceAutoGrant(grant)"));
|
||||
assert!(ADMIN_POLICY_SCRIPT.contains("var canRevoke = !systemOwned &&"));
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user