- 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
38 KiB
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 functions;Convex 只作为迁移数据来源和短期影子比对对象。
- 不让前端直接访问 SQLite;所有读写必须经过 Rust API。
3. 目标运行架构
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
默认数据库位置:
/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 配置:
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 或模块:
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,可先放在:
rust/crates/mnote-web/src/control_plane/
但长期建议独立 crate,避免 mnote-web route、SSR 和 DB 细节耦合。
核心 trait:
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 增加:
control_plane: Arc<dyn ControlPlaneStore>
后续所有 auth、session、workspace、access-policy、share、AI policy route 只依赖 trait,不直接依赖 SQLite。
5. SQLite schema v1
5.1 users
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
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_emailpassword_usernameconvex_legacy(仅迁移映射,不作为运行时登录)
密码哈希建议使用 Argon2id。若先做最小实现,可明确用当前已有密码验证口径,但必须留 password_version 以便升级。
5.3 auth_sessions
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_sessionmnote_actor_idmnote_actor_namemnote_actor_emailmnote_actor_type
其中 mnote_session 是主凭证,actor cookies 只是 SSR/display 快捷缓存,可由 session route 刷新。
5.4 workspaces
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);
默认工作区:
/mnt/Data1T/Mnote_data/users/<user-id>/workspaces/my-space
workspaces.name 应由真实用户名生成,例如 shujuan 的空间,不再 fallback 到 DEV_USER_NAME。
5.5 workspace_members
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 初期支持:
ownereditorviewer
5.6 directory_grants
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
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
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
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
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
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
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 机制:
SQLite transaction writes outbox_events
-> outbox dispatcher reads pending event
-> AppState broadcast channel send
-> WS clients receive control-plane delta
事件类型初期:
control.user.updatedcontrol.workspace.updatedcontrol.grant.createdcontrol.grant.revokedcontrol.share.createdcontrol.share.revokedcontrol.ai_policy.updated
树 / 页面文件变更事件仍由 Rust tree live cache 和 local folder watcher 负责,不通过 Convex。
7. 迁移阶段
Phase 0:设计落盘与任务拆分
- 写入本设计稿。
- 拆分 Reasonix 并行任务。
- Codex 复核 Reasonix 输出后合入。
Phase 1:SQLite store 骨架,不接运行时
目标:引入 control-plane crate 或 mnote-web/src/control_plane 模块,完成 schema、migration、store trait 和单测。
验收:
- 可在临时目录创建 SQLite DB。
- migration 可重复执行。
- users/auth/workspaces/grants/share/outbox 基础 CRUD 单测通过。
- 不改现有运行时行为。
- 当前已完成:users/auth_sessions/workspaces/directory_grants/share_links/outbox 最小 CRUD 与单测;share 运行时接入仍在 Phase 4。
Phase 2:Auth/session 切到 SQLite
目标:注册、登录、session、退出登录先走 SQLite。
验收:
- 新注册用户默认写入
users。 - 新用户默认 workspace 与 owner grant 创建。
/api/session返回真实 name/email/id。- 登录无需 Convex backend。
- 老 cookie / dev fallback 不再污染真实登录用户的 workspace 名称。
- 当前已完成:
/api/auth/session已支持mnote_sessioncookie 优先从 SQLite session 解析真实用户;/api/auth已接入 SQLite signUp/signIn/signOut,注册会写入users、password identity、默认 workspace/owner grant 和 session cookie。mnote-web session route 定向测试已复跑确认。
Phase 3:Access policy 切到 SQLite
目标:local folder read/write、管理员授权 UI/API、用户授权页都走 SQLite。
验收:
- shujuan 这类新用户默认拥有个人空间写权限。
- 非管理员不能读写未授权目录。
- read grant 不允许 page write/tree mutation/asset upload。
- write grant 可创建页面。
- 管理员新增/删除 grant 后 WS 推送控制面事件。
Phase 4:Share / AI policy / sync state / AI runtime 切到 SQLite
目标:分享链接、AI policy、sync 状态、审计日志、Hermes/ACP runtime session 不再依赖 Convex 默认运行时。
验收:
- 创建 / 撤销分享链接写入 SQLite。
- AI policy 可按用户 / workspace 写入并优先解析 workspace policy。
- sync_state 可被本地 worker 更新。
- audit_log 记录关键控制面操作。
- Hermes/ACP runtime run、event、session list/detail/search/resume、rename、auto-title、delete 默认读写 SQLite;旧 Convex store 仅通过显式
legacyConvex=1进入。
Phase 5:Convex shadow 验证与下线
目标:所有默认运行路径不访问 Convex。Convex 仅作为迁移导出源或手工回滚源。
验收:
- 默认 Rust route / unit smoke 在未配置 Convex backend 时通过,覆盖 local-first session、授权管理、分享基础链路与 ACP runtime store。
npm run check:local-first-convex-guard更新为禁止新增 Convex 控制面代码。- 本设计稿口径更新:Convex 退役为历史迁移源 / 显式 legacy 兼容入口。
- 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兼容测试覆盖。
- 10/10 通过,确认 ACP runtime 默认 SQLite;旧 Convex store 仅由
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-planecrate 已落地,覆盖 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.dblegacy_id_mapaudit_log中的 migration 记录migration-report.json
迁移规则:
- 先创建 admin users。
- 再导入真实注册 users。
- 对每个用户确保默认 workspace。
- 读取
.mnote/workspace.json建立 workspace 记录和 owner membership。 - 读取
access-policy.json导入 directory grants。 - 读取 Convex share/membership/sync 数据导入对应表。
- 对旧 id 写
legacy_id_map。 - 生成报告:导入数量、跳过数量、冲突、不可迁移数据。
冲突处理:
- 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 A:SQLite schema/store 骨架
Ownership:
rust/crates/control-plane/**或rust/crates/mnote-web/src/control_plane/**rust/Cargo.tomlrust/crates/mnote-web/Cargo.toml(如选择内嵌模块则尽量不改)
任务:
- 新增 ControlPlaneStore trait、models、SQLiteStore。
- 新增 migrations v1。
- 实现 users/auth_sessions/workspaces/directory_grants/outbox 最小 CRUD。
- 补 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 B:Auth/session SQLite 切换准备
Ownership:
rust/crates/mnote-web/src/routes/session.rsrust/crates/mnote-web/src/ssr/pages/auth.rs- 可新增
rust/crates/mnote-web/src/routes/auth_sqlite.rs - 只读参考
gateway.rs,不得覆盖已有未提交改动。
任务:
- 梳理现有 signIn/signUp/session cookie 链。
- 在不破坏现有行为前提下,设计并实现 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 -- --nocapturecargo test --manifest-path rust/Cargo.toml -p mnote-web auth -- --nocapture
Worker C:Access 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 -- --nocapturecargo 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.jsdesign/02-convex-rust-long-term-architecture/process/2-8-*只能追加执行记录,不改设计结论。
任务:
- 设计并实现迁移 dry-run 工具,读取
access-policy.json与 workspace manifests。 - 输出
migration-report.json。 - 增加 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.jsonnpm run check:local-first-convex-guard
10. 总体验收矩阵
完全切换前必须满足:
- Convex backend 停止时,新用户可注册、登录、看到自己的个人空间。
- 新用户默认可在个人空间创建页面。
- 管理员可新增 / 删除目录授权。
- 普通用户不能访问未授权目录。
- read share 不能写,write grant 可以写。
/api/session不依赖 Convex JWT。/api/realtime/ws能收到控制面事件。- AI allowed roots 不扩大到未授权目录。
NEXT_PUBLIC_CONVEX_URL=http://127.0.0.1:9时 local-first 主路径 smoke 通过。infra/convex已软删除到recycle/20260522-convex-runtime-retirement/infra/convex/,不再被npm run desktop:hot或默认启动链路要求。- 根
convex/functions 源码已软删除到recycle/20260522-convex-runtime-retirement/convex/,scripts/run-convex-deploy.js已随之归档。 package.json/ npm lock / pnpm lock 已移除只服务根 Convex functions 的@auth/core、@convex-dev/auth、convex依赖。- 文档更新,明确 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-22:Codex 写入本设计稿并拆分 Reasonix Worker A-D 任务书。
- 2026-05-22:Worker 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-22:Codex 接管并完成 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_sessioncookie 优先查 SQLite session,命中后返回真实userId/email/name/authMode=sqliteSession,未命中时保留 forwarded actor / dev fallback。- 验证:
cargo test --manifest-path rust/Cargo.toml -p control-plane -- --nocapture8 passed;cargo check --manifest-path rust/Cargo.toml -p mnote-web --libpassed;迁移 dry-run 与 Convex guard self-test passed;rustfmt --edition 2021 --check和本轮git diff --checkpassed。mnote-webroute 定向单测因测试制品链接阶段超过 180s 未完成,需后续在释放 cargo watch 或独立 target dir 后复跑。
- 新增
- 2026-05-22:Codex 继续推进 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_sessioncookie,避免 SQLite 登录后仍停留在登录页。- 验证:
cargo test --manifest-path rust/Cargo.toml -p control-plane -- --nocapture12 passed;迁移 dry-run、Convex guard self-test、rustfmt --edition 2021 --check、本轮git diff --checkpassed。timeout 240s cargo check --manifest-path rust/Cargo.toml -p mnote-web --lib仍卡在rustc --crate-name mnote_web阶段并超时,未得到通过结论。
- 2026-05-22:Codex 修复 SQLite session 在页面壳中的真实 actor 解析,并收口授权管理弹窗验证:
gateway新增current_actor_id/current_actor_type/current_actor_display_name/default_workspace_name_for_context,根入口、授权管理入口、文档页 shell、mindmap shell 均优先使用 SQLitemnote_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 -- --nocapturepassed;cargo test --manifest-path rust/Cargo.toml -p mnote-web session -- --nocapture46 passed;cargo test --manifest-path rust/Cargo.toml -p mnote-web access_policy -- --nocapture7 passed;cargo test --manifest-path rust/Cargo.toml -p mnote-web share_grant -- --nocapture4 passed;cargo test --manifest-path rust/Cargo.toml -p control-plane -- --nocapture15 passed。
- 2026-05-22:Codex 继续收口 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 -- --nocapture8 passed;cargo test --manifest-path rust/Cargo.toml -p mnote-web share_grant -- --nocapture4 passed;cargo test --manifest-path rust/Cargo.toml -p mnote-web root_entry_renders_access_policy_dialog_templates_for_all_actors -- --nocapturepassed;cargo test --manifest-path rust/Cargo.toml -p control-plane -- --nocapture15 passed;cargo fmt --manifest-path rust/Cargo.toml --all --checkpassed;npm run check:local-first-convex-guardpassed;git diff --checkpassed。浏览器临时实例127.0.0.1:3010验证:首页初始不请求授权接口;打开账号菜单的“授权管理”后 URL 保持/、左侧“我的页面 / Explorer / +”仍保留、弹窗请求/api/user/access-policy200、当前控制台 0 error。
- 授权管理弹窗脚本从旧
- 2026-05-22:Codex 推进 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 -- --nocapture1 passed;cargo test --manifest-path rust/Cargo.toml -p mnote-web share_grant -- --nocapture4 passed。
- 新增 SQLite share links 运行时 API:
- 2026-05-22:Codex 收口 Phase 4 / Phase 5 默认运行时:
control-plane新增002-ai-runtime-store.sql,落地ai_runtime_runs/ai_runtime_events,并补齐upsert/list/append与 sessionrename/auto-title/deletestore 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 -- --nocapture20 passed;cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_client_acp -- --nocapture10 passed;cargo test --manifest-path rust/Cargo.toml -p mnote-web session -- --nocapture46 passed;cargo test --manifest-path rust/Cargo.toml -p mnote-web local_access_policy -- --nocapture2 passed;cargo test --manifest-path rust/Cargo.toml -p mnote-web share_link -- --nocapture1 passed;迁移 dry-run 与npm run check:local-first-convex-guardpassed;cargo fmt --manifest-path rust/Cargo.toml --all已执行。
- 2026-05-22:Codex 追加完成 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均已确认只启动 Rustmnote-webhot 链路,不启动 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 源码回流默认控制面。
- 根