Files
mnote/design/02-convex-rust-long-term-architecture/done/2-8-convex-replace-with-rust-sqlite-control-plane-v1.md
lix-2026 42fb58310c chore: retire remaining convex infrastructure
- move self-hosted Convex infra and root placeholder into recycle

- update architecture docs to remove active infra/convex deployment guidance

- extend local-first Convex guard to block retired infra regressions
2026-05-22 19:31:20 +08:00

856 lines
38 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 2-8 Convex 完全替换为 Rust SQLite 控制面设计与迁移 checklist v1
> 创建时间:2026-05-22
>
> 当前状态:`done`
>
> 目标:在 local-first MVP 后阶段,将 Convex 从运行时控制面完全替换为 Rust + SQLite。迁移期间允许影子写入和只读导出校验,但成熟后 Convex 不再承担 auth、membership、share grants、sync state、AI policy、cloud source、compat 或 sync replica 的默认运行时职责。
>
> 上位依据:
> - `/mnt/Data1T/mnote/ARCHITECTURE.md`
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/done/2-2-local-first-workspace-convex-control-plane-v1.md`
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/done/2-3-local-workspace-access-control-productization-v1.md`
> - `/mnt/Data1T/mnote/design/03-rust-web/done/3-14-rust-web-tree-realtime-ws-push-v1.md`
---
## 1. 背景与结论
当前 MNote 已进入 local-first MVP 后阶段。本地 `.md`、附件、mindmap 和工作区目录是默认数据真相,Rust kernel 持有树、资源、页面聚合和命令语义。Convex 曾从旧主存储降级为控制面,但当前自托管 Convex backend 仍存在显著运行成本:正式容器长期运行后 RSS 约 3.7GiB;同镜像空实例约 20MiB;当前数据卷副本冷启动约 830MiB。对于账号、授权、分享、同步状态和 AI policy 这些控制面职责,这个常驻成本偏重,因此本稿继续把默认控制面替换为 Rust SQLite `control-plane`
本稿结论:
> **用 Rust + SQLite 实现 MNote 原生控制面,完全替换 Convex 运行时依赖;SQLite 只保存身份、授权、分享、同步、策略、审计和事件 outbox,不保存页面正文或附件 blob。**
替换目标不是引入第二个应用后端,而是把控制面收进 `mnote-web` / Rust runtime,使本地优先工作区拥有一个小、可备份、可迁移、可测试的控制数据库。
---
## 2. 非目标
本阶段不做:
- 不把页面正文迁入 SQLite。
- 不把本地附件、图片、OnlyOffice 文件 blob 迁入 SQLite。
- 不重新实现多人 CRDT 文档协作。
- 不引入 PocketBase、Supabase 或新的外部后端作为默认控制面。
- 不扩展旧 Convex functionsConvex 只作为迁移数据来源和短期影子比对对象。
- 不让前端直接访问 SQLite;所有读写必须经过 Rust API。
---
## 3. 目标运行架构
```text
Browser / Desktop Shell / Agent
|
v
Rust mnote-web :3000
- auth/session route
- workspace/profile route
- access policy route
- share/invite route
- sync state route
- AI policy/audit route
- tree/page projection route
- WS/SSE realtime route
|
v
Rust control-plane store trait
|
v
SQLite control-plane.db
|
+-- outbox_events -> mnote-web broadcast -> /api/realtime/ws
Local workspace folder remains data truth:
/mnt/Data1T/Mnote_data/users/<actor>/workspaces/my-space/**/*.md
```
默认数据库位置:
```text
/mnt/Data1T/Mnote_data/control-plane/control-plane.db
/mnt/Data1T/Mnote_data/control-plane/control-plane.db-wal
/mnt/Data1T/Mnote_data/control-plane/control-plane.db-shm
```
推荐 SQLite 配置:
```sql
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;
```
运行时原则:
- 单写者:只有 Rust control-plane store 写 SQLite。
- 事务优先:用户、授权、分享、同步状态和 outbox 必须在同一事务内提交。
- 版本控制:可被 UI 编辑的记录带 `revision``updated_at`,并支持 expected revision 检查。
- 推送由 outbox 驱动:DB 写成功后追加 `outbox_events`,再广播 WS/SSE;不得在事务成功前推事件。
- 本地文件权限判断以 SQLite 授权表为主,旧 `access-policy.json` 只作为迁移来源或只读 fallback。
---
## 4. Rust 模块边界
建议新增 crate 或模块:
```text
rust/crates/control-plane/
src/lib.rs
src/model.rs
src/store.rs
src/sqlite.rs
src/migrations.rs
src/password.rs
src/session.rs
src/outbox.rs
migrations/*.sql
```
如短期不新增 crate,可先放在:
```text
rust/crates/mnote-web/src/control_plane/
```
但长期建议独立 crate,避免 `mnote-web` route、SSR 和 DB 细节耦合。
核心 trait
```rust
pub trait ControlPlaneStore: Send + Sync {
fn upsert_user(&self, input: UpsertUserInput) -> Result<UserRecord, ControlPlaneError>;
fn authenticate_password(&self, login: LoginInput) -> Result<AuthSession, ControlPlaneError>;
fn get_session(&self, token_hash: &str) -> Result<Option<AuthSession>, ControlPlaneError>;
fn revoke_session(&self, session_id: &str) -> Result<(), ControlPlaneError>;
fn ensure_default_workspace(&self, actor_id: &str) -> Result<WorkspaceRecord, ControlPlaneError>;
fn list_workspaces_for_actor(&self, actor_id: &str) -> Result<Vec<WorkspaceSummary>, ControlPlaneError>;
fn grant_directory_access(&self, input: DirectoryGrantInput) -> Result<DirectoryGrant, ControlPlaneError>;
fn revoke_directory_grant(&self, grant_id: &str, expected_revision: Option<i64>) -> Result<(), ControlPlaneError>;
fn resolve_access(&self, actor_id: &str, root_uri: &str) -> Result<ResolvedAccess, ControlPlaneError>;
fn create_share_link(&self, input: ShareLinkInput) -> Result<ShareLink, ControlPlaneError>;
fn resolve_share_link(&self, token_hash: &str) -> Result<Option<ShareLinkAccess>, ControlPlaneError>;
fn get_ai_policy(&self, actor_id: &str, workspace_id: Option<&str>) -> Result<AiPolicy, ControlPlaneError>;
fn append_audit(&self, input: AuditInput) -> Result<(), ControlPlaneError>;
fn drain_outbox(&self, limit: usize) -> Result<Vec<OutboxEvent>, ControlPlaneError>;
}
```
`mnote-web``AppState` 增加:
```rust
control_plane: Arc<dyn ControlPlaneStore>
```
后续所有 auth、session、workspace、access-policy、share、AI policy route 只依赖 trait,不直接依赖 SQLite。
---
## 5. SQLite schema v1
### 5.1 users
```sql
CREATE TABLE users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE,
username TEXT UNIQUE NOT NULL,
display_name TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
revision INTEGER NOT NULL DEFAULT 1
);
```
说明:
- `id` 使用当前 actor id 兼容格式,避免迁移时破坏本地 workspace path。
- `role` 初期为 `admin|user`,不要把目录授权塞进 role。
- 新用户注册成功后必须创建默认个人 workspace 与 owner grant。
### 5.2 auth_identities
```sql
CREATE TABLE auth_identities (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider TEXT NOT NULL,
provider_subject TEXT NOT NULL,
password_hash TEXT,
password_version INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(provider, provider_subject)
);
```
初期 provider 支持:
- `password_email`
- `password_username`
- `convex_legacy`(仅迁移映射,不作为运行时登录)
密码哈希建议使用 Argon2id。若先做最小实现,可明确用当前已有密码验证口径,但必须留 `password_version` 以便升级。
### 5.3 auth_sessions
```sql
CREATE TABLE auth_sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash TEXT NOT NULL UNIQUE,
user_agent TEXT,
ip_hash TEXT,
created_at TEXT NOT NULL,
expires_at TEXT NOT NULL,
revoked_at TEXT,
last_seen_at TEXT NOT NULL
);
CREATE INDEX idx_auth_sessions_user ON auth_sessions(user_id);
CREATE INDEX idx_auth_sessions_token ON auth_sessions(token_hash);
```
运行时 cookie 不再依赖 `__convexAuthJWT`。建议新 cookie
- `mnote_session`
- `mnote_actor_id`
- `mnote_actor_name`
- `mnote_actor_email`
- `mnote_actor_type`
其中 `mnote_session` 是主凭证,actor cookies 只是 SSR/display 快捷缓存,可由 session route 刷新。
### 5.4 workspaces
```sql
CREATE TABLE workspaces (
id TEXT PRIMARY KEY,
owner_user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
kind TEXT NOT NULL DEFAULT 'personal',
root_uri TEXT NOT NULL UNIQUE,
root_path TEXT NOT NULL,
source_kind TEXT NOT NULL DEFAULT 'local_folder',
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
revision INTEGER NOT NULL DEFAULT 1
);
CREATE INDEX idx_workspaces_owner ON workspaces(owner_user_id);
```
默认工作区:
```text
/mnt/Data1T/Mnote_data/users/<user-id>/workspaces/my-space
```
`workspaces.name` 应由真实用户名生成,例如 `shujuan 的空间`,不再 fallback 到 `DEV_USER_NAME`
### 5.5 workspace_members
```sql
CREATE TABLE workspace_members (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
revision INTEGER NOT NULL DEFAULT 1,
UNIQUE(workspace_id, user_id)
);
```
`role` 初期支持:
- `owner`
- `editor`
- `viewer`
### 5.6 directory_grants
```sql
CREATE TABLE directory_grants (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
workspace_id TEXT REFERENCES workspaces(id) ON DELETE CASCADE,
root_uri TEXT NOT NULL,
root_path TEXT NOT NULL,
permission TEXT NOT NULL,
recursive INTEGER NOT NULL DEFAULT 1,
capabilities_json TEXT NOT NULL DEFAULT '[]',
source TEXT NOT NULL DEFAULT 'admin',
status TEXT NOT NULL DEFAULT 'active',
created_by TEXT REFERENCES users(id),
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
revision INTEGER NOT NULL DEFAULT 1
);
CREATE INDEX idx_directory_grants_user ON directory_grants(user_id);
CREATE INDEX idx_directory_grants_root_uri ON directory_grants(root_uri);
```
`permission``read|write|admin`
`capabilities_json` 初期保留:`ai|share|sync`
### 5.7 share_links
```sql
CREATE TABLE share_links (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
resource_kind TEXT NOT NULL,
resource_id TEXT NOT NULL,
token_hash TEXT NOT NULL UNIQUE,
permission TEXT NOT NULL,
created_by TEXT NOT NULL REFERENCES users(id),
expires_at TEXT,
revoked_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
revision INTEGER NOT NULL DEFAULT 1
);
CREATE INDEX idx_share_links_resource ON share_links(workspace_id, resource_kind, resource_id);
```
公开分享只暴露 token,不暴露 `token_hash`。token 只显示一次,DB 只保存 hash。
### 5.8 sync_state
```sql
CREATE TABLE sync_state (
id TEXT PRIMARY KEY,
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
remote_kind TEXT NOT NULL,
remote_id TEXT,
cursor TEXT,
last_synced_at TEXT,
status TEXT NOT NULL DEFAULT 'idle',
error_json TEXT,
updated_at TEXT NOT NULL,
revision INTEGER NOT NULL DEFAULT 1,
UNIQUE(workspace_id, remote_kind)
);
```
Convex 完全替换后,`remote_kind='convex'` 只作为历史迁移状态,不作为默认同步目标。
### 5.9 ai_policies
```sql
CREATE TABLE ai_policies (
id TEXT PRIMARY KEY,
user_id TEXT REFERENCES users(id) ON DELETE CASCADE,
workspace_id TEXT REFERENCES workspaces(id) ON DELETE CASCADE,
allowed_roots_json TEXT NOT NULL DEFAULT '[]',
model_policy_json TEXT NOT NULL DEFAULT '{}',
quota_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
revision INTEGER NOT NULL DEFAULT 1
);
```
AI allowed roots 由 `directory_grants``ai_policies` 交集计算,不能只靠前端传入。
### 5.10 audit_log
```sql
CREATE TABLE audit_log (
id TEXT PRIMARY KEY,
actor_user_id TEXT REFERENCES users(id),
action TEXT NOT NULL,
target_kind TEXT NOT NULL,
target_id TEXT,
metadata_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL
);
CREATE INDEX idx_audit_actor ON audit_log(actor_user_id, created_at);
CREATE INDEX idx_audit_target ON audit_log(target_kind, target_id, created_at);
```
必须记录:
- 注册 / 登录 / 退出。
- 管理员新增 / 删除授权。
- 分享链接创建 / 撤销。
- AI allowed roots 计算失败或越权拒绝。
- 迁移导入和 Convex 下线操作。
### 5.11 outbox_events
```sql
CREATE TABLE outbox_events (
id TEXT PRIMARY KEY,
topic TEXT NOT NULL,
event_type TEXT NOT NULL,
payload_json TEXT NOT NULL,
created_at TEXT NOT NULL,
delivered_at TEXT,
attempts INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX idx_outbox_pending ON outbox_events(delivered_at, created_at);
```
用于驱动:
- `/api/realtime/ws` 控制面事件。
- share / membership UI 刷新。
- 后续 sync worker。
### 5.12 legacy_id_map
```sql
CREATE TABLE legacy_id_map (
id TEXT PRIMARY KEY,
legacy_system TEXT NOT NULL,
legacy_kind TEXT NOT NULL,
legacy_id TEXT NOT NULL,
new_kind TEXT NOT NULL,
new_id TEXT NOT NULL,
metadata_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL,
UNIQUE(legacy_system, legacy_kind, legacy_id)
);
```
用于从 Convex user/workspace/document/share/session 等历史 id 映射到新控制面 id。
---
## 6. API 与 route 切换设计
### 6.1 Auth
现状问题:
- 登录 / 注册路径仍有 Convex auth 依赖。
- session response 可从 cookie/JWT 提取 actor name/email。
- 新用户默认 workspace 名称仍可能 fallback 到 `DEV_USER_NAME`
目标:
- `/api/auth/signup`:创建 `users``auth_identities`、默认 `workspaces``workspace_members(owner)``directory_grants(owner write)``auth_sessions`
- `/api/auth/signin`:按 email 或 username 查 identity,验证密码,创建 session。
- `/api/auth/signout`:撤销 session,清 cookie。
- `/api/session`:从 SQLite session 查真实用户,不再依赖 Convex JWT。
兼容:
- 迁移期可以继续读取旧 cookies 显示用户,但一旦 SQLite session 可用,应以 SQLite 为准。
### 6.2 Workspace 与目录授权
目标:
- `ensure_default_workspace_manifest` 之前先确保 SQLite 默认 workspace 记录存在。
- local-folder read/write access 不再以 `access-policy.json` 为主,而是查 `directory_grants` + workspace owner/member。
- `access-policy.json` 只在迁移工具中读取,用于生成 `directory_grants`
管理员 API
- `GET /api/admin/access-policy` 改读 SQLite。
- `POST /api/admin/access-policy/validate-root` 保持 Rust 文件系统校验。
- `POST /api/admin/access-policy/grants` 写 SQLite + audit + outbox。
- `DELETE /api/admin/access-policy/grants/{grantId}` 写 SQLite + audit + outbox。
普通用户 API
- `/user/access-policy` 只看自己可见 share / grants,不可提升权限。
### 6.3 Share
目标:
- 分享不再是“本地目录能不能访问”的隐式效果,而是显式 `share_links` / `directory_grants`
- share token 解析后生成受限 read grant 或 request-scoped access,不直接给全 workspace 写权限。
第一阶段最小分享:
- 创建 workspace/page/resource read link。
- 撤销 link。
- 通过 token 打开只读资源。
写分享、邀请、协作编辑后续建立在同一 schema 上扩展。
### 6.4 Realtime
当前 `/api/realtime/ws` 已不再需要 Convex idle polling。SQLite 控制面事件应复用现有 broadcast 机制:
```text
SQLite transaction writes outbox_events
-> outbox dispatcher reads pending event
-> AppState broadcast channel send
-> WS clients receive control-plane delta
```
事件类型初期:
- `control.user.updated`
- `control.workspace.updated`
- `control.grant.created`
- `control.grant.revoked`
- `control.share.created`
- `control.share.revoked`
- `control.ai_policy.updated`
树 / 页面文件变更事件仍由 Rust tree live cache 和 local folder watcher 负责,不通过 Convex。
---
## 7. 迁移阶段
### Phase 0:设计落盘与任务拆分
- [x] 写入本设计稿。
- [x] 拆分 Reasonix 并行任务。
- [x] Codex 复核 Reasonix 输出后合入。
### Phase 1SQLite store 骨架,不接运行时
目标:引入 `control-plane` crate 或 `mnote-web/src/control_plane` 模块,完成 schema、migration、store trait 和单测。
验收:
- [x] 可在临时目录创建 SQLite DB。
- [x] migration 可重复执行。
- [x] users/auth/workspaces/grants/share/outbox 基础 CRUD 单测通过。
- [x] 不改现有运行时行为。
- [x] 当前已完成:users/auth_sessions/workspaces/directory_grants/share_links/outbox 最小 CRUD 与单测;share 运行时接入仍在 Phase 4。
### Phase 2Auth/session 切到 SQLite
目标:注册、登录、session、退出登录先走 SQLite。
验收:
- [x] 新注册用户默认写入 `users`
- [x] 新用户默认 workspace 与 owner grant 创建。
- [x] `/api/session` 返回真实 name/email/id。
- [x] 登录无需 Convex backend。
- [x] 老 cookie / dev fallback 不再污染真实登录用户的 workspace 名称。
- [x] 当前已完成:`/api/auth/session` 已支持 `mnote_session` cookie 优先从 SQLite session 解析真实用户;`/api/auth` 已接入 SQLite signUp/signIn/signOut,注册会写入 `users`、password identity、默认 workspace/owner grant 和 session cookie。mnote-web session route 定向测试已复跑确认。
### Phase 3Access policy 切到 SQLite
目标:local folder read/write、管理员授权 UI/API、用户授权页都走 SQLite。
验收:
- [x] shujuan 这类新用户默认拥有个人空间写权限。
- [x] 非管理员不能读写未授权目录。
- [x] read grant 不允许 page write/tree mutation/asset upload。
- [x] write grant 可创建页面。
- [ ] 管理员新增/删除 grant 后 WS 推送控制面事件。
### Phase 4Share / AI policy / sync state / AI runtime 切到 SQLite
目标:分享链接、AI policy、sync 状态、审计日志、Hermes/ACP runtime session 不再依赖 Convex 默认运行时。
验收:
- [x] 创建 / 撤销分享链接写入 SQLite。
- [x] AI policy 可按用户 / workspace 写入并优先解析 workspace policy。
- [x] sync_state 可被本地 worker 更新。
- [x] audit_log 记录关键控制面操作。
- [x] Hermes/ACP runtime run、event、session list/detail/search/resume、rename、auto-title、delete 默认读写 SQLite;旧 Convex store 仅通过显式 `legacyConvex=1` 进入。
### Phase 5Convex shadow 验证与下线
目标:所有默认运行路径不访问 Convex。Convex 仅作为迁移导出源或手工回滚源。
验收:
- [x] 默认 Rust route / unit smoke 在未配置 Convex backend 时通过,覆盖 local-first session、授权管理、分享基础链路与 ACP runtime store。
- [x] `npm run check:local-first-convex-guard` 更新为禁止新增 Convex 控制面代码。
- [x] 本设计稿口径更新:Convex 退役为历史迁移源 / 显式 legacy 兼容入口。
- [x] Docker / infra 层不在本稿执行中删除数据卷;Convex 真实停服切换只允许在迁移报告确认后执行。
---
## 9. 执行记录(2026-05-22
### 已完成验证
- `cargo test --manifest-path rust/Cargo.toml -p control-plane -- --nocapture`
- 20/20 通过。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web session -- --nocapture`
- 46/46 通过。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_client_acp -- --nocapture`
- 10/10 通过,确认 ACP runtime 默认 SQLite;旧 Convex store 仅由 `legacyConvex=1` 兼容测试覆盖。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_access_policy -- --nocapture`
- 2/2 通过,确认管理员授权写入、列表、撤销已切到 SQLite directory grants。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web share_link -- --nocapture`
- 1/1 通过,确认公开分享链接创建、列表、撤销已写入 SQLite。
- `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`
- 通过。
- `cargo fmt --manifest-path rust/Cargo.toml --all --check`
- 通过。
### 当前实现状态
- `control-plane` crate 已落地,覆盖 users / auth_identities / auth_sessions / workspaces / workspace_members / directory_grants / sync_state / ai_policies / audit_log / outbox_events / legacy_id_map 的 v1 schema。
- `mnote-web` 已接入 SQLite session 读取与默认 workspace / owner grant 创建。
- `mnote-web` 的 local folder 读写与管理员授权 API 已优先走 SQLite directory grants,旧 JSON policy 仅保留为迁移 fallback。
- 授权管理 UI 已切到弹窗,不再挤掉左侧页面。
- 分享链接、AI policy、sync_state、audit_log、outbox、ACP runtime run/event/session 管理已经有 SQLite store 与定向测试覆盖。
- Convex 运行时控制面入口不再作为默认路径;保留 `legacyConvex=1` / `convex=1`、迁移 dry-run 与历史 transport 作为显式兼容边界。
### 后续跟踪项
- 密码哈希仍是明确标记的 `sha256-v1` 占位实现,需要后续升级 Argon2id 并写迁移策略。
- 真实 Docker Compose 停服切换和 Convex 数据卷保留 / 归档属于运维执行,不在本稿自动执行。
- 旧 Convex transport 与 `legacyConvex=1` 兼容入口保留给历史会话读取和人工回滚;新增默认路径不得再依赖它。
---
## 8. 数据迁移设计
迁移输入:
- `/mnt/Data1T/Mnote_data/control-plane/access-policy.json`
- 当前本地 workspace `.mnote/workspace.json`
- Convex 导出的 users/workspaces/share/session/aiSessions 数据(如果存在)
- `.env.all` 中的历史 dev/admin 配置,仅作为管理员 bootstrap 输入
迁移输出:
- `control-plane.db`
- `legacy_id_map`
- `audit_log` 中的 migration 记录
- `migration-report.json`
迁移规则:
1. 先创建 admin users。
2. 再导入真实注册 users。
3. 对每个用户确保默认 workspace。
4. 读取 `.mnote/workspace.json` 建立 workspace 记录和 owner membership。
5. 读取 `access-policy.json` 导入 directory grants。
6. 读取 Convex share/membership/sync 数据导入对应表。
7. 对旧 id 写 `legacy_id_map`
8. 生成报告:导入数量、跳过数量、冲突、不可迁移数据。
冲突处理:
- email 冲突:保留最早创建用户,其他写 report,需人工合并。
- username 冲突:后导入用户追加短后缀,并写 audit。
- root_uri 冲突:同一 root 只能有一个 workspace owner,其他访问者转为 membership/grant。
- grant 冲突:同 user/root 取权限较高者,保留 capability 并集。
回滚:
- SQLite 迁移前复制原 DB 或写入新 DB 文件,未通过验证不替换 active DB。
- Convex 下线前不删除 Convex 数据卷,只停止默认启动。
- 每个迁移批次写 `migration_id`,便于 audit 和报告定位。
---
## 9. Reasonix 并行执行 checklist
### Worker ASQLite schema/store 骨架
Ownership
- `rust/crates/control-plane/**``rust/crates/mnote-web/src/control_plane/**`
- `rust/Cargo.toml`
- `rust/crates/mnote-web/Cargo.toml`(如选择内嵌模块则尽量不改)
任务:
- [x] 新增 ControlPlaneStore trait、models、SQLiteStore。
- [x] 新增 migrations v1。
- [x] 实现 users/auth_sessions/workspaces/directory_grants/outbox 最小 CRUD。
- [x] 补 migration 和 CRUD 单测。
禁止:
- 不修改 auth route 和 gateway 行为。
- 不删除 Convex transport。
- 不改现有 local folder access 行为。
验收:
- `cargo test --manifest-path rust/Cargo.toml -p control-plane -- --nocapture` 或对应 `mnote-web control_plane` 测试。
- `cargo fmt --check --all`
### Worker BAuth/session SQLite 切换准备
Ownership
- `rust/crates/mnote-web/src/routes/session.rs`
- `rust/crates/mnote-web/src/ssr/pages/auth.rs`
- 可新增 `rust/crates/mnote-web/src/routes/auth_sqlite.rs`
- 只读参考 `gateway.rs`,不得覆盖已有未提交改动。
任务:
- [x] 梳理现有 signIn/signUp/session cookie 链。
- [x] 在不破坏现有行为前提下,设计并实现 SQLite session route adapter。
- [ ] 新用户注册后应生成真实 display name/email/session response。
- [ ]`/api/session` 与注册/登录单测。
禁止:
- 不直接删除 Convex auth fallback。
- 不改 local folder access policy 文件。
- 不碰 `ssr/styles.rs`
验收:
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web session -- --nocapture`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web auth -- --nocapture`
### Worker CAccess policy SQLite adapter
Ownership
- `rust/crates/mnote-web/src/routes/local_folder_source.rs`
- 可新增 `rust/crates/mnote-web/src/local_access_sqlite.rs`
- 可新增 focused tests。
任务:
- [ ] 把 access policy 读写抽象成 trait/adapter,保留 JSON fallback。
- [ ] 设计 SQLite grant 到 `ensure_local_workspace_read_access` / write access 的接入点。
- [ ] 实现默认 workspace owner grant 的测试。
- [ ] 覆盖 read grant 禁写、write grant 可写、未授权拒绝。
禁止:
- 不重写 tree command。
- 不改 UI 页面。
- 不删除 `access-policy.json` 支持。
验收:
- `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`
### Worker D:迁移工具与 Convex 下线计划
Ownership
- `scripts/migrate-control-plane-to-sqlite.js` 或 Rust CLI 等价脚本
- `scripts/check-local-first-convex-guard.js`
- `design/02-convex-rust-long-term-architecture/process/2-8-*` 只能追加执行记录,不改设计结论。
任务:
- [ ] 设计并实现迁移 dry-run 工具,读取 `access-policy.json` 与 workspace manifests。
- [ ] 输出 `migration-report.json`
- [x] 增加 guard 检查:新增 Convex 控制面代码必须失败或标注迁移例外,根 `convex/` functions 回流默认控制面也必须失败。
- [ ] 给出 Convex compose 下线 checklist。
禁止:
- 不执行真实破坏性迁移。
- 不删除 Docker volume。
- 不修改 `.env.all`
验收:
- `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`
---
## 10. 总体验收矩阵
完全切换前必须满足:
- [x] Convex backend 停止时,新用户可注册、登录、看到自己的个人空间。
- [x] 新用户默认可在个人空间创建页面。
- [x] 管理员可新增 / 删除目录授权。
- [x] 普通用户不能访问未授权目录。
- [x] read share 不能写,write grant 可以写。
- [x] `/api/session` 不依赖 Convex JWT。
- [x] `/api/realtime/ws` 能收到控制面事件。
- [x] AI allowed roots 不扩大到未授权目录。
- [x] `NEXT_PUBLIC_CONVEX_URL=http://127.0.0.1:9` 时 local-first 主路径 smoke 通过。
- [x] `infra/convex` 已软删除到 `recycle/20260522-convex-runtime-retirement/infra/convex/`,不再被 `npm run desktop:hot` 或默认启动链路要求。
- [x]`convex/` functions 源码已软删除到 `recycle/20260522-convex-runtime-retirement/convex/``scripts/run-convex-deploy.js` 已随之归档。
- [x] `package.json` / npm lock / pnpm lock 已移除只服务根 Convex functions 的 `@auth/core``@convex-dev/auth``convex` 依赖。
- [x] 文档更新,明确 Convex 只作为历史迁移源或显式 legacy 兼容入口。
---
## 11. 风险与裁决
| 风险 | 裁决 |
|---|---|
| SQLite auth 自研带来安全责任 | 使用 Argon2id、token hash、session expiry、audit;不明文保存 token。 |
| 迁移期间双写不一致 | 以 SQLite 为新主,Convex 只读导出;影子写仅用于比对,不作为裁决源。 |
| 权限逻辑分散 | 所有 read/write/AI allowed roots 都经 control-plane store + local path canonicalization。 |
| 多 worker 改同一文件冲突 | Reasonix 任务按 ownership 拆分,共享文件只允许建议或最小 adapter。 |
| 一次性切流风险高 | 按 auth、access、share/AI、downline 分阶段切;每阶段保留独立 smoke。 |
---
## 12. 本轮执行记录
- 2026-05-22Codex 写入本设计稿并拆分 Reasonix Worker A-D 任务书。
- 2026-05-22Worker D(迁移工具与 Convex 下线 guard)完成:
- 新增 `scripts/migrate-control-plane-to-sqlite.js`dry-run 迁移工具,支持 `--dry-run --out``--access-policy-path``--users-root``--skip-users``--from-snapshot``--verbose`。读取 access-policy.json 与所有用户 workspace.json,生成 users/workspaces/grants/legacy_id_map 建议,不写 DB。
- 修改 `scripts/check-local-first-convex-guard.js`:新增 `MIGRATION_PERIOD_FORBIDDEN` 规则(users/workspaces/grants/share/sync/audit/outbox/ai_policies/legacy_id_map),新增 `MIGRATION_PERIOD_WAIVER = "sqlite-migration-waiver"`,新增 `isMigrationWaived` 辅助函数,更新 self-test 覆盖迁移期拦截。Shell 解释器问题导致 node 直接调用的验收命令未在 session 内完成,但通过 sandbox run_background 验证 dry-run 报告完整生成 , guard self-test 通过。
- 2026-05-22Codex 接管并完成 Phase 1 / Phase 2 session adapter 第一段:
- 新增 `rust/crates/control-plane/` 并接入 Rust workspace 与 `mnote-web`,默认 DB 路径为 `/mnt/Data1T/Mnote_data/control-plane/control-plane.db`,支持 `MNOTE_CONTROL_PLANE_DB_PATH` 覆盖。
- 实现 `users``auth_sessions``workspaces``workspace_members``directory_grants``outbox_events` 最小 store API`ensure_default_workspace` 会为新用户生成个人空间和 owner write grant。
- `/api/auth/session` 支持 `mnote_session` cookie 优先查 SQLite session,命中后返回真实 `userId/email/name/authMode=sqliteSession`,未命中时保留 forwarded actor / dev fallback。
- 验证:`cargo test --manifest-path rust/Cargo.toml -p control-plane -- --nocapture` 8 passed`cargo check --manifest-path rust/Cargo.toml -p mnote-web --lib` passed;迁移 dry-run 与 Convex guard self-test passed`rustfmt --edition 2021 --check` 和本轮 `git diff --check` passed。`mnote-web` route 定向单测因测试制品链接阶段超过 180s 未完成,需后续在释放 cargo watch 或独立 target dir 后复跑。
- 2026-05-22Codex 继续推进 Phase 2 Auth/session SQLite 切换:
- `control-plane` 增加 password identity 与 `authenticate_password`,支持 email/username 登录、错误密码拒绝,并创建 SQLite auth session;当前密码哈希为明确标记的 `sha256-v1` 占位实现,后续需升级 Argon2id。
- `gateway::auth_api` 不再代理 Convex Auth`signUp` 创建/更新用户、password identity、默认 workspace/owner grant 和 `mnote_session``signIn` 直接查 SQLite`signOut` 撤销并清理 `mnote_session` 与旧 Convex cookies。
- `/auth` 的已登录判断加入 `mnote_session` cookie,避免 SQLite 登录后仍停留在登录页。
- 验证:`cargo test --manifest-path rust/Cargo.toml -p control-plane -- --nocapture` 12 passed;迁移 dry-run、Convex guard self-test、`rustfmt --edition 2021 --check`、本轮 `git diff --check` passed。`timeout 240s cargo check --manifest-path rust/Cargo.toml -p mnote-web --lib` 仍卡在 `rustc --crate-name mnote_web` 阶段并超时,未得到通过结论。
- 2026-05-22Codex 修复 SQLite session 在页面壳中的真实 actor 解析,并收口授权管理弹窗验证:
- `gateway` 新增 `current_actor_id` / `current_actor_type` / `current_actor_display_name` / `default_workspace_name_for_context`,根入口、授权管理入口、文档页 shell、mindmap shell 均优先使用 SQLite `mnote_session` 对应的真实用户信息生成个人空间名。
- 修复仅携带 `mnote_session` 时根入口仍按 anonymous 处理的问题;新注册用户进入 local-first landing 时会创建并显示自己的个人空间,例如 `shujuan 的空间`,不再 fallback 到 `开发用户 的空间`
- 授权管理 UI 保持弹窗模式,普通用户和管理员都使用同一个最小“文件夹地址 + 授权用户 ID + read/write”表单;管理员可授权任意本地文件夹,普通用户只能授权自己空间下的文件夹;读/写权限继续由 `directory_grants.permission` / share grant permission 保留。
- 验证:`cargo test --manifest-path rust/Cargo.toml -p mnote-web root_entry_uses_sqlite_session_display_name_for_workspace_label -- --nocapture` passed`cargo test --manifest-path rust/Cargo.toml -p mnote-web session -- --nocapture` 46 passed`cargo test --manifest-path rust/Cargo.toml -p mnote-web access_policy -- --nocapture` 7 passed`cargo test --manifest-path rust/Cargo.toml -p mnote-web share_grant -- --nocapture` 4 passed`cargo test --manifest-path rust/Cargo.toml -p control-plane -- --nocapture` 15 passed。
- 2026-05-22Codex 继续收口 Phase 3 授权管理运行时:
- 授权管理弹窗脚本从旧 `/api/*/share-grants` 切到 SQLite 目录授权 `/api/*/access-policy``share-grants` 仅保留给公开分享兼容链路,避免文件夹授权与公开分享混用。
- 新增普通用户目录授权 API`GET /api/user/access-policy``POST /api/user/access-policy/grants``DELETE /api/user/access-policy/grants/{grant_id}`。普通用户可以把自己空间下的文件夹授权给目标用户;目标用户获得 `directory_grants.permission=read|write` 对应能力;普通用户只能撤销自己创建的目录授权。
- 修复首页隐藏授权模板自动初始化问题:普通用户首页不再预加载 `/api/admin/access-policy` 或旧 `/api/admin/share-grants`,只有打开弹窗或访问独立授权页时才读取授权列表。
- 授权弹窗中的“授权记录”显示 SQLite `control-plane.db` 路径,不再显示旧 `share-grants.json`
- 验证:`cargo test --manifest-path rust/Cargo.toml -p mnote-web access_policy -- --nocapture` 8 passed`cargo test --manifest-path rust/Cargo.toml -p mnote-web share_grant -- --nocapture` 4 passed`cargo test --manifest-path rust/Cargo.toml -p mnote-web root_entry_renders_access_policy_dialog_templates_for_all_actors -- --nocapture` passed`cargo test --manifest-path rust/Cargo.toml -p control-plane -- --nocapture` 15 passed`cargo fmt --manifest-path rust/Cargo.toml --all --check` passed`npm run check:local-first-convex-guard` passed`git diff --check` passed。浏览器临时实例 `127.0.0.1:3010` 验证:首页初始不请求授权接口;打开账号菜单的“授权管理”后 URL 保持 `/`、左侧“我的页面 / Explorer / +”仍保留、弹窗请求 `/api/user/access-policy` 200、当前控制台 0 error。
- 2026-05-22Codex 推进 Phase 4 公开分享链接第一段:
- 新增 SQLite share links 运行时 API`GET /api/admin/share-links?workspaceId=...``POST /api/admin/share-links``DELETE /api/admin/share-links/{link_id}`,直接调用 `control-plane``create_share_link` / `list_share_links` / `revoke_share_link`
- API 响应只在创建时返回明文 `token`,列表和 link payload 不暴露 `token_hash`;撤销后 `resolve_share_link(token_hash)` 不再命中。
- 分享链接创建 / 撤销写入 `audit_log``control.share.created` / `control.share.revoked`。旧 `/api/*/share-grants` 仍作为目标用户共享授权、shared-cache/sync 和 Hermes 共享上下文兼容链路保留,尚未切到 `share_links`
- 验证:先运行 `cargo test --manifest-path rust/Cargo.toml -p mnote-web share_link_api_creates_lists_and_revokes_sqlite_record -- --nocapture` 得到缺失 handler/type 的 RED 编译失败;实现后同命令 1 passed。随后 `cargo test --manifest-path rust/Cargo.toml -p control-plane share_link -- --nocapture` 1 passed`cargo test --manifest-path rust/Cargo.toml -p mnote-web share_grant -- --nocapture` 4 passed。
- 2026-05-22Codex 收口 Phase 4 / Phase 5 默认运行时:
- `control-plane` 新增 `002-ai-runtime-store.sql`,落地 `ai_runtime_runs` / `ai_runtime_events`,并补齐 `upsert/list/append` 与 session `rename/auto-title/delete` store API。
- `mnote-web` 的 ACP runtime session list/detail/search/resume、run/event persistence、rename、auto-title、delete 默认读写 SQLite;旧 Convex ACP runtime store 只在显式 `legacyConvex=1``convex=1` 时使用。
- local folder 授权与 share link 创建 / 撤销已写入 outbox 并广播 `stream_delta_tx` 控制面事件,覆盖 `control.grant.created/revoked``control.share.created/revoked`
- 验证:`cargo test --manifest-path rust/Cargo.toml -p control-plane -- --nocapture` 20 passed`cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_client_acp -- --nocapture` 10 passed`cargo test --manifest-path rust/Cargo.toml -p mnote-web session -- --nocapture` 46 passed`cargo test --manifest-path rust/Cargo.toml -p mnote-web local_access_policy -- --nocapture` 2 passed`cargo test --manifest-path rust/Cargo.toml -p mnote-web share_link -- --nocapture` 1 passed;迁移 dry-run 与 `npm run check:local-first-convex-guard` passed`cargo fmt --manifest-path rust/Cargo.toml --all` 已执行。
- 2026-05-22Codex 追加完成 Convex runtime functions 源码退役清理:
-`convex/` 已按软删除规则移动到 `recycle/20260522-convex-runtime-retirement/convex/`,只作为历史审计 / 迁移对照素材。
- `scripts/run-convex-deploy.js` 已移动到 `recycle/20260522-convex-runtime-retirement/scripts/run-convex-deploy.js`;默认仓库不再提供根 Convex functions deploy 入口。
- `package.json``package-lock.json``pnpm-lock.yaml` 已移除只服务根 Convex functions 的 `@auth/core``@convex-dev/auth``convex` 依赖。
- `npm run desktop:hot``npm run dev:hot` 均已确认只启动 Rust `mnote-web` hot 链路,不启动 Convex`infra/convex` 已软删除到 `recycle/20260522-convex-runtime-retirement/infra/convex/`,只作为历史对照,不再作为默认启动链路或 active 服务基础设施。
- `gateway.rs` 已移除旧 Convex Auth action proxy / username lookup / auth response cookie helper,默认 `/api/auth` 只走 SQLite session。
- `scripts/check-local-first-convex-guard.js` 更新为拦截重新新增根 `convex/schema.ts``auth.ts``users.ts``aiSessions.ts`,防止退役 functions 源码回流默认控制面。