Cherry Studio Provider & Model 注册表系统:预设数据加载、规范化、种子写入与用户数据合并全解析
【免费下载链接】cherry-studioAI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs项目地址: https://gitcode.com/GitHub_Trending/ch/cherry-studio
导读
Cherry Studio 将数百家 AI 提供方(Provider)与数千个模型的定义沉淀为三份随包发布的 JSON 注册表数据(位于 packages/provider-registry/data),并在运行时将其与 SQLite 中的用户配置按"分层 Delta"契约合并,从而让上游目录更新能够零数据迁移地触达既有安装。本文基于 docs/references/provider-model/README.md 与其核心正文 docs/references/provider-model/provider-registry.md,结合 packages/provider-registry 与主进程数据服务源码,完整讲解:三类注册表 JSON 的职责划分、RegistryLoader的缓存与 O(1) 索引机制、模型 ID 规范化(normalizeModelId)规则、预设 Provider 的 insert-only 种子写入,以及模型/Provider 配置在"注册表 → 用户行"上的三层合并优先级。读完你既能理解 Cherry Studio 的 Provider/Model 数据架构,也能掌握"哪些字段该持久化、哪些字段该读时解析、何时需要数据回填"的扩展设计方法。
一、整体架构:三份 JSON + 主进程服务 + 两张数据库表
从源码结构看,整套 Provider/Model 体系由三层组成:
@cherrystudio/provider-registry (包) ├── data/ │ ├── models.json 预设模型(能力、定价、模态、推理支持……) │ ├── providers.json 预设提供方(端点、apiFeatures、元数据) │ └── provider-models.json 提供方专属的模型级覆盖(按提供方微调) ├── src/ │ ├── registry-loader.ts RegistryLoader:加载、校验、缓存、索引、空闲 TTL │ ├── registry-utils.ts 纯函数:lookupRegistryModel、buildPersistedEndpointConfigs │ ├── utils/normalize.ts normalizeModelId(聚合前缀、变体后缀……) │ └── schemas/ Zod 校验 Schema │ src/main/data/ ├── db/seeding/ │ └── seeders/ │ └── presetProviderSeeder.ts ISeeder:仅插入的 Provider 身份/认证脚手架 ├── services/ │ ├── ProviderRegistryService.ts 注册表查找与 Provider/Model 基线解析 │ ├── ModelService.ts Model CRUD 与用户 Delta 覆盖 │ └── ProviderService.ts Provider CRUD 与读取时 Provider 合并 └── api/handlers/ ├── models.ts Model CRUD、对账与注册表解析路由 └── providers.ts Provider CRUD 与预设投影路由三个 JSON 文件是目录事实的唯一来源(source of truth),架构文档刻意不重复目录条数,因为其体积随上游数据独立变化。加载时,三份文件分别经过ModelListSchema、ProviderListSchema、ProviderModelListSchema的 Zod 校验(packages/provider-registry/src/schemas);校验通过后才进入内存索引。
1.1 三类数据的职责划分
| 文件 | 内容 | 典型字段 |
|---|---|---|
models.json | 全局模型目录(跨 Provider 复用) | id、name、capabilities、contextWindow、maxOutputTokens、inputModalities/outputModalities、pricing、reasoning、parameterSupport、imageGeneration、ownedBy、openWeights |
providers.json | 预设提供方定义 | id、name、defaultChatEndpoint、endpointConfigs(baseUrl、adapterFamily、modelsApiUrls、reasoningFormat)、apiFeatures、authMethods、authOptional、modelListSource、serverTools、metadata.website、availableInEditions |
provider-models.json | 提供方对特定模型的覆盖 | providerId、modelId(引用models.json)、apiModelId(调用 API 时真实使用的 ID)、capabilities(add/remove/force)、limits、pricing、reasoningContracts、endpointTypes、inputModalities/outputModalities、disabled、replaceWith、name等独立字段 |
以 providers.json 中的真实条目为例,cherryin声明了四个端点(anthropic-messages、google-generate-content、openai-chat-completions、openai-responses),其中openai-chat-completions端点额外声明reasoningFormat: { type: "openai-chat" },并携带serverTools(web-search、url-context)与官网元数据。而provider-models.json中302ai对claude-opus-4-5等模型以apiModelId映射"带日期快照的真实调用 ID"(如claude-opus-4-1-20250805),同时覆盖其定价——这就是"目录规范 ID"与"厂商实际 ID"解耦的典型用法。
值得注意的边界:apiModelId的作用是保留提供方原始的 ID 格式(OpenRouter 的anthropic/claude-3-5-sonnet、Vertex AI 的global.anthropic.claude-3-5-sonnet-v1:0等);未设置时直接用modelId作为 API 调用 ID。若某模型在models.json中没有对应条目,provider-models.json的name、description、family、ownedBy、imageGeneration等字段可以让它"完全独立地"存活在覆盖文件里——解析器会用synthesizePresetFromOverride(ProviderRegistryService.ts)从覆盖合成一个预设,无需污染全局模型目录。
二、数据流:三条关键路径
2.1 启动时:预设 Provider 种子写入
DbService.onInit() → SeedRunner.runAll(seeders) → PresetProviderSeeder.run(db) → RegistryLoader.loadProviders() // 读取 providers.json → SELECT 已有 provider ID(user_provider) → 仅 INSERT 新增的 Provider 身份/认证行 → 绝不物化注册表拥有的连接配置SeedRunner会在providers.json版本变化时重跑该 Seeder,但 Seeder 始终是insert-only:已存在的 Provider 行被跳过。这一设计的正确性建立在"注册表拥有的连接配置从不写入行"之上——每次读取时都从当前注册表实时解析。行内只保存:身份(providerId、presetProviderId)、用户拥有的显示名(name)以及必要的认证壳(authConfig)。
从 presetProviderSeeder.ts 源码可见,认证壳仅对三个复用他人端点协议的厂商生成:vertexai生成{ type: 'iam-gcp', project: '', location: '' },azure-openai生成{ type: 'iam-azure', apiVersion: '' },aws-bedrock生成{ type: 'iam-aws', region: '' };其余返回null(按 v2 约定,这类厂商的 URL 路由完全由authType驱动,见ProviderSettings/utils/provider.ts的说明)。toDbRow中presetProviderId取p.presetProviderId ?? p.id,即"无显式分组归属时,预设就是自己"。
规范预设不可删除。多数预设满足providerId === presetProviderId;少数别名/分组预设(如zai→zhipu、minimax-global→minimax)通过注册表查找同样受到保护——isRegistryProvider只要发现providers.json中存在该 ID 即判定为预设行。而用户自建、从预设继承的 Provider 可以正常删除。
2.2 按需触发:模型创建
POST /models [{ providerId: 'openai', modelId: 'gpt-4o' }] → handler: 对每个条目调用 providerRegistryService.lookupModel(providerId, modelId) → RegistryLoader.findModel('gpt-4o') // O(1) 索引,规范化兜底 → RegistryLoader.findOverride('openai', 'gpt-4o') // O(1) 索引 → 从注册表数据解析端点 profile // 仅主进程,不持久化 → 返回 { presetModel, registryOverride, reasoningProfile } → handler: modelService.create(items) → mergePresetModel(preset, override, ...) → 将 DTO 显式字段与注册表基线比较 → 仅 INSERT 与基线不同的可空列到 user_model → list/get/mutation 响应 → 重建当前注册表基线 → 覆盖每个非空稀疏列lookupModel(ProviderRegistryService.ts)是"注册表 + 数据库感知"的单模型查找:先取 Provider 上下文(含presetProviderId与默认端点),再用loader.findOverride(presetProvider.id, modelId)找覆盖,loader.findModel(...)找预设;若命中覆盖但模型目录无条目,则走synthesizePresetFromOverride合成。该服务不拥有任何数据库表、不直接访问数据库,用户数据一律经ProviderService获取。
2.3 解析 SDK 模型列表
GET /providers/:providerId/models:resolve?ids=gpt-4o&ids=o3 → providerRegistryService.resolveModels(providerId, modelIds) → 对每个 modelId: → RegistryLoader.findModel(modelId) // O(1),规范化兜底 → RegistryLoader.findOverride(providerId, modelId) // O(1) → mergePresetModel(preset, override, ...) 或 createCustomModel(...) → 返回合并后的 Model[]resolveModels(ProviderRegistryService.ts)是"SDK 只提供 ID、其余全部来自注册表"的关键路径:SDK 数据不会覆盖精心维护的注册表数据。对注册表中未命中的 ID,createCustomModel生成最小自定义模型(能力为空数组、supportsStreaming: true、默认启用);注册表合并失败被视为致命错误,避免把不完整结果当成功同步持久化。解析时还会用deriveResolvedModelName生成可区分显示名——当多个 SKU 共享同一规范条目时(如MiniMax/MiniMax-M2.1与裸MiniMax-M2.1),规范化命中的条目会附加"命名空间前缀/日期快照/变体"等装饰后缀,防止界面混淆。
三、合并函数与优先级
3.1 三个函数、三种场景
| 函数 | 适用场景 | 合并层 |
|---|---|---|
mergePresetModel | 注册表查询、resolveModels | preset → override |
applyUserOverlay | 带显式用户 Delta 的 Model 读取 | 合并后的注册表基线 → user |
createCustomModel | 注册表无匹配 | 仅 modelId |
共享逻辑被抽取为applyPresetAndOverride(preset + override 合并,见 ProviderRegistryService.ts,处理能力、模态、限制、定价、参数支持的逐字段合并)与resolveReasoning(推理配置解析)。
3.2 合并优先级
非空稀疏列(用户 Delta) > provider-models.json > models.json 最高优先级 中间 最低对预设支撑的行(preset-backed row),每个可空的模型配置列都是独立的"所有权标记":null表示继承注册表,任何非空值都是用户 Delta。自定义行则存储完整配置。这一约定让目录变更无需数据迁移即可触达既有行——空字符串与空数组作为显式覆盖依旧有效。
applyUserOverlay(ModelService.ts)的实现非常直白:对name、description、capabilities、contextWindow、maxInputTokens/maxOutputTokens、pricing、parameterSupport、reasoning、supportsStreaming、endpointTypes、模态等字段逐一判断"!= null才覆盖",undefined/null即"未设置"。配合createModelsSqliteHandlers中"仅插入与基线不同的可空列",共同保证 user_model 里永远只存真正的用户偏差。
3.3 用户覆盖保护
当用户修改某个可被注册表增强的字段(例如name),该值直接存入对应可空列。读取时从当前注册表出发、逐个应用非空列;创建/PATCH 时把入参值与当前注册表基线比较——因此渲染层回显(renderer echo)不会冻结目录值;把值恢复回基线时该列被清空(重新变回null继承)。
3.4 未来新增字段的分支策略
- 注册表拥有(用户不可编辑):加入注册表 Schema、运行时
Model与mergePresetModel;不要新增user_model列或持久化 Delta。既有预设行在下一次读取时自动获得该字段,零 Schema 迁移、零数据回填。 - 用户可编辑的预设字段:为其增加一个可空 Delta 列,纳入 create/PATCH 覆盖映射与预设 Delta 字段集。Schema 迁移仅新增列,既有行无需回填——
null即继承当前注册表值。 - 自定义模型字段:自定义行拥有完整配置,新必需字段要么给运行时默认值、要么做自定义行回填;这是预设继承规则的有意例外。
- 一个列可同时被自定义行与预设行共享,对自定义行必填、对预设行保持
null(典型如capabilities、reasoning)。
四、RegistryLoader:缓存、索引与空闲 TTL
registry-loader.ts 是注册表 JSON 的读取与查询引擎,其生命周期契约:
- 懒加载:首次访问时才读盘(启动时不加载);
- 预计算索引:首次加载后一次性构建,换取 O(1) 查找;
- 空闲 TTL:默认 30 秒无访问即自动失效(
DEFAULT_IDLE_TTL_MS = 30_000); - 访问即触碰:每次
findModel/findOverride/loadModels都会重置计时器; - 服务级缓存:
ProviderRegistryService共享一个 loader;Provider Seeder 自建独立 loader。
RegistryLoader的构造函数接收三个文件路径(RegistryPaths)并允许自定义 TTL;touch()内部用setTimeout实现失效,invalidate()清空全部数据与索引,下次访问重新加载。
4.1 索引一览
| 索引 | 键 | 用途 |
|---|---|---|
modelById | model.id | 精确模型查找 |
modelByNormId | normalizeModelId(id) | 规范化兜底 |
modelBySizedNorm | 保留参数规模的规范化 ID | 解析带参数规模标签的变体(gpt-oss:20b→gpt-oss-20b) |
overrideByKey | providerId::modelId | 精确覆盖查找 |
overrideByNormKey | providerId::normalizeModelId(id) | 规范化兜底 |
overrideByApiKey | providerId::apiModelId | 按提供方侧调用 ID 精确查找 |
overrideByNormApiKey | providerId::normalizeModelId(apiModelId) | 规范化后的提供方 ID 兜底 |
overridesByProvider | providerId | 某提供方的全部覆盖 |
索引构建有几个值得注意的细节(均有源码注释佐证):modelBySizedNorm让gpt-oss-20b与gpt-oss-120b保持区分(它们在大小写无关键上会坍缩为同一个gpt-oss);overrideByKey采用"自变体优先"规则——同一提供方可能用多个apiModelId服务同一个规范模型(如 tokenhub 带日期的原厂直供变体共享deepseek-v4-flash),规范键必须解析到无日期的自变体(apiModelId === modelId),日期变体只能经apiModelId索引触达。
4.2 查询 API
loader.findModel(modelId) // O(1):精确 → 规范化兜底 loader.findOverride(providerId, modelId) // O(1):精确 → 规范化兜底 loader.getOverridesForProvider(providerId) // O(1):按提供方分组 loader.invalidate() // 释放全部数据,下次访问重载findModel对带冒号规模标签的 ID(gpt-oss:20b)会先用colonVariantTagToHyphen对齐为连字符拼写再走modelBySizedNorm,宁可返回null也不做"错误兄弟型号"的规模无关猜测。findOverride同样严格:精确的规范modelId与提供方apiModelId两种精确查找必须先于两种规范化兜底,否则google.gemma-3-27b-it这类 ID 会被同族的gemma-3-12b-it抢先占用规范化键。
另外 registry-loader.ts 还定义了REGISTRY_SCHEMA_VERSION = 2与REGISTRY_MIN_APP_VERSION = '2.0.13':远程注册表更新器按v{版本}/路径拉取数据,只有 Schema 结构变化才升版本;新模态/能力/effort 等枚举词条的增长不再升级(v2 客户端会丢弃不认识的字段),但旧运行时无法执行的语义值(新 adapter family、端点类型等)由REGISTRY_MIN_APP_VERSION兜底门控。
五、模型 ID 规范化(normalizeModelId)
用户侧看到的模型 ID 往往与注册表规范 ID 不同,规范化让两者对齐:
| 用户看到 | 注册表有 | 规范化动作 |
|---|---|---|
aihubmix-gpt-4o | gpt-4o | 剥掉聚合前缀 |
gpt-4o:free | gpt-4o | 剥掉变体后缀 |
claude-3.5-sonnet | claude-3-5-sonnet | 规范化版本分隔符 |
aihubmix-gpt-4o:free | gpt-4o | 组合处理 |
normalizeModelId()实现在 packages/provider-registry/src/utils/normalize.ts,执行顺序为:
1. 剥离 Provider 前缀("anthropic/claude-3" → "claude-3",取最后一个 "/" 段) 2. 小写化 3. 剥离聚合前缀(aihubmix-、zai-、siliconflow-、nvidia-、groq-……) 4. 展开已知缩写(mm- → minimax-) 5. 剥离变体后缀(:free、-thinking、(beta)……) 6. 剥离参数规模(-72b、-7b……) 7. 规范化版本分隔符(3.5 → 3-5、3p5 → 3-5) 8. 下划线折叠为连字符(HF 风格 bce-embedding-base_v1 → bce-embedding-base-v1)实现细节相当严谨:COMMON_AGGREGATOR_PREFIXES中刻意不放mm-(它是 MiniMax 缩写,交给PREFIX_EXPANSIONS展开,若先当聚合前缀剥掉会得到孤儿 IDm2-1);-medium同样被排除在变体后缀之外(它是真实型号层级mistral-medium);剥变体时还保护non/no/pre/anti/post等复合前缀开头的词(-no-think不被误剥)。变体 → 量化 → 日期快照的剥离被组织为stripVariantQuantDateSuffixes的不动点循环(一次遍历非幂等,因为尾部日期会屏蔽内层变体)。此外还有针对 Bedrock 跨厂商 ARN 的专门处理:us.anthropic.claude-sonnet-4-5-v1:0会被折叠为claude-sonnet-4-5(区域+厂商点号前缀、厂商连字符前缀、-v1:0修订全部剥离)。
查找策略:精确匹配优先、规范化兜底。这保证当gpt-4o与aihubmix-gpt-4o作为独立条目同时存在时,精确匹配胜出,规范化不会造成错误折叠。
六、关键数据库表
6.1 user_provider
表定义见 src/main/data/db/schemas/userProvider.ts。核心原则是"一个 Provider 实例 = 一个 apiHost(1:1),一个 apiHost 可挂多个 API Key(1:N)":
| 列 | 用途 |
|---|---|
providerId | 主键,用户自定义唯一 ID |
presetProviderId | 指向 providers.json 条目(null = 自定义 Provider)。双重职责:既是来源预设标识,也是侧边栏分组键——少数注册表行(zai→zhipu、minimax-global→minimax)指向不同预设以归入该分组 |
name | 用户拥有的显示名,首次种子写入时由预设初始化 |
endpointConfigs | JSON Delta:用户的baseUrl覆盖;自定义 Provider 还可存adapterFamily路由提示 |
defaultChatEndpoint | 可空用户覆盖;null 继承注册表默认 |
apiKeys | JSON 数组的 API Key 条目 |
apiFeatures | JSON Delta:仅存与注册表/应用默认不同的标志;null 继承全部默认 |
authConfig | 统一认证配置(如iam-gcp/iam-azure/iam-aws) |
6.2 user_model
| 列 | 用途 |
|---|---|
id | 确定性主键:providerId::modelId |
providerId+modelId | Provider 内的唯一模型身份 |
presetModelId | 指向 models.json 条目(null = 自定义模型) |
name/capabilities/supportsStreaming | 自定义行必填;预设行是可空 Delta |
inputModalities/outputModalities | 自定义行完整配置或可空 Delta |
contextWindow/maxOutputTokens | 自定义行完整配置或可空 Delta |
reasoning | 自定义模型的内在控制/令牌上限;预设行从注册表解析 |
pricing | 自定义行完整配置或可空 Delta |
parameters | 自定义行完整配置或可空 Delta |
orderKey | 提供方模型列表中的分数排序键 |
notes | 用户备注 |
七、Provider 配置合并:行是 Delta,不是快照
Provider 连接配置遵循与模型相同的分层、读时合并。user_provider行是Delta:只存用户显式设置的值;键缺失即"使用注册表值"。合并发生在rowToRuntimeProvider(ProviderService)经由ProviderRegistryService.mergeEndpointConfigs/getProviderDisplayMetadata完成:
user_provider (DB, delta) > providers.json (registry) > app 默认值| 字段 | 所有权 | 解析规则 |
|---|---|---|
endpointConfigs[ep].baseUrl | 用户 | 行 > 注册表 |
endpointConfigs[ep].adapterFamily | 注册表 | 注册表 > 行(自定义 Provider 提示)>inferAdapterFamily(ep) |
endpointConfigs[ep].modelsApiUrls | 注册表 | 仅注册表 |
| 端点类型键集合 | 注册表 ∪ 用户 | 注册表键与行键的并集 |
apiFeatures | 混合 | {...DEFAULT_API_FEATURES, ...registry, ...row} |
defaultChatEndpoint | 混合 | 行 > 注册表 |
mergeEndpointConfigs(ProviderRegistryService.ts)的具体实现印证了这一点:端点键取并集(新增端点无需迁移);adapterFamily优先注册表值、否则行值、最后inferAdapterFamily(ep)按端点协议推导(anthropic-messages→anthropic、openai-responses→openai、兜底openai-compatible,见 registry-utils.ts);dialect逐键浅合并,行只声明用户发现的偏差;输出按字段重建,历史遗留的仅注册表字段(如reasoningFormatType)绝不会跨入运行时状态。
为什么零数据迁移(对应 issue #17096):注册表拥有的事实从不冻结进行。端点类型新增、adapterFamily 变化、baseUrl/功能开关/默认端点调整,都会在 Delta 契约下自动到达既有行。写路径强制维持 Delta:EndpointConfigOverride是唯一可持久化的端点形状,PATCH 规范化会丢弃与注册表基线相等的值。
一个直观例子:未被用户触碰的预设baseUrl不在行中;若提供方在providers.json中修改该 URL,下一次读取就会返回新 URL。用户自定义的代理 URL 则留在行里持续生效,直到用户将其重置为当前注册表值。
name是有意例外:它是用户拥有的完整值(种子初始化),不是注册表 Delta;后续注册表改名不会覆盖它。若产品语义要改为"改名之前一直继承",name必须先转换成显式 Delta 表示。
7.1 何时需要回填
注册表内容更新在存储所有权契约不变时不需要回填:
- 仅注册表字段读时直接解析;
baseUrl、apiFeatures、defaultChatEndpoint等混合字段在行 Delta 缺失时继承;- 既有用户覆盖有意持续生效,不是陈旧数据;
- 新注册表拥有字段应加入读时投影,而非持久化。
Schema 迁移可能仍需为新用户可编辑字段新增存储,但预设行在"null/缺失=继承"语义下无需数据回填。只有两种情形必须回填:完整自定义行新增无运行时默认值的必需字段;或既有字段的所有权从完整快照改为 Delta 且需保留旧库数据。
新增注册表字段的分支:
- 注册表拥有:只加进读时合并输出;对端点配置字段不要加入
EndpointConfigOverride——Zod 会自动从写 DTO 剥离未知键,零迁移。 - 用户可编辑端点字段(混合所有权):加入
EndpointConfigOverrideSchema(其keyof集合即权威所有权声明),在合并中加一条row.x ?? registry.x规则,写路径可选丢弃等于基线的值,零迁移。 - 用户可编辑 Provider 字段:若属于既有 JSON Delta(如
apiFeatures),扩展对应 Schema 与合并规则即零迁移;否则需选择显式持久化位置。新增独立列属 Schema 变更,但可空预设 Delta 列仍无需值回填。 - 永远不要把注册表拥有值作为行快照持久化——这正是注册表更新变陈旧的确切成因。
八、推理(Reasoning)配置的双边界设计
推理配置被刻意拆成两个边界:
- 模型数据声明内在控制与令牌上限(
ReasoningSupportSchema的controls:effort离散档位 /budget数字预算 /toggle开关,见 packages/provider-registry/src/schemas/model.ts)。主进程注册表增强会将其投影为渲染层控件消费的运行时selectableEfforts(deriveSelectableEfforts按端点 wire profile 过滤掉无法表达的选择,如无off时剔除none); - Provider 注册表数据声明封闭的
reasoningFormatwire profile(reasoningContracts、endpointConfigs[*].reasoningFormat),仅在主进程解析与解释,从不复制进 SQLite、DataApi 或渲染层状态。
请求路径按"精确 provider-model → 端点覆盖/默认 → 穷尽格式默认"的顺序解析出一个 profile(resolveReasoningProfileFromRegistry:contract.wire ?? format.wire ?? selectFormatWire(formatDefault, wireDialect)),再与提交时的规范选择组合,最终发射为原生 AI SDK Provider 选项或通用兼容参数。wireDialect(effort/budget)解决同一协议两代参数形状互斥的问题(Gemini 3thinkingLevelvs 2.xthinkingBudget、Claude 4.6+adaptivevs ≤4.5enabled+budget_tokens),且该事实随模型而非端点。更完整的 Schema、优先级与 UI→请求数据流见 packages/provider-registry/docs/reasoning-control.md。
九、文件索引速查
| 内容 | 位置 |
|---|---|
| 注册表 JSON 数据 | packages/provider-registry/data |
| Zod Schema | packages/provider-registry/src/schemas |
| RegistryLoader(加载、索引、TTL) | packages/provider-registry/src/registry-loader.ts |
| 纯查找/转换函数 | packages/provider-registry/src/registry-utils.ts |
| 规范化工具 | packages/provider-registry/src/utils/normalize.ts |
| 种子运行器 | src/main/data/db/seeding/SeedRunner.ts |
| 预设 Provider 种子 | src/main/data/db/seeding/seeders/presetProviderSeeder.ts |
| 注册表服务(合并查询) | src/main/data/services/ProviderRegistryService.ts |
| 模型服务(用户 Delta 覆盖) | src/main/data/services/ModelService.ts |
| Provider 服务(读时合并) | src/main/data/services/ProviderService.ts |
| DB Schema | src/main/data/db/schemas/userModel.ts、userProvider.ts |
| 合并行为测试 | src/main/data/services/tests/modelMerger.test.ts |
结语
Cherry Studio 的 Provider/Model 注册表系统用"三份 JSON 做事实源、SQLite 行做 Delta、读时三层合并"的组合,同时满足了目录数据的频繁更新与用户自定义的持久化需求。理解这套设计的关键在于三句话:注册表拥有的值永不落库(读时解析,让上游更新零迁移生效);每个可空列是独立的所有权标记(null 继承、非空即用户覆盖);models.json→provider-models.json→ 用户 Delta 的优先级是全局唯一规则。无论是排查"为什么改了注册表 baseUrl 不生效"、为目录贡献新模型,还是设计自己的 Provider/Model 数据架构,本文所述的索引策略、规范化步骤与 Delta 契约都是可直接复用的范式。
【免费下载链接】cherry-studioAI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs项目地址: https://gitcode.com/GitHub_Trending/ch/cherry-studio
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考