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

294 lines
12 KiB
Markdown
Raw 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.
# MNote control-plane Turso / libSQL 运行手册
> 状态:首轮 Rust control-plane cutover 手册。
> 范围:只覆盖 MNote Rust control-plane;本地 `.md`、附件、mindmap、OnlyOffice 文件仍是 local-first 文件真相,不迁入 Turso。
## 后端选择
```bash
# 默认本地 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``mnote-web-hot`、prod runtime 默认使用 `libsql-local``sqlite` 不再是 mnote-web 运行时后端。
- `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 不写入仓库。当前本机私有配置文件:
```bash
/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,不纳入版本管理。
加载方式:
```bash
set -a
source /mnt/Data1T/Mnote_data/control-plane/turso.env
set +a
```
确认不打印 token
```bash
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'
```
## 初始化
```bash
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
```bash
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 迁移
```bash
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 备份
SQLite 仅作为 control-plane-admin 迁移 / 导出格式和测试隔离实现,不再作为 mnote-web runtime fallback。需要离线备份时执行:
```bash
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.backup.sqlite \
--backup-existing
```
备份文件仅用于审计、对照和后续手工恢复,不作为 `npm run dev:hot` / `npm run dev:hot` 启动后端。
## 验证命令
```bash
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_control_plane_store_for_tests
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/mnote-web-hot.test.js
node scripts/task-dev-hot-plan-test.js
node scripts/task-control-plane-admin-libsql-roundtrip-smoke.js
codegraph sync .
```
脚本审计:
```bash
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
- `control-plane-admin` 的 SQLite 迁移 / 导出参数
- `scripts/lib/control-plane-test-env.js` 的显式测试后端参数
- `mnote-web-hot` / `dev-hot` / `prod-build-start``sqlite` runtime 的拒绝逻辑
- 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 未验证前,本地功能测试优先使用 `libsql-local`;不再回落到 SQLite runtime。
## Turso dev DB 后续云端验证项
remote / local-replica / synced 更接近云端同步、备份和多设备准备层;它们不影响当前 local-first 本地功能测试。真实 Turso 验证需要:
```bash
export MNOTE_TURSO_DATABASE_URL=libsql://...
export MNOTE_TURSO_AUTH_TOKEN=...
```
然后执行:
```bash
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
```bash
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
- [x] 增加 `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-remote``turso-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_log``outbox_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 / LightRAG,不迁入 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。
## 密钥泄露检查
```bash
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。