跳转到内容

SSOT 双写架构与崩溃恢复机制

SSOT 双写架构与崩溃恢复机制

学习目标

  • 理解 CC Switch 中 SQLite 到 Live 配置的双向同步流程
  • 掌握 Live 配置接管的四步流程与标志管理
  • 了解崩溃恢复的三层兜底策略
  • 识别双写不一致的风险与防护机制

前置知识

下文聚焦 CC Switch 的具体代码实现。


项目实践

数据库架构

CC Switch 使用 SQLite 作为唯一数据源(SSOT),数据库包含以下核心表:

用途
providers供应商配置(ID、名称、分类、settings JSON)
mcp_serversMCP 服务器配置
prompts提示词模板
skillsSkills 安装记录
live_backupsLive 配置备份(接管前存储原始内容)
proxy_config代理配置(地址、端口、接管标志)
failover_queue故障转移队列
stream_check_logs流式检测日志
settings通用键值存储

Schema 版本:当前为 10 版,每次修改表结构时递增,升级时自动执行增量迁移。

启动导入流程

CC Switch 启动时的配置导入逻辑在 src-tauri/src/lib.rssetup 回调中:

启动流程:
1. 初始化 SQLite 数据库(创建表 + Schema 迁移)
2. 如果数据库为空:
2.1 从 Live 配置导入默认供应商(import_default_config)
2.2 种子官方预设供应商(init_default_official_providers)
2.3 从 Live 导入 MCP 服务器(表空时触发)
2.4 从 Live 导入提示词(表空时触发)
2.5 初始化默认 Skills 仓库
3. OpenCode / OpenClaw: 每次启动都从 Live 同步(幂等)
4. 后台异步: Codex 历史迁移、健康状态同步

设计意图

  • 对于 Claude/Codex/Gemini 等”覆盖模式”应用,只在首次导入(数据库非空时跳过),避免覆盖用户后续在 CC Switch 中的修改
  • 对于 OpenCode/OpenClaw 等”追加模式”应用,每次启动都同步(幂等操作,按 ID 去重),因为这些应用的配置格式支持多供应商共存

双向同步:正向写入

当用户切换供应商时:

switch_provider(provider_id) {
if proxy_active:
hot_switch_provider() // 代理模式:热切换
else:
1. 更新数据库 current_provider
2. 从数据库读取供应商完整配置
3. 合并 common_config(共享配置片段)
4. 原子写入 Live 文件
}

Common Config 合并:CC Switch 支持”共享配置片段”——用户在供应商之间共享的数据(如插件配置)。写入 Live 时,自动将共享配置合并到供应商的专属配置中。

双向同步:反向回填

当活跃供应商的 Live 配置被外部修改时(如用户手动编辑了 settings.json):

read_live_provider_settings(app_type) {
1. 读取 Live 文件
2. 解析为结构化 JSON
3. 提取可同步的字段(API Key、Base URL、环境变量)
4. 跳过不可同步的字段(供应商 ID、名称、分类)
5. 更新数据库中当前供应商的配置
}

回填保护:回填时不覆盖数据库中的 idnamecategory 等关键字段,只同步配置值。

Live 接管四步流程

代理启动接管(start_with_takeover):

步骤 1: backup_live_configs()
→ 读取 Claude/Codex/Gemini 的 Live 文件
→ 序列化后存储到 live_backups 表
步骤 2: sync_live_to_providers()
→ 从 Live 文件提取 Token
→ 同步到数据库中当前供应商的 settings_config
步骤 3: set_live_takeover_active(true)
→ 设置接管标志(在写入接管配置之前!)
→ 即使后续步骤失败,下次启动也能检测到残留
步骤 4: takeover_live_configs()
→ 写入代理地址到各应用的 Live 文件
→ 写入 PROXY_MANAGED 占位符替换真实 Token
→ 启动 axum 代理服务器

标志先于数据写入是关键设计——即使接管写入中途崩溃,下次启动时 live_takeover_active = true 标志仍然存在,触发自动恢复。

崩溃恢复:三层兜底

应用在 lib.rs 的启动后台任务中检测并恢复:

Layer 1: 从 live_backups 表恢复
→ 查询该应用的备份
→ 解析原始配置
→ 原子写入 Live 文件
→ 成功则返回
Layer 2: 从 SSOT 重建
→ 读取数据库中当前供应商的配置
→ write_live_with_common_config() 写回 Live
→ 成功则返回
Layer 3: 清理占位符
→ 移除 Live 中的代理地址
→ 移除 PROXY_MANAGED Token
→ 记录日志,等待用户修复

退出清理

应用退出前(cleanup_before_exit):

1. 检测接管残留(备份存在 或 Live 含占位符)
2. 如有残留 → restore_live_configs() 恢复
3. 清除 live_takeover_active 标志
4. 删除所有 Live 备份
5. 保留 proxy_config.enabled 状态(下次启动自动恢复代理)

关键决策:使用 stop_with_restore_keep_state 而非 stop_with_restore——前者保留 proxy_config.enabled 状态,后者清除。这意味着如果用户上次使用了代理,正常关闭后下次打开应用时代理会自动恢复。


问题与规避

陷阱 1:数据库迁移失败导致应用无法启动

问题:Schema 升级过程中如果数据库损坏或版本不匹配,应用可能无法启动。

CC Switch 的解决方案

  • 迁移前自动创建预迁移备份(backup_database_file()
  • 迁移失败时弹出系统对话框,提供重试/退出选项
  • 对话框内容根据系统 locale 自动切换中英文
  • 数据库文件不会被自动删除,用户可以手动回退

陷阱 2:JSON 配置迁移到 SQLite 时解析失败

问题:旧版 CC Switch 使用 config.json 存储配置,迁移到 SQLite 时如果 JSON 格式错误,迁移会失败。

CC Switch 的解决方案

  • 在创建数据库之前先验证 JSON 是否可以加载
  • 加载失败时弹出对话框让用户选择重试/退出
  • 如果用户选择退出,数据库文件尚未创建,下次启动可以重试
  • 迁移成功后归档旧 JSON 文件(rename 为 config.json.migrated)而非删除

陷阱 3:回填时 Live 文件格式异常

问题:用户手动编辑 Live 文件可能导致格式错误(如非 JSON 对象根节点)。

CC Switch 的解决方案

  • read_claude_live() 等方法在读取后验证根节点类型
  • 如果根节点不是对象,返回明确错误信息(包含当前类型和文件路径)
  • 调用方捕获错误后跳过回填,不覆盖数据库中的有效配置

设计取舍

SQLite vs JSON 文件存储

维度SQLiteJSON 文件
查询能力复杂查询(按分类筛选、统计)需全量加载后内存过滤
并发安全Mutex 保护,线程安全需要文件锁
迁移能力版本化 Schema,增量升级手动迁移
备份SQL 导出 + 快照直接复制文件
可编辑性需要工具文本编辑器直接编辑

CC Switch 从 JSON 迁移到 SQLite 的原因:管理 50+ 供应商时,JSON 的查询和并发处理能力成为瓶颈,且无法支持用量统计、健康检测等需要聚合查询的功能。


参考来源

  • CC Switch 源码: src-tauri/src/lib.rs(启动流程)
  • CC Switch 源码: src-tauri/src/services/proxy.rs(接管/恢复逻辑)
  • CC Switch 源码: src-tauri/src/database/schema.rs(Schema 定义)