16 张表读一遍:所谓 SSOT 到底存了哪些东西

2026-08-10

CC Switch 的 README_ZH.md 在「核心设计模式」一节里,第一条就是 SSOT:所有数据存储在 ~/.cc-switch/cc-switch.dbREADME_ZH.md:424-429)。紧接着的第二条却是「双层存储」——SQLite 存可同步的数据,JSON 存设备级设置。两条并排放在一起,第一条的「所有」就已经不是字面意思了。

真正把这件事说清楚,得把建表代码读一遍。这篇就干这一件事:src-tauri/src/database/schema.rs 里的 16 张表,各自存什么、哪些是「事实源」、哪些只是运行期产生的日志与聚合数据。以下全部基于我们本地 clone 的 cc-switch 仓库快照 c39c903(提交日期 2026-08-10),仓库内版本号 3.19.2。我们只读源码文本,没有安装也没有运行过这个桌面应用

先把 16 张表摊开

建表函数是 create_tables_on_connschema.rs 全文 3239 行,建表部分集中在 schema.rs:25-330。源码里每张表上面都有一行带编号的注释,我们按那个编号原样列出:

源码编号表名这张表承载什么
1providers供应商条目本体,复合主键 (id, app_type)
2provider_endpoints供应商的自定义端点,外键挂在 providers 上
3mcp_serversMCP 服务器条目,带 6 个按应用的启用开关列
4prompts提示词条目
5skillsSkills 的统一结构记录(v3.10.0+)
6skill_reposSkills 的来源仓库
7settings纯 KV:key TEXT PRIMARY KEY, value TEXT
8proxy_config代理配置,每应用一行,app_type 为主键
9provider_health供应商健康状态
10proxy_request_logs代理请求明细日志,主键 request_id
11model_pricing模型定价
12stream_check_logs连通性检查记录
16proxy_live_backupLive 配置备份
17usage_daily_rollups用量日聚合
18session_log_sync会话日志同步进度
19profiles全应用共享的项目实体

编号从 12 直接跳到 16,中间 13、14、15 缺号,而实际建表数是 16 张——编号与张数对不上。旁边只有一句注释写「circuit_breaker_config 已合并到 proxy_config 表中」(schema.rs:246schema.rs:251schema.rs:263)。关于这几个编号与数据库版本递推,我们另有一篇专门讲,这里只说到差异为止。

想自己把这张表核一遍,不需要装应用,clone 下来跑一条命令就够:

grep -n "CREATE TABLE IF NOT EXISTS" src-tauri/src/database/schema.rs

注意这条命令数出来的行数会多于 16。要对准这 16 张,把范围限在 schema.rs:25-330 这段建表函数内。顺带可以数索引:schema.rs 中出现 10 处 CREATE INDEX IF NOT EXISTS,其中 5 个建在 proxy_request_logs 上(provider / created_at / model / session / status 五个维度),1 个建在 stream_check_logs,其余散在迁移路径里(schema.rs:213-259schema.rs:400schema.rs:674schema.rs:2958schema.rs:2987)。索引压在哪张表上,本身就说明了这个库预期哪张表会变大。

反直觉的那一处:SSOT 是按域切的,不是全局的

读完 16 张表再回头看那句「所有数据存储在 ~/.cc-switch/cc-switch.db」,会发现代码注释给出的口径要细得多——同一个仓库里,不同的数据各有各的事实源,方向甚至相反。

方向一,事实源在库里,文件是投影。 MCP 就是这样:codex_config.rs:2068 的注释写「MCP 服务器的 SSOT 是 DB 的 mcp_servers 表」,而各工具真实读取的 live config.toml 里那段配置是同步出去的结果;services/provider/live.rs:2557 又从另一侧确认了同一件事——「Live 里的 [mcp_servers] 是 MCP 同步的投影(SSOT 在 DB 表)」。modelCatalog 也是同一模式,它是 cc-switch 的私有字段,SSOT 在 DB,live 文件只是投影(services/provider/live.rs:889services/provider/mod.rs:3712)。供应商本体则在 provider.rs:7 留了一行注释:「SSOT 模式:不再写供应商副本文件」。

方向二,事实源在文件系统,库里那张表不是唯一依据。 database/migration.rs:195 的注释写得很直接:「v3.10.0+:Skills 的 SSOT 已迁移到文件系统(~/.cc-switch/skills/)+ 数据库统一结构」。也就是说,第 5 号 skills 表在库里、但 Skills 内容的事实源在磁盘目录上。

所以「以谁为准」这个问题,在这个项目里没有统一答案,得看是哪一域的数据。这一点在排查「我改了配置怎么没生效」时是关键:按代码注释里的 SSOT 口径,live 文件是同步出去的结果,因此手改 live 文件未必是改在事实源上;反过来,事实源在文件系统的那一域,只改库里的记录同样不是改在事实源上。具体的覆盖时机我们没有运行验证。判断方法就是去对应模块 grep 一次 SSOT 这个词,代码注释里标得比 README 细。

16 张表里,有一批根本不算「你的配置」

第二个容易看走眼的地方是:这 16 张表不是同一类东西。有几张存的是运行期产生的日志与聚合数据,项目自己在代码里把它们从同步范围里单独圈了出来。

同步导出时跳过的表(SYNC_SKIP_TABLES)有 5 张:proxy_request_logsstream_check_logsprovider_healthproxy_live_backupusage_daily_rollupsdatabase/backup.rs:74-80)。同步导入时保留本地、不被覆盖的表(SYNC_PRESERVE_TABLES)有 4 张:proxy_request_logsstream_check_logsproxy_live_backupusage_daily_rollupsdatabase/backup.rs:84-89)。

两张清单差一个:provider_health 在 SKIP 里但不在 PRESERVE 里。这个差异我们只陈述位置,不推断原因。

这批表还有保留期。启动时与周期维护里跑的是同一套清理动作:cleanup_old_stream_check_logs(7)rollup_and_prune(30),有行被回收时再执行一次 PRAGMA incremental_vacuum;database/mod.rs:151-164)。所以「库里存了哪些东西」这个问题,对日志类表的准确答案是「最近一段时间的」,不是全部历史。

几个值得单独看一眼的表结构细节

providers 是复合主键 (id, app_type) 同一个 id 在不同应用下是两行独立记录,列里含 settings_configmeta(默认 '{}')、is_currentin_failover_queueschema.rs:27-42)。provider_endpointsprovider_health 都带 FOREIGN KEY (provider_id, app_type) REFERENCES providers(id, app_type) ON DELETE CASCADEschema.rs:56schema.rs:191),而初始化时确实开了 PRAGMA foreign_keys = ONdatabase/mod.rs:112-119)——外键约束不是写着好看的,删一条供应商会连带清掉挂在它下面的端点与健康记录。

settings 表是整个库的兜底。 表结构只有一行:CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT)schema.rs:120)。但 dao/settings.rs 里挂在这个 KV 上的配置面很宽:按应用的配置片段、全局出站代理地址、按应用的代理接管开关,以及 rectifier / optimizer / copilot-optimizer / log 几套配置。更容易踩空的是 profiles:项目实体本身在第 19 号表里、payload 是原始 JSON 文本按 app 分槽,而「当前选中哪个 profile」这个标记不在 profiles 表里,它在 settings 表,key 形如 current_profile_id_<scope>schema.rs:311-322dao/profiles.rs:5-6)。要查当前状态却盯着 profiles 表看,是会白看的。

proxy_config 的应用枚举比 AppType 窄。 这张表 app_type 为主键,带 CHECK (app_type IN ('claude','codex','gemini','grokbuild'))schema.rs:126-127),默认监听 127.0.0.1:15721schema.rs:128-129)。而 AppType 枚举共 8 个成员:Claude、ClaudeDesktop、Codex、Gemini、GrokBuild、OpenCode、OpenClaw、Hermes(app_config.rs:370-398)。8 与 4 是两处不同的口径,引用时别混着用。

金额列全是 TEXT proxy_request_logs 里的 input_cost_usdoutput_cost_usdcache_read_cost_usdcache_creation_cost_usdtotal_cost_usdcost_multiplier 六列都是 TEXT 而不是浮点(schema.rs:198-206)。这张表同时保存 modelrequest_modelpricing_model 三个模型维度,注释还标了两种空值语义:pricing_model 为 NULL 表示 v11 之前的历史行,'' 表示未计价的错误行(schema.rs:194-208)。你要自己写 SQL 拉这张表做统计,这两条得先看清楚,否则 NULL 与空串会被当成同一类。

聚合表的主键是六元组。 usage_daily_rollups 主键为 (date, app_type, provider_id, model, request_model, pricing_model),注释解释保留 request_model / pricing_model 是为了「明细被 prune 后接管计费仍可审计」(schema.rs:272-296)。配合上一节说的 30 天剪枝看,这个设计意图就清楚了:明细会被删,聚合行要能自己站得住。

session_log_sync 存的是进度而不是内容。 它以 file_path 为主键,记 last_modifiedlast_line_offsetlast_synced_at 三个字段(schema.rs:301-308)——库里存的是「读到哪儿了」,日志正文仍在各 CLI 自己的目录里。

库外面还有什么,以及一句必要的提醒

回到开头那句「双层存储」。落在这 16 张表之外的,至少有这几处:设备级设置走 JSON 而不进库;app_config_dir 的覆盖值存在 Tauri Store 的 app_paths.json 文件里,key 是 app_config_dir_overrideapp_store.rs:10config.rs:203-206);Skills 的内容在 ~/.cc-switch/skills/;数据库备份文件在库文件同级的 backups/ 子目录。

库本身的位置是 get_app_config_dir().join("cc-switch.db"),默认目录 ~/.cc-switchdatabase/mod.rs:101config.rs:207)。Database 结构体只有一个字段 conn: Mutex<Connection>,注释说明是因为 rusqlite::Connection 本身不是 Sync 的(database/mod.rs:80-82)。另外这个库注册了 SQLite update_hook,INSERT/UPDATE/DELETE 都会通知 webdav_auto_sync::notify_db_changed(table)s3_auto_sync::notify_db_changed(table)database/mod.rs:84-94)——表一变动,自动同步 worker 就知道了。

最后一句提醒是必须写的:这个应用会读写 ~/.claude~/.codex 等真实 CLI 配置文件,并在本机保存 API Key,providers 表的 settings_config 列承载的就是供应商配置正文。因此 cc-switch.dbbackups/ 目录属于本机敏感数据。往任何共享位置放这个库文件或它的 SQL 导出之前,先想清楚里面有什么。上面提到的同步跳过清单只说明哪些表不参与同步,不代表其余表里没有敏感内容。


本文依据 CC Switch 官方仓库(github.com/farion1231/cc-switch)的 README、docs/ 下的用户手册与发布说明、 src/config/ 的预设定义与 src-tauri/src/ 的后端源码整理,核对日 2026-08-10,对应仓库快照 c39c903。 本文内容为仓库源码与文档口径,我们没有安装或运行过这个桌面应用, 因此不涉及界面外观、操作手感与切换速度的任何描述。 文中出现的阈值与默认值均为源码中的默认配置,不构成对实际运行结果的保证。 该项目仍在快速迭代,版本与默认值随时可能变动,请以仓库最新内容为准。 安全相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。