Files
mnote/docs/operations/control-plane-turso.md
T

12 KiB
Raw Blame History

MNote control-plane Turso / libSQL 运行手册

状态:首轮 Rust control-plane cutover 手册。 范围:只覆盖 MNote Rust control-plane;本地 .md、附件、mindmap、OnlyOffice 文件仍是 local-first 文件真相,不迁入 Turso。

后端选择

# 默认回滚 / 离线模式
export MNOTE_CONTROL_PLANE_BACKEND=sqlite
export MNOTE_CONTROL_PLANE_DB_PATH=/mnt/Data1T/Mnote_data/control-plane/control-plane.db

# 无云本地 libSQL 验证
export MNOTE_CONTROL_PLANE_BACKEND=libsql-local
export MNOTE_TURSO_LOCAL_PATH=/mnt/Data1T/Mnote_data/control-plane/control-plane-libsql.db

# 真实 Turso remote
export MNOTE_CONTROL_PLANE_BACKEND=turso-remote
export MNOTE_TURSO_DATABASE_URL=libsql://...
export MNOTE_TURSO_AUTH_TOKEN=...

# Turso local replica
export MNOTE_CONTROL_PLANE_BACKEND=turso-local-replica
export MNOTE_TURSO_LOCAL_REPLICA_PATH=/mnt/Data1T/Mnote_data/control-plane/control-plane-replica.db
export MNOTE_TURSO_DATABASE_URL=libsql://...
export MNOTE_TURSO_AUTH_TOKEN=...

# libSQL synced database
export MNOTE_CONTROL_PLANE_BACKEND=turso-synced
export MNOTE_TURSO_SYNCED_PATH=/mnt/Data1T/Mnote_data/control-plane/control-plane-synced.db
export MNOTE_TURSO_DATABASE_URL=libsql://...
export MNOTE_TURSO_AUTH_TOKEN=...

Sync interval for turso-local-replica / turso-synced

export MNOTE_TURSO_SYNC_INTERVAL_MS=5000

当前决策边界

2026-07-03 已配置真实 Turso dev DB/token,但当前不把 Turso Cloud 作为 MNote 本地功能测试的前置条件,也不把它当成本地文件网盘。

  • 本地 .md、附件、mindmap、OnlyOffice 文件仍是 local-first 文件真相,不迁入 Turso。
  • Turso 只承接 Rust control-plane 元数据:auth、session、workspace、grant、AI policy/runtime、audit/outbox 等。
  • dev-hot 默认可继续使用 libsql-localdesktop-hot 仍保守保留 sqlite fallback 默认。
  • turso-remote / turso-local-replica / turso-synced 只作为 dev DB dry-run、迁移演练、回滚演练和后续受保护 smoke。
  • remote 成为长期候选默认前,必须先完成 Argon2id password hash migration、remote smoke、rollback export 和 token 轮换策略。

本机私有 Turso env

真实 URL/token 不写入仓库。当前本机私有配置文件:

/mnt/Data1T/Mnote_data/control-plane/turso.env

要求:

  • 文件权限必须是 0600
  • 只在本机 shell 中 source,不要复制到设计文档、日志、issue、截图或 commit。
  • repo 内不得出现 JWT 明文、MNOTE_TURSO_AUTH_TOKEN=<jwt> 或带 token 的 libSQL URL。
  • tmp/turso.md 仅作为本机临时凭据来源,已被 tmp/ ignore,不纳入版本管理。

加载方式:

set -a
source /mnt/Data1T/Mnote_data/control-plane/turso.env
set +a

确认不打印 token

printf 'backend=%s\n' "$MNOTE_CONTROL_PLANE_BACKEND"
printf 'url_host=%s\n' "$(printf '%s' "$MNOTE_TURSO_DATABASE_URL" | sed -E 's#^libsql://([^/]+).*#\1#')"
test -n "$MNOTE_TURSO_AUTH_TOKEN" && echo 'token_present=yes'

初始化

cargo run --manifest-path rust/Cargo.toml -p control-plane --bin control-plane-admin -- \
  init \
  --backend "$MNOTE_CONTROL_PLANE_BACKEND"

默认会创建测试账号:

  • 邮箱:mnote.e2e@example.com
  • 用户名:mnote-e2e
  • 默认 workspace、directory grant、AI policy、UI preference、audit/outbox 初始化记录

可先 dry-run

cargo run --manifest-path rust/Cargo.toml -p control-plane --bin control-plane-admin -- \
  init \
  --backend "$MNOTE_CONTROL_PLANE_BACKEND" \
  --dry-run

SQLite 到 libSQL / Turso 迁移

cargo run --manifest-path rust/Cargo.toml -p control-plane --bin control-plane-admin -- \
  migrate-sqlite-to-target \
  --source /mnt/Data1T/Mnote_data/control-plane/control-plane.db \
  --backend "$MNOTE_CONTROL_PLANE_BACKEND" \
  --dry-run

cargo run --manifest-path rust/Cargo.toml -p control-plane --bin control-plane-admin -- \
  migrate-sqlite-to-target \
  --source /mnt/Data1T/Mnote_data/control-plane/control-plane.db \
  --backend "$MNOTE_CONTROL_PLANE_BACKEND" \
  --reset-target

回滚到 SQLite

cargo run --manifest-path rust/Cargo.toml -p control-plane --bin control-plane-admin -- \
  export-target-to-sqlite \
  --backend "$MNOTE_CONTROL_PLANE_BACKEND" \
  --output /mnt/Data1T/Mnote_data/control-plane/control-plane.db \
  --backup-existing

export MNOTE_CONTROL_PLANE_BACKEND=sqlite
export MNOTE_CONTROL_PLANE_DB_PATH=/mnt/Data1T/Mnote_data/control-plane/control-plane.db
npm run desktop:hot

回滚前必须保留 Turso DB 至少 30 天,便于审计和补导出。

验证命令

cargo check --manifest-path rust/Cargo.toml -p mnote-web
cargo test --manifest-path rust/Cargo.toml -p control-plane
cargo test --manifest-path rust/Cargo.toml -p control-plane --features turso-unit-tests -- --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 mnote-web local_folder_watcher -- --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
codegraph sync .

脚本审计:

rg -n "sqlite3|better-sqlite3|node:sqlite|MNOTE_CONTROL_PLANE_DB_PATH|control-plane\\.(sqlite|db)" scripts rust/crates \
  -g '!target' -g '!node_modules' -g '!recycle'

允许命中范围:

  • control-plane-admin CLI
  • sqlite fallback 环境变量
  • scripts/lib/control-plane-test-env.js
  • desktop-hot / dev-hot / prod-build-start 的后端选择逻辑
  • legacy evidence.sqlite / local search 测试边界

不允许命中范围:

  • smoke 脚本直接调用 sqlite3 CLI 写 control-plane DB
  • Node 脚本用 SQLite 库写生产 control-plane DB

故障边界

  • 缺 URL/token:应返回 InvalidInput 或启动时报清晰错误,不允许 panic。
  • 本地 libSQL path 不可写:应返回 storage error,不允许 panic。
  • remote 网络不可达、5xx、限流、replica 同步失败:属于后续云端同步/备份链路验收,必须在真实 Turso dev DB 配好后补注入测试。
  • 真实 remote / replica / synced 未验证前,默认保留 sqlite fallback;本地功能测试优先使用 libsql-local

Turso dev DB 后续云端验证项

remote / local-replica / synced 更接近云端同步、备份和多设备准备层;它们不影响当前 local-first 本地功能测试。真实 Turso 验证需要:

export MNOTE_TURSO_DATABASE_URL=libsql://...
export MNOTE_TURSO_AUTH_TOKEN=...

然后执行:

set -a
source /mnt/Data1T/Mnote_data/control-plane/turso.env
set +a

MNOTE_CONTROL_PLANE_BACKEND=turso-remote \
cargo run --manifest-path rust/Cargo.toml -p control-plane --bin control-plane-admin -- init --backend turso-remote

MNOTE_CONTROL_PLANE_BACKEND=turso-remote \
cargo run --manifest-path rust/Cargo.toml -p control-plane --bin control-plane-admin -- \
  migrate-sqlite-to-target \
  --source /mnt/Data1T/Mnote_data/control-plane/control-plane.db \
  --backend turso-remote \
  --dry-run

当前已完成的非破坏性 remote dry-run

set -a
source /mnt/Data1T/Mnote_data/control-plane/turso.env
set +a

cargo run --manifest-path rust/Cargo.toml -q -p control-plane --bin control-plane-admin -- \
  init --backend turso-remote --dry-run

cargo run --manifest-path rust/Cargo.toml -q -p control-plane --bin control-plane-admin -- \
  migrate-sqlite-to-target \
  --source /mnt/Data1T/Mnote_data/control-plane/control-plane.db \
  --backend turso-remote \
  --dry-run

2026-07-03 结果:

  • init --backend turso-remote --dry-run 返回 ok=true
  • migrate-sqlite-to-target --backend turso-remote --dry-run 返回 ok=true,并成功读取本地 SQLite 源库表计数,targetBackend=TursoRemote
  • 这只证明 URL/token 和 admin CLI 配置可用,不代表 remote 已成为默认后端。

通过后再进入 remote / replica 默认切换;不要把 OpenHub SQLite 和 Rust control-plane 首轮本地 cutover 绑死。

后续优化 checklist

  • 增加 MNOTE_TURSO_SYNC_INTERVAL_MS,让 remote replica / synced 同步间隔可配置。
  • remote replica 启动后做 explicit initial sync,并验证 read_your_writes 行为。
  • 新增 control-plane health endpoint,仅暴露 backend/mode/schema/sync 状态,不暴露 token。
  • Argon2id password migration 完成前,不把 remote Turso 作为长期默认。
  • secret-gated turso-dev store contract smokeinit、migration、session、grant、audit/outbox、export rollback。
  • browser QA 覆盖 turso-remoteturso-local-replica,但失败不阻塞本地 libsql-local smoke。

P0 / P1 / P2 状态

当前已完成的是 local cutover 和安全基线,不是所有 Turso 优势都已经落地:

  • P0 已完成:唯一写入口、脚本直写清退、libsql-local store、migration、admin roundtrip、browser QA、local 多线程并发防损坏测试、remote dry-run 凭据验证、MNOTE_TURSO_SYNC_INTERVAL_MS
  • P0 未完成:explicit initial sync、control-plane health endpoint、Argon2id password migration。
  • P1 未完成:读写并发性能优化。当前 TursoControlPlaneStore 仍是单 Mutex<TursoConnection>,读写都串行;这是故意保守的安全基线,不是最终性能设计。
  • P2/P3 暂缓:CDC、Tantivy FTS、vector/hybrid search、Browser WASM/OPFS、OpenHub SQLite 迁移。

读写并发设计

当前实现优先保证一致性和可回滚:

  • 单进程内通过 Mutex<TursoConnection> 串行化 control-plane 操作。
  • 脚本和 smoke 不再直接写生产 control-plane DB,降低多进程直写损坏风险。
  • local 多线程测试已覆盖 session、sidebar shortcut、AI runtime event、audit/outbox 并发写。

后续性能优化需要单独实施:

  • 写入路径保持单写者或独占写锁;多表写入必须显式事务。
  • 读路径可以评估 read connection pool 或 RwLock,但必须先按 store 方法分类只读/写入,并跑压力测试。
  • remote replica 只适合本地读副本;写仍转发 remote primary,不把 embedded replica 当成高并发本地写主路径。
  • 在同步 trait 未改 async 前,不扩大 block_in_place 使用面。

CDC / Tantivy 评估

CDC 当前不进入默认路线:

  • Turso CDC 通过 PRAGMA capture_data_changes_conn(...) 把变更写入 CDC 表,按连接开启,尊重事务边界。
  • CDC 可以用于后续审计归档、外部消息桥接、实时分析或同步 spike。
  • MNote control-plane 当前已有 audit_logoutbox_events,这些记录业务语义,比通用表级 CDC 更适合当前主链。
  • CDC 可能捕获敏感字段,启用前必须定义脱敏、保留期、权限和导出边界。

Tantivy FTS 当前不进入 control-plane

  • Turso FTS 是 CREATE INDEX ... USING fts + fts_match / fts_score / fts_highlight,不是 SQLite FTS5 的无成本替换。
  • 当前 control-plane 只存轻量元数据,全文搜索收益有限。
  • 页面正文、知识库和 RAG 仍走 local-first 文件 / OpenHub / WeKnora,不迁入 Turso control-plane。
  • 后续如需搜索页面标题、路径、AI 会话标题或轻量摘要,可单独做 P2/P3 spike。

Browser WASM / OPFS 当前不进入主线:

  • 它适合纯浏览器 / PWA 离线 SQL 存储,但当前 MNote 主路径是本地文件夹 + Rust mnote-web + watcher。
  • 浏览器 OPFS 不能替代本地 .md、附件、mindmap、OnlyOffice 文件真相,否则会制造第三份正文真相。
  • 浏览器内数据库也不能作为 auth、grant、AI policy 的权限真源;这些仍必须由 Rust control-plane 校验。
  • 后续可作为 P3 spike:只存可重建 cache、草稿、最近访问索引或纯 Web 离线收集箱,不存不可恢复正文唯一副本和长期 secret。

密钥泄露检查

rg -n "eyJhbGciOi|MNOTE_TURSO_AUTH_TOKEN=.*ey|authToken=.*ey|mnote-liaibo\.aws-ap-northeast-1\.turso\.io" . \
  -g '!target/**' \
  -g '!node_modules/**' \
  -g '!recycle/**' \
  -g '!*.db' \
  -g '!*.sqlite' \
  -g '!*.sqlite3' \
  -g '!*.png' \
  -g '!*.jpg' \
  -g '!*.jpeg'

允许结果:无输出;或者只命中文档中不含 token 的 host 示例。任何 JWT 明文命中都必须立即移出仓库路径并轮换 token。