# 2-9 Turso / libSQL 控制面切换 checklist v1 > 创建时间:2026-07-02 > > 当前状态:`process` > > 目标:在接受 Turso / libSQL Beta 与云服务风险的前提下,把 MNote 当前 Rust SQLite 控制面切换到 Turso / libSQL 后端,降低多个进程直接写同一个 SQLite 文件带来的损坏风险,并为后续云同步、审计、权限和 AI 控制面维护提供更稳的存储边界。 > > 上位依据: > - `/mnt/Data1T/mnote/ARCHITECTURE.md`(`CURRENT_ARCHITECTURE.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` --- ## 1. 结论 当前可以开始转向 Turso,但迁移边界必须收紧: > **第一阶段只切 MNote control-plane,不迁移 local-first 文件真相,不迁移页面正文、附件、mindmap、Office 文件,不迁移已经退役的 `evidence.sqlite`。** Turso 的收益不在于“SQLite 语法换个驱动”本身,而在于把控制面写入统一收口到受控 store / API 后端,避免脚本、测试、进程和运行时同时直接写同一个 SQLite 文件。若继续保留脚本直接 `sqlite3` 写库、测试临时覆盖 `MNOTE_CONTROL_PLANE_DB_PATH`、OpenHub 直接写自己的 SQLite 文件,那么换 Turso 只能解决一部分问题,不能解决当前最致命的并发写入纪律问题。 2026-07-03 hard cutover 更新:用户已确认不再保留 mnote-web runtime SQLite fallback。当前策略改为 **Turso/libSQL-only runtime**:`dev-hot`、`desktop-hot`、prod 和 mnote-web 默认 `libsql-local`,显式 `MNOTE_CONTROL_PLANE_BACKEND=sqlite` 必须拒绝启动;SQLite 仅保留给 `control-plane-admin` 迁移/导出和 Rust 测试隔离。 2026-07-03 补充决策:已经获得真实 Turso `mnote` dev DB URL/token,并完成 remote dry-run 连接验证;但分叉讨论与本轮评估结论一致,**Turso Cloud 对当前 MNote 的意义不是“立刻把产品变成云同步应用”**。当前阶段仍以 local-first 文件真相和本机开发稳定性为主,云端能力只作为 control-plane 的受控验证、备份、迁移演练和后续多设备准备层。 因此默认路线调整为: - `dev-hot`、`desktop-hot` 和 prod runtime 默认使用 `libsql-local`,覆盖本地 store、migration 和脚本纪律;不再保留 runtime SQLite fallback。 - `desktop-hot` 不立即切到 `turso-remote` 或 `turso-local-replica`。 - Turso remote 只做 dev DB dry-run、迁移演练、回滚演练和后续受保护 smoke,不作为当前本地功能测试的阻塞项。 - local-first 文件正文、附件、mindmap、OnlyOffice 仍完全留在本地文件系统,不迁入 Turso。 推荐执行顺序: 1. 先把 `ControlPlaneStore` 后端抽象和脚本直写清掉。 2. 再新增 Turso / libSQL store。 3. 本地默认先切 `libsql-local`,真实 Turso remote 只做受控验证。 4. 等密码哈希、remote smoke、回滚导出和 token 管理都稳定后,再评估是否让 `turso-local-replica` 成为普通默认。 5. 最后评估 OpenHub 会话库是否单独迁移。 --- ## 2. 范围 ### 2.1 必迁移 - Rust `control-plane` crate 当前的 SQLite store: - users / password identities / sessions - workspaces / workspace members - directory grants / share links - sync state / outbox / audit log - sidebar shortcuts / UI preferences / navigation recent - AI policy / AI runtime run 和 event - AI agent profile access - external conversation bindings - `mnote-web` 对 control-plane 的构造、注入和测试 helper。 - 所有直接 `sqlite3` 写 control-plane 的 smoke / 测试脚本。 - 启动脚本和部署脚本里的 `MNOTE_CONTROL_PLANE_DB_PATH` 默认路径逻辑必须移除;运行时只读取 libSQL/Turso env。 ### 2.2 第一阶段不迁移 - 本地 `.md` 页面正文。它仍是 local-first 正文真相。 - 本地附件、图片、mindmap、OnlyOffice 文件。 - `.mnote/index/evidence.sqlite`。该库已从当前主线退役,本轮不迁移。 - LightRAG 自身存储。知识库/RAG provider 存储不在 control-plane 切换范围内。 - OpenHub 自身 SQLite 会话库。它可以作为第二阶段单独迁移,不应和 Rust control-plane 首次切换绑死。 - RAGFlow。当前不作为主线迁移目标。 - 浏览器直连 Turso。所有读写仍必须经过 Rust / OpenHub API。 --- ## 3. 目标架构 ```text Browser / Desktop Shell / Agent | v Rust mnote-web :3000 - auth/session - workspace/profile - access policy - share/invite - sync state - AI policy/runtime - audit/outbox | v ControlPlaneStore trait | +-- SqliteControlPlaneStore 仅限 admin 迁移 / 导出 / Rust 测试隔离 | +-- TursoControlPlaneStore 默认目标后端 ``` 推荐环境变量: ```text MNOTE_CONTROL_PLANE_BACKEND=libsql-local|turso-remote|turso-local-replica|turso-synced MNOTE_TURSO_LOCAL_PATH=/mnt/Data1T/Mnote_data/control-plane/control-plane-libsql.db MNOTE_TURSO_DATABASE_URL=libsql://... MNOTE_TURSO_AUTH_TOKEN=... MNOTE_TURSO_LOCAL_REPLICA_PATH=/mnt/Data1T/Mnote_data/control-plane/control-plane-replica.db ``` 默认策略建议: - 当前已落地:`dev-hot`、`desktop-hot` 和 prod runtime 默认 `libsql-local`。 - 已配置真实 Turso dev DB:仅用于 remote dry-run、迁移演练、回滚演练和受保护 smoke,不改变 local-first 主路径。 - Turso store 全测试通过后:CI 先跑 `sqlite` + `libsql-local`;`turso-dev` 作为需要 secret 的受保护 job。 - 稳定后才评估:普通运行是否从 `libsql-local` 提升为 `turso-local-replica`;不恢复 SQLite runtime fallback。 本轮新增云端边界: - remote / replica / synced 更接近“控制面云备份、迁移演练、多设备准备层”,不是本地文件网盘。 - control-plane 可以进入 Turso;正文、附件、mindmap、OnlyOffice、OpenHub 会话库和 RAG 存储不能因为这次 cutover 顺带迁入。 - remote 上线前必须先完成密码哈希从 `sha256-v1` 升级到 Argon2id,避免把弱哈希带入云端控制面。 - token 只允许存放在本机私有 env 或 secret manager,不写入仓库、日志、截图和设计文档。 --- ## 4. 当前代码事实 ### 4.1 Rust control-plane - `rust/crates/control-plane/src/store.rs` - `ControlPlaneStore` 是同步 trait,当前是正确抽象边界,应保持接口稳定。 - `rust/crates/control-plane/src/sqlite.rs` - 唯一实现是 `SqliteControlPlaneStore`。 - 内部是 `Mutex`。 - 当前配置 `journal_mode=WAL`、`synchronous=NORMAL`、`foreign_keys=ON`、`busy_timeout=5000`。 - `rust/crates/control-plane/src/migrations.rs` - 直接依赖 `rusqlite::Connection`。 - `rust/crates/control-plane/src/error.rs` - 直接实现 `From`。 - `rust/crates/control-plane/Cargo.toml` - 当前声明 `rusqlite = { version = "0.34", features = ["bundled"] }`。 ### 4.2 Rust mnote-web - `rust/crates/mnote-web/src/app.rs` - 当前直接导入 `SqliteControlPlaneStore`。 - `AppState.control_plane` 当前是 `Arc`,应改成 `Arc`。 - 历史状态:`open_control_plane_store()` 曾读取 `MNOTE_CONTROL_PLANE_DB_PATH` 并打开 SQLite;hard cutover 后该路径已删除。 - `rust/crates/mnote-web/src/local_folder_watcher_registry.rs` - 字段已经是 `Arc`,但 `new()` 参数仍偏向具体 SQLite 类型,需要收口。 - `rust/crates/mnote-web/src/routes/local_search_index.rs` - 历史上使用过 `rusqlite` 处理 `evidence.sqlite`;该库已退役,不能作为 Turso 迁移目标。 - `rust/crates/mnote-web/src/routes/local_folder_source.rs` - 部分 best-effort 索引刷新路径会自行打开 control-plane store;Phase 1 已适配 trait object,后续可评估复用 `AppState` 中的 store。 - `rust/crates/mnote-web/src/ssr/pages/admin.rs` - admin 页面存在 control-plane DB 路径展示逻辑;Turso 默认后端切换时需要改成后端状态展示,不应继续只显示 SQLite 文件路径。 ### 4.3 脚本和 smoke 当前至少有两类脚本必须改: - 直接调用 `sqlite3` CLI 写库。 - 通过临时 `control-plane.sqlite3` 和 `MNOTE_CONTROL_PLANE_DB_PATH` 绕过真实 store;hard cutover 后运行时启动链不得再接受该方式。 这些脚本如果不改,即使 runtime 切 Turso,测试和开发链路仍会制造第二套写入口。 --- ## 5. Phase 0:风险登记和依赖确认 - [x] 确认 Turso / libSQL 的当前 Beta 风险:API 稳定性、SLA、区域、限流、连接数、备份能力、最大数据库大小。 - 当前记录为运行手册中的准入风险:真实 remote / replica / synced 作为云端同步/备份/多设备准备层后续启用;不保留 mnote-web runtime SQLite fallback,也不把本地文件真相迁入 Turso。 - [x] 固定 Rust `libsql` crate 版本,不使用浮动主分支依赖。 - [x] 确认当前 8 个 migration SQL 在 libSQL 上可执行。 - [x] 明确 remote-only、local replica、pure local 三种模式的行为差异。 - [x] 明确 token 管理规则:只走环境变量或本机 secret 文件,不写入仓库。 - [x] 记录故障边界:认证失败、网络超时、Turso 5xx、限流、schema migration 失败、local replica 同步失败。 - 代码已覆盖 missing/empty URL/token、timeout/rate-limit error mapping、invalid local path;真实 5xx/限流/同步失败属于后续云端验证,不阻塞本地 control-plane 功能测试。 - [x] 决定第一阶段是否允许离线写入。当前策略:`libsql-local` 允许本地离线写入;`turso-local-replica` 模式下本地 replica 可离线读、上线后同步;纯 `turso-remote` 模式依赖网络,失败时返回明确错误。 验收: - [x] 已形成 Turso dev 数据库和 dev token,用于云端同步/备份链路验收。 - 本机私有 env:`/mnt/Data1T/Mnote_data/control-plane/turso.env`,权限 `0600`,不入仓库。 - 数据库 URL host 已验证为 `mnote-liaibo.aws-ap-northeast-1.turso.io`,token 不写入设计文档。 - [x] 后续 `libsql` 最小连接 demo 能跑通 dev 数据库。 - 2026-07-03 已执行 `control-plane-admin init --backend turso-remote --dry-run`,返回 `ok=true`。 - 2026-07-03 已执行 `control-plane-admin migrate-sqlite-to-target --backend turso-remote --dry-run`,返回源库表计数和 `targetBackend=TursoRemote`。 - [x] 本机无 token 时不会 panic,返回 `InvalidInput`;真实断网场景作为后续 Turso dev DB 验证。 --- ## 6. Phase 1:先加固 ControlPlaneStore 边界 目标:在不改变数据库后端的情况下,先把 runtime 从具体 SQLite 类型中解耦。 - [x] `rust/crates/mnote-web/src/app.rs` - [x] `AppState.control_plane: Arc` 改为 `Arc`。 - [x] `open_control_plane_store()` 改为返回 trait object。 - [x] 新增 `MNOTE_CONTROL_PLANE_BACKEND` factory 入口,已实现 sqlite/libsql-local/turso-remote/turso-local-replica/turso-synced 全部分支。 - [x] 当前默认 SQLite 行为与现状一致。 - [x] `rust/crates/mnote-web/src/local_folder_watcher_registry.rs` - [x] `new()` 参数改为 `Arc`。 - [x] watcher 测试通过 trait object 构造器编译与运行。 - [x] `rust/crates/mnote-web/src/routes/local_search_index.rs` - [x] 已复核:该文件中的 SQLite 写入仅限 `#[cfg(test)]` 的 `evidence.sqlite` / local search legacy 测试,不是 control-plane DB 直写;本轮不迁移。 - [x] `rust/crates/control-plane/src/lib.rs` - [x] 保持 `SqliteControlPlaneStore` 导出,新增后端前不破坏现有测试。 验收: - [x] `cargo test --manifest-path rust/Cargo.toml -p control-plane` - [x] `cargo test --manifest-path rust/Cargo.toml -p mnote-web` 已运行并确认本轮 control-plane 相关路径无新增失败。 - 全量结果为 855 passed, 1 failed;唯一失败是既存 CSS 体积阈值 `ssr::styles::tests::mnote_css_is_reasonably_sized`,与 control-plane cutover 无关。 - [x] `MNOTE_CONTROL_PLANE_BACKEND=sqlite npm run desktop:hot` 已改为拒绝启动,防止运行时回落到 SQLite。 --- ## 7. Phase 2:新增 TursoControlPlaneStore 目标:新增 Turso/libSQL 实现,并在 mnote-web runtime 中移除 SQLite fallback。 - [x] `rust/crates/control-plane/Cargo.toml` - [x] 增加 `libsql` 依赖。 - [x] 保留 `rusqlite` 仅服务 control-plane-admin 迁移/导出和 Rust 测试隔离。 - [x] `rust/crates/control-plane/src/turso.rs` - [x] 新增 `TursoControlPlaneStore`。 - [x] 支持 remote URL + token。 - [x] 支持 local replica path + remote sync URL。 - [x] 支持 pure local libSQL 模式,用于无云测试。 - [x] 实现 `ControlPlaneStore` 全部方法。 - [x] `rust/crates/control-plane/src/migrations.rs` - [x] 从 `rusqlite::Connection` 绑定改为后端无关迁移入口,或新增 libSQL 迁移入口。 - [x] PRAGMA 只在本地 SQLite / local replica 下执行;remote Turso 跳过 WAL 类配置。 - [x] `rust/crates/control-plane/src/error.rs` - [x] 增加 `From`。 - [x] 区分 constraint、not found、auth、network、timeout、rate limit。 - [x] `rust/crates/control-plane/src/lib.rs` - [x] 导出 `TursoControlPlaneStore`。 设计注意: - 当前 trait 是同步接口。第一阶段建议保持同步边界,避免把所有 route 改成异步 store trait。 - 如果 `libsql` API 只能异步访问 remote,应在 store 内部集中处理 async runtime 包装,不要让每个 route 自己 block。 - 多语句写入必须明确事务边界;不能把原来 SQLite 单连接串行行为误拆成多个独立远程请求。 验收: - [x] `cargo test --manifest-path rust/Cargo.toml -p control-plane --features turso-unit-tests -- --test-threads=1` - [x] 同一套 store 行为测试已在本地可测后端运行:`sqlite` ✅ / `libsql-local` ✅ / feature-gated local Turso store tests ✅。 - [ ] 后续云端验证:同一套 store 行为测试在 `turso-dev` 后端运行。 - `turso-dev` 需要 dev DB/token,不阻塞本地功能测试。 - [ ] migration idempotent 测试在 Turso dev 数据库通过。 - `libsql-local` migration idempotent ✅;真实 Turso dev DB 为后续云端验证项(缺 dev DB/token) --- ## 8. Phase 3:移除脚本直写数据库 目标:所有测试和 smoke 通过 API / helper seed 数据,不再直接写 control-plane DB 文件。 ### 8.1 直接 `sqlite3` CLI 用户 以下脚本必须改为调用 Rust API、测试 seed endpoint,或统一 helper: - [x] `scripts/task557-page-ai-run-resume-smoke.js` - [x] `scripts/task558-page-ai-reasonix-live-session-context-smoke.js` - [x] `scripts/task559-page-ai-terminal-status-reconciliation-smoke.js` - [x] `scripts/task560-page-ai-hermes-load-replay-smoke.js` - [x] `scripts/task561-page-ai-session-dashboard-smoke.js` - [x] `scripts/task562-page-ai-reasonix-approval-plan-smoke.js` - [x] `scripts/task512-chatonly-doubao-sync-smoke.js` - [x] `scripts/task513-chatonly-provider-sync-smoke.js` - [x] `scripts/task527-chatonly-api-provider-smoke.js` - [x] `scripts/task530-knowledge-rag-page-ai-final-answer-smoke.js` ### 8.2 临时 DB path 覆盖用户 以下脚本必须改为使用统一 test control-plane backend,不再自己创建临时 SQLite 文件后覆盖环境变量: - [x] `scripts/task492-sidebar-starred-shortcuts-smoke.js` - [x] `scripts/task494-filetree-lazy-loading-dedup-smoke.js` - [x] `scripts/task496-editor-open-parallel-runtime-aggregate-smoke.js` - [x] `scripts/task497-local-page-tree-filetree-open-performance-smoke.js` - [x] `scripts/task498-starred-page-tree-scope-and-local-edit-smoke.js` - [x] `scripts/task499-sidebar-tree-view-state-smoke.js` - [x] `scripts/task500-navigation-page-route-guard-smoke.js` - [x] `scripts/task167-local-markdown-title-body-options-no-convex-smoke.js` ### 8.3 启动和部署脚本 - [x] `scripts/prod-build-start.js` - [x] 不再只设置 `MNOTE_CONTROL_PLANE_DB_PATH`。 - [x] 支持 `MNOTE_CONTROL_PLANE_BACKEND` 和 Turso env。 - [x] `scripts/desktop-hot.js` - [x] 明确 dev 默认后端。 - [x] 防止 hot/prod runtime 再次写 SQLite control-plane 文件。 - [x] `scripts/dev-hot.js` - [x] 同步 backend 环境变量策略。 验收: - [x] `rg -n "sqlite3|MNOTE_CONTROL_PLANE_DB_PATH|control-plane\\.sqlite3|control-plane\\.db" scripts rust/crates --glob '!target/**' --glob '!recycle/**'` 中不再出现未解释的直写入口。 - [x] 需要 seed 的脚本统一走 `scripts/lib/control-plane-dev-seed.js` / `scripts/lib/control-plane-test-env.js` 或测试 API。 - [x] smoke 失败时不再留下孤立临时 control-plane DB(`scripts/task-control-plane-admin-libsql-roundtrip-smoke.js` 使用 `/tmp` 前缀并在 finally 清理)。 --- ## 9. Phase 4:数据初始化和迁移 当前仍是测试期,不需要保留正式业务数据,但仍需要迁移工具来保证以后可运维。 - [x] 新增 control-plane 初始化命令: - [x] 创建 schema。 - [x] 创建默认 admin / e2e 用户。 - [x] 创建默认 workspace / membership。 - [x] 写入必要 AI policy / UI preference 默认值。 - [x] 新增本地 SQLite 到 Turso 的一次性迁移工具: - [x] 支持 `--dry-run`。 - [x] 输出源表行数、目标表行数、跳过项、失败项。 - [x] 支持目标清空后重建 dev DB。 - [x] 新增 Turso 到 SQLite 的回滚导出工具: - [x] 回滚前自动备份本地 DB。 - [x] 支持只导出 control-plane schema。 验收: - [ ] 后续云端验证:空 Turso dev DB 可通过初始化命令启动 MNote(缺 `MNOTE_TURSO_DATABASE_URL` / `MNOTE_TURSO_AUTH_TOKEN`)。 - [ ] 初始化后的测试账号 `mnote.e2e@example.com` 可登录。 - 后续云端验证:同 Turso dev DB/token 缺 - [x] libSQL/Turso 可导出为 SQLite 备份;SQLite 备份不再作为 mnote-web runtime fallback。 --- ## 10. Phase 5:并发、损坏和故障测试 目标:证明切换确实解决当前痛点,而不是只换驱动名。 - [x] 并发写测试: - [x] 多线程同时创建 session(libSQL local)。 - [x] 多线程同时 upsert sidebar shortcut(libSQL local)。 - [x] 多线程同时写 AI runtime events(libSQL local)。 - [x] 多线程同时 append audit/outbox(libSQL local)。 - [ ] 故障注入: - [x] Turso token 缺失返回 `InvalidInput` 且不 panic。 - [ ] Turso URL 不可达。 - 后续云端验证:缺 Turso dev DB(libSQL local 模式下无远程连接) - [ ] 网络超时。 - 后续云端验证:缺 Turso dev DB - [ ] 远端返回 5xx。 - 后续云端验证:缺 Turso dev DB - [ ] 远端限流。 - 后续云端验证:缺 Turso dev DB - [ ] local replica 文件不可写。 - 后续云端验证:缺 Turso dev DB - [x] 数据一致性: - [x] unique constraint 冲突仍返回可处理错误。 - [x] session 创建和查询一致。 - [x] outbox 不重复投递。 - [x] AI runtime event 顺序可恢复。 验收: - [x] 新增 `cargo test -p control-plane` 并发测试。 - [x] 新增 Node smoke 覆盖 admin init / SQLite→libSQL local / libSQL local→SQLite roundtrip,且失败时清理临时库。 - [x] 已覆盖缺 token 时不 panic;远端网络/5xx/限流作为后续真实 Turso dev DB 验证。 --- ## 11. Phase 6:默认切换 > dev-hot.js 已默认使用 `MNOTE_CONTROL_PLANE_BACKEND=libsql-local`(`dev-hot.js:75`);desktop-hot.js 已默认 `libsql-local`。remote / local-replica / synced 更接近云端同步、备份和多设备准备层,不影响当前 local-first 本地功能测试;它们作为后续云端验收项,不再阻塞本轮本地 libSQL cutover。 > > 2026-07-03 决策更新:真实 Turso dev DB/token 已可用,但云端价值对当前阶段有限;本轮只把它接入为“可验证但不默认”的控制面 dev target,不把 remote / replica 作为 local-first 功能测试前置条件。 - [x] dev 环境先切到本地 libSQL: - [x] `MNOTE_CONTROL_PLANE_BACKEND=libsql-local` - [x] 不依赖 Turso dev DB/token,覆盖本地 store、migration 和 script discipline,不保留 runtime SQLite fallback。 - [x] Turso dev DB/token 已配置为本机私有 env: - [x] `/mnt/Data1T/Mnote_data/control-plane/turso.env` - [x] 文件权限 `0600` - [x] repo 内未出现 JWT/token 明文命中。 - [x] Turso remote 最小验证: - [x] `control-plane-admin init --backend turso-remote --dry-run` 返回 `ok=true`。 - [x] `control-plane-admin migrate-sqlite-to-target --backend turso-remote --dry-run` 成功读取本地 SQLite 源库并规划导入 remote。 - [ ] 后续云端同步/备份链路切换(低优先级,非当前默认): - [ ] `MNOTE_CONTROL_PLANE_BACKEND=turso-local-replica` - [ ] 先完成 Argon2id password migration。 - [ ] 观察登录、workspace、授权、分享、sidebar、navigation、AI runtime。 - [ ] 验证 replica initial sync、`MNOTE_TURSO_SYNC_INTERVAL_MS`、断网和 token 失效边界。 - [ ] CI 增加后端矩阵: - [ ] `sqlite` - [ ] `libsql-local` - [ ] `turso-dev` 作为后续受保护云端 job,需要 secret 时才跑;失败不阻塞本地 local-first smoke。 - [ ] 本机正式默认切换到 remote / replica 前(后续云端验证): - [ ] 备份当前 `/mnt/Data1T/Mnote_data/control-plane/control-plane.db`。 - [ ] 记录当前 schema version。 - [ ] 跑完整 smoke 基线。 默认切换验收: - [x] 本地 cutover:`libsql-local` store 行为、migration、admin roundtrip、seed helper 和 SQLite runtime 拒绝路径已验证。 - [ ] 云端 cutover:`npm run desktop:hot` 使用 `turso-local-replica` 启动后可登录测试账号。 - [ ] 云端 cutover:`/api/auth/session` 返回真实 Turso control-plane session。 - [x] 文件树和文档页仍读取本地工作区文件。 - [x] 新建/重命名/打开页面不依赖 Turso 存正文。 - [ ] 云端 cutover:断开 Turso 后错误边界符合 Phase 0 决策。 ### 11.1 当前不做的 Turso 云端事项 - 不把 remote Turso 当成“网盘备份”替代本地文件系统;它只保存 control-plane 元数据。 - 不把 remote / replica / synced 设为 `desktop-hot` 默认。 - 不把 OpenHub SQLite、local search、RAG provider 数据跟随 control-plane 一起迁移。 - 不在 repo 内保存真实 token;`tmp/turso.md` 和 `turso.env` 都属于本机私有文件。 ### 11.2 下一轮可执行 checklist - [x] `rust/crates/mnote-web/src/app.rs`:解析 `MNOTE_TURSO_SYNC_INTERVAL_MS`,传入 remote replica / synced builder。 - [ ] `rust/crates/control-plane/src/turso.rs`:remote replica 建库时启用 explicit initial sync;必要时启用 `read_your_writes(true)`。 - [ ] `rust/crates/mnote-web/src/routes`:新增安全的 control-plane health endpoint,暴露 backend/mode/schema version/sync status,不暴露 token。 - [ ] `rust/crates/control-plane`:补真实 remote dev DB 的 store contract smoke,作为 secret-gated 测试。 - [ ] `rust/crates/control-plane`:补 Argon2id password hash migration,再允许 remote 成为长期候选默认。 - [ ] `docs/operations/control-plane-turso.md`:记录本机私有 env、remote dry-run、低优先级云端边界和 token 防泄露检查。 ### 11.3 Turso / libSQL 优势落地状态校准 > 2026-07-03 校准:上一轮 P0/P1/P2 是“最值得利用的能力清单”,不是全部已实现清单。当前已完成的是 local cutover、脚本直写清理、local libSQL 并发防损坏测试和 remote dry-run;读写并发性能设计、sync health、Argon2id、CDC/Tantivy 都尚未落地。 | 优先级 | 能力 | 当前状态 | 结论 | | --- | --- | --- | --- | | P0 | 唯一写入口 / 脚本不直写 control-plane DB | 已完成 | 继续保持所有 seed / smoke 走 API、helper 或 `control-plane-admin`。这是降低 SQLite 损坏风险的核心收益。 | | P0 | `libsql-local` store + migration + browser QA | 已完成 | 当前可作为本地测试主线;不依赖 Turso Cloud。 | | P0 | 并发防损坏测试 | 已完成 local 版 | `rust/crates/control-plane/tests/turso_store.rs` 已用多线程同时创建 session、shortcut、AI runtime events、audit/outbox,验证 local libSQL store 在当前单进程并发下不损坏。 | | P0 | 真实 Turso remote 凭据 dry-run | 已完成 non-destructive 版 | 已验证 URL/token 可用,`init --dry-run` 与 `migrate-sqlite-to-target --dry-run` 成功;未把 remote 设为默认。 | | P0 | `MNOTE_TURSO_SYNC_INTERVAL_MS` / initial sync | 已完成:env var 解析与传递 | `mnote-web/src/app.rs` 已解析 `MNOTE_TURSO_SYNC_INTERVAL_MS` 并传给 remote replica / synced;explicit initial sync 仍待后续验证。 | | P0 | control-plane health endpoint | 未完成 | 需要新增只读诊断端点,暴露 backend/mode/schema/sync 状态,不暴露 token。 | | P0 | Argon2id password hash migration | 未完成 | remote 成为长期默认前必须完成,避免把 `sha256-v1` 占位哈希带入云端。 | | P1 | 读写并发性能设计 | 仅完成安全基线,未实现性能优化 | 当前 `TursoControlPlaneStore` 是 `Mutex`,读写全部串行;这能保证简单安全,但不能发挥 remote / replica 的读并发优势。 | | P2 | CDC / change data capture | 未采用 | 当前 control-plane 已有显式 `audit_log` 和 `outbox_events`,CDC 只适合作为后续审计/同步 spike,不进入本轮。 | | P2 | Tantivy-powered full-text search | 未采用 | 当前 control-plane 表主要是元数据,全文搜索收益有限;正文和知识库搜索仍属于 OpenHub/LightRAG/local search 方向,不跟 control-plane 绑死。 | | P3 | Browser WASM / OPFS database | 未采用 | 对纯 Web 离线笔记有价值,但当前 MNote 主形态是本地文件夹 + Rust mnote-web + 文件系统 watcher;浏览器内 SQL 不能替代本地 `.md` 真相,也不适合作为 control-plane 权限真源。 | ### 11.4 读写并发设计原则 当前实现优先解决“不要损坏”和“不要绕过写入口”,不是追求最大吞吐: - `TursoControlPlaneStore` 现在用单个 `Mutex` 串行化所有 control-plane 操作。 - 这能避免本轮最致命的问题:多个运行时、脚本或测试直接写同一个 SQLite 文件导致损坏。 - 这也意味着 remote / replica 模式下,读操作暂时不能和写操作并行,属于保守实现。 后续若要优化读写并发,必须按以下顺序推进: - [ ] 先把 `ControlPlaneStore` 方法按只读 / 写入 / 多表事务分类,形成文件级清单。 - [ ] 写入路径继续保持单写者队列或独占写锁;多表写入必须显式事务,不允许拆成多个独立锁周期。 - [ ] 只读路径可以引入独立 read connection pool 或 `RwLock`,但必须先证明 libSQL remote / replica 连接在当前 Rust SDK 版本下可安全共享。 - [ ] remote replica 只把读放在本地副本;写仍以 remote primary 为准,不把 embedded replica 当成高并发本地写主路径。 - [ ] 在改 `Mutex -> RwLock/read pool` 前,先补 benchmark 和压力测试:并发 session lookup、grant resolve、navigation recent、AI event append、audit/outbox drain。 - [ ] 在同步 trait 未改 async 前,不扩大 `block_in_place` 使用面;否则大量并发请求可能耗尽 Tokio worker。 推荐下一步不是直接把 `Mutex` 改成 `RwLock`,而是先加: - `MNOTE_TURSO_SYNC_INTERVAL_MS` - explicit initial sync - control-plane health endpoint - secret-gated remote store contract smoke 这些完成后,再评估读连接池或 async store trait。 ### 11.5 CDC 与 Tantivy FTS 评估 #### CDC / Change Data Capture Turso 文档中的 CDC 通过 `PRAGMA capture_data_changes_conn(...)` 按连接开启,把 insert/update/delete/DDL 变更写入 CDC 表;它尊重事务边界,rollback 不产生 CDC 记录。当前本地文档还明确 CDC 与 MVCC 在同一连接上互斥。 对 MNote 的判断: - 当前不作为 P0:control-plane 已有业务语义明确的 `audit_log` 和 `outbox_events`,比通用 CDC 更适合作为权限、分享、AI policy 的审计与事件源。 - 可作为 P2 spike:如果后续要把 control-plane 变更桥接到 OpenHub、实时分析、外部消息队列或审计归档,可以评估 CDC 作为补充。 - 不替代业务审计:CDC 记录的是表级变更,不知道“用户授权目录”“AI policy 变更”“分享撤销”等业务意图,仍需要应用层 audit/outbox。 - 不进首轮 remote:CDC 仍是早期能力,且可能捕获敏感字段;必须先定义字段脱敏、保留期、权限和导出路径。 结论:**P2 / research only**,不进入当前 Turso control-plane 默认切换。 #### Tantivy-powered Full-text Search Turso 文档显示 FTS 通过 `CREATE INDEX ... USING fts` 和 `fts_match` / `fts_score` / `fts_highlight` 使用 Tantivy,不是 SQLite FTS5 的完全同语法替换。 对 MNote 的判断: - 当前 control-plane 不需要:users、workspaces、grants、recent、shortcuts、policy 都是轻量元数据,`LIKE` 或精确索引足够。 - 不迁正文:页面正文 `.md` 仍在本地文件系统,知识库问答主线是 OpenHub/LightRAG,不把正文全文塞进 control-plane。 - 可作为 P2/P3:如果以后要做轻量“标题 / 文件路径 / 页面摘要 / AI 会话标题”的本地快速搜索,可以单独设计 Turso FTS 索引。 - 不替代 LightRAG/OpenHub RAG:Tantivy FTS 是关键词检索,不等于知识库引用、OCR、语义检索和 agent citation 链。 结论:**对当前 control-plane 价值低;对未来轻量元数据搜索有价值;不进入本轮。** #### Browser WASM / OPFS Turso / libSQL 的 Browser WASM + OPFS 路线适合“纯浏览器应用也要持久化 SQL 数据库”的场景。官方资料也能看到 browser WASM 示例、`@tursodatabase/sync-wasm` 和本地浏览器数据库方向;Context7 返回的 sync 文档强调 synced database 可做离线写入再同步。 对 MNote 的判断: - 当前不作为 P0/P1:MNote 的主形态不是纯浏览器笔记应用,而是 `3000 Rust SSR + local-first workspace + 本地文件系统`。本地 `.md`、附件、mindmap、OnlyOffice 已经有真实文件真相和 watcher,同步到浏览器 OPFS 反而会制造第三份数据真相。 - 不替代 control-plane:auth、session、grant、AI policy 仍必须在 Rust control-plane 中受控;浏览器 OPFS 数据库不能成为权限真源,否则会弱化服务端校验和 agent 访问边界。 - 不适合当前桌面链路:浏览器 WASM/OPFS 往往受浏览器 storage quota、origin 隔离、清站点数据、cross-origin isolation / SharedArrayBuffer 等运行条件影响;这些不如当前本地文件系统路径可审计、可备份、可由 agent 直接 patch。 - 可作为 P3 独立 spike:如果未来要做“无桌面端 / 无本地服务”的纯 Web 离线阅读、临时草稿、移动端 PWA、离线收集箱,可以评估 WASM OPFS 存储轻量 cache、草稿或最近访问索引。 - 不存敏感真相:即使做 P3,也只应存可重建 cache / draft / projection,不存长期 auth token、分享授权真相、完整知识库和不可恢复正文唯一副本。 结论:**对当前网页中的笔记系统不是主线优化;对未来纯 Web/PWA 离线模式有研究价值,归 P3 spike。** --- ## 12. Phase 7:OpenHub 会话库单独评估 OpenHub 属于当前 AI 主线,但它不是 Rust control-plane。不能把 Rust control-plane 切换和 OpenHub SQLite 切换混成一个不可回滚的大改。 > 当前 Phase 7 为独立后续评估:OpenHub 不属于 Rust control-plane,其 SQLite 会话库迁移不绑定 Rust control-plane 切换。OpenHub 侧直写 SQLite 的文件路径和 schema 需单独审计(属于 OpenHub 自身治理范围);Rust control-plane 本地 libSQL cutover 已完成,remote / replica / synced 仅作为后续云端同步/备份验收项,不影响 OpenHub 单独评估。 - [ ] 先确认 OpenHub 当前数据库文件、schema 和写入路径。 - [ ] 确认哪些数据属于可丢弃会话缓存,哪些属于用户需要保留的 AI 历史。 - [ ] 新增 `OPENHUB_DB_BACKEND=sqlite|turso`,默认先保持 `sqlite`。 - [ ] OpenHub Python 侧增加 Turso/libSQL client abstraction。 - [ ] 会话创建、消息写入、会话恢复、MNote 嵌入 Page AI 全链路验证。 - [ ] 若 OpenHub 会话写入频率高于 control-plane,单独做性能和限流评估。 OpenHub 切换门槛: - [ ] Rust control-plane Turso 已稳定。 - [ ] OpenHub 不再通过脚本直接改 SQLite。 - [ ] Page AI / OpenHub smoke 可在 Turso 后端通过。 --- ## 13. Phase 8:文档和运行手册 - [x] 更新 `ARCHITECTURE.md`: - [x] control-plane 从 `Rust SQLite` 改为 `Turso/libSQL-only runtime`。 - [x] 明确本地 `.md` 文件仍是正文真相。 - [x] 更新 `CURRENT_ARCHITECTURE.md`: - [x] 当前默认后端、SQLite admin-only 边界和备份恢复方式。 - [x] 更新 `AGENTS.md`: - [x] 常用命令增加 Turso backend env。 - [x] smoke 基线说明不得直写 control-plane DB。 - [x] 新增运行手册:`docs/operations/control-plane-turso.md` - [x] dev token 配置。 - [x] dev DB reset / init。 - [x] SQLite 导出备份。 - [x] Turso 到 SQLite 回滚。 - [x] 故障排查。 --- ## 14. 备份与恢复策略 hard cutover 后不再保留 mnote-web runtime SQLite 一键回滚;SQLite 只作为导出备份格式和迁移源。 ```bash cargo run --manifest-path rust/Cargo.toml -p control-plane --bin control-plane-admin -- \ export-target-to-sqlite --backend libsql-local --output /tmp/mnote-control-plane-backup.sqlite --backup-existing ``` 恢复 checklist(仅在 remote / replica / synced 成为默认后需要完整执行): - [ ] 从 Turso/libSQL 导出最新 control-plane 备份。 - [ ] 保留当前 libSQL/Turso 目标库快照。 - [ ] 将备份重新导入新的 libSQL/Turso 目标库,或接受 dev 数据重置。 - [ ] 重启 `npm run desktop:hot`。 - [ ] 验证登录、workspace、授权、sidebar、navigation、AI policy。 - [ ] Turso DB 保留至少 30 天用于审计和补导出。 --- ## 15. 最小落地顺序 如果只追求最快降低 SQLite 损坏风险,按这个顺序做: 1. `AppState.control_plane` 改成 `Arc`。 2. 增加 control-plane factory 和 backend env。 3. 改掉 17 个直写/临时 DB 脚本。 4. 增加 `TursoControlPlaneStore`,先跑 libSQL local。 5. 跑 control-plane store 行为测试矩阵。 6. 接 Turso dev DB。 7. 切 `desktop:hot` dev 默认后端。 8. 补并发写和故障注入测试。 9. 再决定是否迁 OpenHub 会话库。 不删除 `SqliteControlPlaneStore` 实现本身,但它不再是 mnote-web runtime fallback;仅作为迁移源、导出备份格式和 Rust 测试隔离。 --- ## 16. 执行记录 ### 2026-07-02 Phase 1 起步 - [x] 已将 `rust/crates/mnote-web/src/app.rs` 的 `AppState.control_plane` 解耦为 `Arc`。 - [x] 已将 `open_control_plane_store()` 改为返回 trait object,内部仍使用 `SqliteControlPlaneStore`,未引入 Turso 实现。 - [x] 已新增 `MNOTE_CONTROL_PLANE_BACKEND` factory 入口,Phase 2 已完成 sqlite/libsql-local/turso-remote/turso-local-replica/turso-synced 全部分支(详见下文全 Phase 同步记录)。 - [x] 已将 `rust/crates/mnote-web/src/local_folder_watcher_registry.rs` 构造器参数改为 `Arc`。 - [x] 已适配 `rust/crates/mnote-web/src/routes/local_folder_source.rs` 中 best-effort 索引刷新调用。 - [x] 已复核脚本直写入口:P0 为 10 个生产 control-plane `sqlite3` 直写 smoke;P1/P2 为 Rust 初始化路径和 8 个临时 DB smoke。 - [x] 验证通过:`cargo check --manifest-path rust/Cargo.toml -p mnote-web`。 - [x] 验证通过:`cargo test --manifest-path rust/Cargo.toml -p mnote-web local_folder_watcher -- --test-threads=1`。 - [x] 验证通过:`cargo test --manifest-path rust/Cargo.toml -p control-plane`。 - [x] 已验证(已知失败):`cargo test --manifest-path rust/Cargo.toml -p mnote-web` 全量为 855 passed, 1 failed(既有 CSS 体积阈值 `ssr::styles::tests::mnote_css_is_reasonably_sized`,断言 `MNOTE_CSS.len() < 190000`,与控制面切换无关)。 ### 2026-07-02 全 Phase 同步与收尾 本轮同步完成了 Phase 2/3/5/6/8 的大幅推进,并更新 checklist 以反映实际代码状态。 **已实现文件(本轮变更范围):** | 文件 | 变更 | |------|------| | `rust/crates/control-plane/Cargo.toml` | 新增 `libsql`、`tokio` 依赖与 `turso-unit-tests` feature | | `rust/crates/control-plane/src/turso.rs` | 4738 行完整 TursoControlPlaneStore 实现:Local/Remote/RemoteReplica/Synced 四种模式 | | `rust/crates/control-plane/src/migrations.rs` | 新增 `run_libsql_migrations()` async 迁移入口 | | `rust/crates/control-plane/src/error.rs` | 新增 `From`,区分 constraint/unauthorized/storage | | `rust/crates/control-plane/src/lib.rs` | 导出 `TursoControlPlaneConfig`、`TursoControlPlaneMode`、`TursoControlPlaneStore` | | `rust/crates/control-plane/src/sqlite.rs` | 保持 SqliteControlPlaneStore 作为 admin 迁移/导出与测试隔离实现,store 行为测试覆盖完整 | | `rust/crates/control-plane/src/model.rs` | 新增字段 | | `rust/crates/mnote-web/src/app.rs` | `AppState.control_plane: Arc` 解耦;5 种后端 factory | | `rust/crates/mnote-web/src/local_folder_watcher_registry.rs` | 构造器改为 `Arc` | | `rust/crates/mnote-web/src/routes/local_folder_source.rs` | best-effort 索引刷新适配 trait object | | `rust/crates/mnote-web/src/ssr/pages/admin.rs` | 适配 control-plane 后端状态展示 | | `docs/operations/control-plane-turso.md` | 新增 Turso/libSQL control-plane 初始化、迁移、回滚、验证和故障边界运行手册 | | `scripts/desktop-hot.js` | 支持 `MNOTE_CONTROL_PLANE_BACKEND`,默认 `libsql-local`,拒绝 `sqlite` | | `scripts/dev-hot.js` | 支持 `MNOTE_CONTROL_PLANE_BACKEND`,默认 `libsql-local` | | `scripts/prod-build-start.js` | 支持 `MNOTE_CONTROL_PLANE_BACKEND` | | `scripts/lib/control-plane-test-env.js` | 新增统一 test env builder,支持全部后端模式 | | `scripts/lib/control-plane-dev-seed.js` | 新增 dev seed API helper,替代 smoke 脚本直接写库 | | `scripts/task-control-plane-admin-libsql-roundtrip-smoke.js` | 新增 admin CLI SQLite → libSQL local → SQLite roundtrip smoke | | `rust/crates/mnote-web/src/routes/dev_seed.rs` | 新增 `/api/dev/seed` gated endpoint 与 targeted tests | | 17 个 smoke 脚本 | 从直接 `sqlite3` CLI 写库改为 API `POST /api/dev/seed` 和 `buildControlPlaneTestEnv` | | `ARCHITECTURE.md` | control-plane 从 `Rust SQLite` 改为 `Turso/libSQL-only runtime` | | `CURRENT_ARCHITECTURE.md` | 记录 ControlPlaneStore trait 架构、后端选择、env vars | | `AGENTS.md` | 常用命令增加 Turso backend env;脚本纪律不得直写 control-plane DB | **已通过测试验证:** - `cargo check --manifest-path rust/Cargo.toml -p mnote-web` ✅ - `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_folder_watcher -- --test-threads=1` ✅ - `cargo test --manifest-path rust/Cargo.toml -p mnote-web app_state_initializes_sqlite_control_plane_store` ✅ - `cargo test --manifest-path rust/Cargo.toml -p mnote-web dev_seed -- --test-threads=1` ✅ - `cargo test --manifest-path rust/Cargo.toml -p control-plane` ✅(28 unit + 3 integration) - `cargo test --manifest-path rust/Cargo.toml -p control-plane --features turso-unit-tests -- --test-threads=1` ✅(64 unit + 3 integration) - `node --test scripts/desktop-hot.test.js` ✅ - `node scripts/task-dev-hot-plan-test.js` ✅ - `node scripts/task-control-plane-admin-libsql-roundtrip-smoke.js` ✅ - `node --check` 覆盖 control-plane helper 与已迁移 smoke 脚本 ✅ - `cargo test --manifest-path rust/Cargo.toml -p mnote-web` ❌ 855 passed, 1 failed(既有 CSS 体积阈值 `MNOTE_CSS.len() < 190000`,与控制面切换无关) **后续云端验证项:** - `MNOTE_TURSO_DATABASE_URL` 与 `MNOTE_TURSO_AUTH_TOKEN` 未配置 → 暂不测试 Turso remote / local-replica / synced 模式;不影响当前本地功能测试 - 真实 Turso dev DB / token 未配置,remote、local-replica、synced、真实 5xx/限流/断网/同步失败作为后续云端验收 - `cargo test --manifest-path rust/Cargo.toml -p mnote-web` 全量 855 passed, 1 failed(既有 CSS 体积阈值失败);本轮 control-plane 相关 targeted tests 已通过 **备份命令(SQLite 只作为导出格式,不作为 runtime rollback):** ```bash cargo run --manifest-path rust/Cargo.toml -p control-plane --bin control-plane-admin -- \ export-target-to-sqlite --backend libsql-local --output /tmp/mnote-control-plane-backup.sqlite --backup-existing ``` **local-first 不迁移边界(已验证未踏入):** - 本地 `.md` 文件正文 → 未迁移 - 本地附件、图片、mindmap、OnlyOffice 文件 → 未迁移 - `.mnote/index/evidence.sqlite`(已退役)→ 未迁移 - LightRAG 自身存储 → 未迁移 - OpenHub 自身 SQLite 会话库 → 未迁移(Phase 7 推迟) - RAGFlow → 未迁移 - 浏览器直连 Turso → 未实现(所有读写经过 Rust API) ### 2026-07-03 最终执行记录 本轮为 stale 状态修正与最终记录归档,不修改代码。 **已修正的 stale 状态:** - [x] `cargo test --manifest-path rust/Cargo.toml -p control-plane` ✅(28 unit + 3 integration) - [x] `cargo test --manifest-path rust/Cargo.toml -p control-plane --features turso-unit-tests -- --test-threads=1` ✅(64 unit + 3 integration) - [x] `cargo test --manifest-path rust/Cargo.toml -p mnote-web` 全量 855 passed, 1 failed(既有 CSS 体积阈值 `MNOTE_CSS.len() < 190000`,与控制面切换无关) - [x] Phase 7 状态修正:OpenHub 不属于 Rust control-plane,其 SQLite 会话库迁移不绑定 Rust control-plane 切换;本地 libSQL cutover 已完成,remote / replica / synced 仅作为后续云端同步/备份验收项 **已通过验证(截至 2026-07-03):** - `cargo check --manifest-path rust/Cargo.toml -p mnote-web` ✅ - `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_folder_watcher -- --test-threads=1` ✅ - `cargo test --manifest-path rust/Cargo.toml -p control-plane` ✅(28 unit + 3 integration) - `cargo test --manifest-path rust/Cargo.toml -p control-plane --features turso-unit-tests -- --test-threads=1` ✅(64 unit + 3 integration) - `cargo test --manifest-path rust/Cargo.toml -p mnote-web dev_seed -- --test-threads=1` ✅ - `node --test scripts/desktop-hot.test.js` ✅ - `node scripts/task-dev-hot-plan-test.js` ✅ - `node scripts/task-control-plane-admin-libsql-roundtrip-smoke.js` ✅ - `MNOTE_CONTROL_PLANE_BACKEND=sqlite npm run desktop:hot` 拒绝启动 ✅ **已知失败:** - `cargo test --manifest-path rust/Cargo.toml -p mnote-web` ❌ 855 passed, 1 failed(既有 CSS 体积阈值 `MNOTE_CSS.len() < 190000`,与控制面切换无关,不属于本轮范围) **后续云端验证项:** - `MNOTE_TURSO_DATABASE_URL` 与 `MNOTE_TURSO_AUTH_TOKEN` 未配置 → 暂不测试 Turso remote / local-replica / synced 模式;不影响当前本地功能测试 - 真实 Turso dev DB / token 未配置,remote、local-replica、synced、真实 5xx/限流/断网/同步失败作为后续云端验收 - `desktop:hot` 默认后端切换到 `turso-local-replica` 暂缓;当前本地开发默认已可用 `libsql-local` - OpenHub SQLite 会话库迁移(Phase 7)属于 OpenHub 独立评估,不绑定 Rust control-plane 切换 **SQLite 导出备份 / admin-only 环境:** ```text # 本地 libSQL 模式(无远程依赖,推荐开发测试) MNOTE_CONTROL_PLANE_BACKEND=libsql-local # Turso remote(需配置 dev DB/token 后启用) MNOTE_CONTROL_PLANE_BACKEND=turso-remote MNOTE_TURSO_DATABASE_URL=libsql://... MNOTE_TURSO_AUTH_TOKEN=... # Turso local replica(需配置 remote URL/token 后启用) MNOTE_CONTROL_PLANE_BACKEND=turso-local-replica MNOTE_TURSO_DATABASE_URL=libsql://... MNOTE_TURSO_AUTH_TOKEN=... MNOTE_TURSO_LOCAL_REPLICA_PATH=/mnt/Data1T/Mnote_data/control-plane/control-plane-replica.db ```