news 2026/9/12 8:16:19

Cherry Studio 数据迁移实战:ProviderModelMigrator 如何把 Redux 模型状态搬进 SQLite v2

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Cherry Studio 数据迁移实战:ProviderModelMigrator 如何把 Redux 模型状态搬进 SQLite v2

Cherry Studio 数据迁移实战:ProviderModelMigrator 如何把 Redux 模型状态搬进 SQLite v2

【免费下载链接】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 的 v2 架构将原来存放在 Redux 中的 Provider/Model 状态迁入 SQLite 的user_provideruser_model表,ProviderModelMigrator正是完成这一动作的专职 migrator。本文围绕该 migrator 的职责、投影(projection)算法、字段映射、数据质量处理与三阶段执行流程展开,结合仓库源码与测试用例,帮助你理解"增量(delta)优先"的迁移设计如何在不确定的旧数据与不断演进的 registry 之间安全地搬运用户配置,并掌握其可复用的实现模式。

一、定位:v2 迁移管线中的第 6 号 migrator

ProviderModelMigrator是 Cherry Studio v2 数据迁移管线中负责"Provider/Model 域"的专用 migrator。在 migratorRegistry.ts 中,它排在BootConfigMigratorPreferencesMigratorNoteMigratorMiniAppMigratorMcpServerMigrator之后、AssistantMigrator之前执行,元数据如下(见 ProviderModelMigrator.ts):

  • idprovider_model
  • nameProvider Model
  • order1.75(数值越小越先执行)

它继承自 BaseMigrator.ts,遵循统一的prepare → execute → validate三阶段生命周期:

阶段职责失败返回
prepare读取 Redux/Dexie 源数据,过滤脏数据、去重、统计条目provider_model_prepare_failed
execute在一个withWriteTx同步事务内插入 provider/model/pin 行provider_model_execute_failed
validate核对源/目标行数、抽查 API Key 迁移完整性provider_model_validate_failed

三个阶段的错误 ID 常量定义在 ProviderModelMigrator.ts,任何一阶段失败都会在返回的error字段中以errorId: message的格式暴露给迁移引擎。

1.1 四类数据源

迁移的数据来源横跨 Redux 与 Dexie 两个旧存储,官方文档给出的来源如下:

数据来源
Providers 与 ModelsReduxstate.llm.providers[]
Provider/Model 设置Reduxstate.llm.settings
置顶模型(Pinned models)Dexiepinned:models
Provider LogoDexieimage://provider-<providerId>

在源码中,prepare()通过ctx.sources.reduxState.getCategory<LlmState>('llm')读取providerssettings,通过ctx.sources.dexieSettings.get('pinned:models')读取置顶模型、通过ctx.sources.dexieSettings.get(\image://provider-${provider.id}`)` 读取 Logo(ProviderModelMigrator.ts)。

一个需要特别注意的设计:受管理的 CherryAI 行不会被从 Redux 复制prepare()会跳过isManagedCherryProviderId(provider.id)命中的行(即cherryai/cherry-cloud),因为 v2 seeder(ensureCherryAiDefaultProviderAndModelTx)已经负责写入规范化的 CherryAI provider 与默认模型,旧有的 CherryAI 置顶模型会被改写为指向该 seeded 模型(CHERRYAI_DEFAULT_UNIQUE_MODEL_ID)。测试 ProviderModelMigrator.test.ts 验证了"旧 CherryAI qwen 置顶 → 重写到 seeded 默认模型"的行为。

附带约束:由于本 migrator 独家负责pinned:models的一次性迁移(写入pin表、entityType='model'),该键不能再被 classification.json 标记为普通偏好项,否则同一份数据会被写入两次。这个约定写在 ProviderModelMigrator.ts 的文件头注释里。

二、核心难点:Preset 归属投影(Ownership Projection)

v2 的设计中,preset 支撑的行是增量(delta),不是快照(snapshot)。也就是说:一行user_provider/user_model只存储用户相对出厂默认值的差异,其余字段由运行时从当前 registry 解析。这意味着迁移器必须回答一个棘手问题:

如何区分"v1 出厂默认值"与"用户手动改过的值"?

答案是不能查当前 v2 registry——因为 registry 本身可能已经变了。文档明确给出了方案:

mappings/v1-provider-model-baseline.json钉死的 final-v1 基线,提取自 v1 修订版d316ec5345680f1de511fd6df3a7fbdb3edad151(v1.9.12),只包含 provider/model 投影所需的字段。sourceRevision字段记录了该出处。其中的 provider API 标志表示的是post-migration-127/129 的 Redux 形态:三个兼容性标志在apiOptions被清空后仍保留在 system-provider 顶层行上。不要用当前的providers.jsonmodels.jsonprovider-models.json替代此基线做比较。

基线文件 v1-provider-model-baseline.json 以sourceRevision+providers[providerId]组织,每个 provider 包含typeapiHostanthropicApiHostisNotSupportDeveloperRoleisNotSupportStreamOptions以及按 model id 索引的models快照。源码中对应的 TypeScript 接口V1ProviderModelBaseline/V1ProviderBaseline/V1ModelBaseline定义在 ProviderModelMigrator.ts。

2.1 投影规则详解

文档归纳了 6 条投影规则,逐条拆解如下(对照 ProviderModelMigrator.ts 的projectProviderDeltaRow/projectModelDeltaRow):

  1. 自定义 provider 整体保留presetProviderId = null的 provider 是用户自定义的,其映射出的 provider 配置全部保留为行自有值。其模型仍可能独立命中全局models.json元数据,但由于没有有效 preset provider,provider-model override 不可用。
  2. preset provider 解析顺序:先按providerId解析,再按presetProviderId解析。第二个查找是必须的——像 Azure OpenAI 这类自定义 ID 但挂靠已知 provider 类型的实例(例如 UUID 形式的0196f996-34fc-7e3f-96d0-10b7f55fd6c8type='azure-openai'),需要回退到 preset id 才能找到 registry 中的预设。对应resolveEffectivePresetProvider()(ProviderModelMigrator.ts)与映射层resolvePresetProviderId()(ProviderModelMappings.ts)。
  3. 逐字段比较、逐键比较 API 特性:provider 字段与 final-v1 基线相等则置 null/缺席;API 特性(如streamOptionsdeveloperRole按 key 逐个比较,一个特性被改动不会冻结其余与基线相等的兄弟标志;不同的值才作为行自有 delta 保留。
  4. preset 关联的自定义 provider ID 使用 final-v1 自定义 provider API 特性基线:非系统 provider 由 v1 迁移 127/129/132 物化出默认方言{ streamOptions: true, developerRole: false }(源码常量V1_CUSTOM_PROVIDER_DIALECT_BASELINE,ProviderModelMigrator.ts)。这样既保留了 v1 迁移后显式的特性选择,又丢弃了未动过的自定义默认值。
  5. 模型元数据匹配顺序:模型先使用有效 provider 的 provider-model override,再匹配全局models.json元数据;全局匹配同样适用于完全自定义的 provider。若有效 provider 的 override 在全局元数据中无对应项,则 migrator合成一个与运行时一致的 provider-exclusive presetsynthesizePresetFromOverride)。
  6. preset 模型字段的稀疏规则:与 final-v1 模型相等的字段置 null;每个非 null 稀疏列在读取时都是权威的,不存在单独的 ownership 标记。

此外还有两个边界情形:

  • 若当前存在 preset,但模型不在 final-v1 基线中,则普通遗留字段没有可证明的用户出处,保持 null;但显式的capabilities[].isUserSelected选择会保留。endpointTypes在 registry 无法重新推导旧路由元数据时也会保留——包括内置 NewAPI provider 的动态模型、以及type='new-api'的自定义 provider;CherryIN 模型在没有旧端点元数据时,会显式恢复其前缀路由(anthropic/→ Anthropic Messages、google/→ Google Generate Content、否则 OpenAI-compatible 回退),对应inferCherryInEndpointTypes()(ProviderModelMigrator.ts)。
  • v1 编辑器合成的0/0定价值等价于"缺席 final-v1 价格",会在定价归一化时丢弃(matchesModelPricingBaseline只比较真实的非 0/0 价格)。

2.2 为什么必须钉死基线而不是用当前 registry

测试 ProviderModelMigrator.test.ts 给出了一个极佳的例子:当前 registry 中 OpenAI 的 chat 端点恰好也使用/v1后缀,但归属判断必须对照钉死的 final-v1 快照(其记录为https://api.openai.com)。因此遗留代理地址https://my-proxy.com/v1被判定为用户所有并写入行;而defaultChatEndpoint与基线相等则置 null,由运行时读取补齐目录事实。这验证了文档的警告:拿今天的 registry 与历史数据比较,无法区分"历史默认值"和"用户编辑"。

文档还给出了更新基线的操作规范:使用已发布的 final-v1 源修订版而非 v2 registry 快照;只保留V1ModelBaseline/V1ProviderBaseline声明的遗留字段;更新sourceRevision;并运行覆盖"基线相等值、真实 delta、自定义 preset ID、provider-exclusive 模型"的 migrator 测试。

三、目标表与字段映射

3.1user_provider表映射

文档给出的映射表如下,标注了每列的转换规则:

v1 源v2 目标转换
idproviderId直接映射;先过滤非法与重复 ID
id/typepresetProviderId已知系统 ID 使用目录 ID;受支持的自定义 provider 类型链接到其 preset
namename直接保留用户自有值
apiHostanthropicApiHostendpointConfigs转换为按端点键的 baseURL,然后对 preset 行投影 final-v1 默认值
typedefaultChatEndpoint通过遗留端点映射表转换,然后只存储 final-v1 delta
apiKey、认证设置apiKeysauthConfig归一化密钥与 provider 特定凭据
遗留 API option 标志apiFeatures转换受支持的标志,然后只存储 final-v1 delta
provider 设置providerSettings归一化 provider 特定用户设置
enabledisEnabled默认 true

在映射层 ProviderModelMappings.ts 中有若干值得展开的实现细节:

  • 端点映射表ENDPOINT_MAP(L62-L74)把遗留字符串端点/类型键映射为EndpointType枚举,例如openaiOPENAI_CHAT_COMPLETIONSopenai-responseOPENAI_RESPONSESanthropicANTHROPIC_MESSAGESgemini/vertexaiGOOGLE_GENERATE_CONTENTollamaOLLAMA_CHAT等;无法识别且非aws-bedrock的类型会记警告后丢弃。
  • API Key 归一化buildApiKeys会把逗号分隔的 v1 密钥串拆成多个{ id: uuidv4(), key, isEnabled: true }条目;AWS Bedrock 在authType === 'apiKey'时从settings.awsBedrock.apiKey取密钥。
  • 认证配置buildAuthConfig:Vertex(iam-gcp,含 project/location/serviceAccount)、AWS Bedrock(api-key-aws/iam-aws)、Azure OpenAI(iam-azure+ apiVersion)、CherryIN OAuth、以及普通api-key遗留 Anthropic Web OAuth 被端到端移除——token 原本存放在独立的凭据文件中、已不再读取,因此此类 provider 会被重置回api-key认证路径,避免 v2 行落入不可恢复状态。
  • provider 设置keepAliveTime(ollama/lmstudio/gpustack)、rateLimitextraHeaders(来自extra_headers)、notescacheControl(来自anthropicCacheControl)会被归一化进providerSettings

3.2user_model表映射

v1 源v2 目标转换
provider ID + modelididproviderIdmodelId构建确定性providerId::modelId身份
有效 registry 匹配presetModelId全局 preset(与 provider 出处无关)或合成的 provider-exclusive preset;未匹配的自定义模型为 null
namedescriptiongroup同名可空列自定义行完整值;preset 行为 final-v1 delta
capabilitiescapabilities归一化能力名;保留显式isUserSelected选择
endpoint_typesupported_endpoint_typesendpointTypes归一化遗留端点别名
supported_text_deltasupportsStreaming自定义行默认 true;preset 行为 final-v1 delta
pricingpricing归一化为运行时定价;丢弃合成的空0/0回显
源顺序orderKey在每个 provider 内分配分数键

实现要点:

  • 模型唯一 ID 通过createUniqueModelId(providerId, modelId)生成,非法 ID 返回 null 并在 prepare 阶段被剔除(ProviderModelMigrator.ts)。
  • capability 映射CAPABILITY_MAP把 v1 类型映射到 v2 能力(visionIMAGE_RECOGNITIONreasoningREASONINGfunction_callingFUNCTION_CALLembeddingEMBEDDINGrerankRERANK),text/web_search除外;用户显式关闭的能力(isUserSelected === false)优先于重复的启用条目,且不会被端点隐含能力重新加回(ProviderModelMappings.ts)。
  • pricing 映射:只迁移 v2 定价契约能无损表达的货币——$→ USD、¥/→ CNY,其余货币记警告后丢弃;输出{ input: { perMillionTokens, currency }, output: { perMillionTokens, currency } }(ProviderModelMappings.ts)。
  • orderKey:provider 顺序键在 seeded CherryAI 行之后生成(generateOrderKeySequenceBetween),模型顺序键按 provider 作用域分配(assignOrderKeysByScope),保证 UI 排序稳定。

四、有意丢弃或重新推导的数据

文档列出了一批"不落库"的数据,这背后是"读时解析(read-time resolution)"的设计哲学:

  • preset 模型无 delta 的字段不存:由当前 registry 在读取时解析;不存储userOverrides所有权数组。
  • registry 独有的 provider 端点字段不复制进 preset 行:如modelsApiUrlsadapterFamily。但迁移后的自定义 relay 可能保留 main-only 的adapterFamily提示——因为没有目录能重新推导它。测试 ProviderModelMigrator.test.ts 覆盖了小米 MIMO 这种自定义 relay 的回填场景:无目录匹配时运行时推断 anthropic 协议族,避免 resolver 回退到 openai-compatible 导致 404。
  • preset 的inputModalitiesoutputModalities、token 限制、reasoning、参数支持不在迁移期从当前 registry 推断;null 委托给读时解析。
  • 遗留isNotSupportEnableThinking没有 v2ApiFeatures目标,直接丢弃
  • 遗留 Anthropic Web OAuth token 在源外,不可恢复,对应 provider 回到 API-key 认证路径。
  • 损坏标识符、重复行(保留首次出现)、retired providers、无效 pin 引用、缺失的可选 Logo 均按下一节规则处理。

五、端点路由边界(Endpoint Routing Boundary)

文档用一小节明确了路由数据的职责划分:

Renderer/API 端点写入只包含用户可编辑的baseUrl值。遗留adapterFamily路由出处可能存在于主进程存储形态的自定义 provider 中,但它不是共享写入 DTO 的一部分。Preset provider 的路由族一律取自当前 registry。

也就是说:v2 的端点配置契约刻意把"用户能改的"(baseUrl)与"系统推导的"(adapterFamily 等路由元数据)分离。迁移时对 catalog 匹配的系统 provider,projectProviderDeltaRow会把 endpointConfigs 削减到只剩 baseUrl 级别的 delta;而对无目录匹配的自定义 provider,才保留LEGACY_TYPE_TO_ADAPTER_FAMILY推导出的路由提示(openai-compatible/openai/anthropic/google/newapi/gateway/ollama,见 ProviderModelMappings.ts),并且 Anthropic Messages 端点刻意跳过该提示——v1 自定义 anthropic relay 即使端点说 anthropic 协议、type也常为openai,端点协议必须优先。

六、数据质量处理清单

prepare()阶段集中完成脏数据过滤,文档给出如下处理矩阵:

问题处理
provider ID 缺失/为空跳过并警告
provider ID 重复保留第一个并警告
model ID 缺失、为空或不安全(route-unsafe)跳过并警告
候选 model ID 全部非法保留 provider、剔除非法模型、汇总警告
provider 内 model ID 重复保留第一个并警告
Retired provider跳过并警告
缺失 final-v1 preset 基线保守保留映射后的 provider 值并警告
无效的置顶模型引用丢弃
缺失可选 Logo保留 provider 但不带 Logo

源码中的实现细节值得注意:

  • 提前过滤的意义:缺失/空providerId若不过滤,会以空字符串主键落进user_provider(SQLite 文本主键允许''),从而遮蔽整个 v2 数据层的查找(ProviderModelMigrator.ts 的注释)。
  • route-unsafe model idcreateUniqueModelId会拒绝含@#等路由不安全字符的 ID(测试用例jackrong-qwopus3.5-27b-v3@?legacy-model#fragment均被剔除,且不影响同一 provider 的其余模型迁移,见 ProviderModelMigrator.test.ts)。
  • 置顶模型归一化normalizePinnedModelId支持对象形态{id, provider}、JSON 字符串、provider/modelprovider::model、以及裸provider::model唯一 ID 五种格式,并会与迁移后的有效模型集合做交叉校验去重(ProviderModelMigrator.ts)。测试验证了旧顺序被保留、非法引用被丢弃(ProviderModelMigrator.test.ts)。

6.1 事务与 Logo 处理的联动

provider 行与 model 行在一个同步withWriteTx事务内插入(ctx.db.transaction(...)),orderKey保持 prepare 阶段准备好的顺序(seeded CherryAI 行之后)。事务内部还有一处精心安排的依赖顺序(ProviderModelMigrator.ts):

  1. ensureCherryAiDefaultProviderAndModelTx(tx)写入 seeded CherryAI 行;
  2. 插入 Logo 的file_entry(其file_entry_id外键依赖文件实体先存在);
  3. 插入 provider 行、按 BATCH_SIZE=100 分批插入 model 行;
  4. 插入 Logo ref 行(其source_id外键依赖 provider 行已存在);
  5. 最后写入pin表的置顶模型行(onConflictDoNothing)。

事务结束后,migrator 通过assertOwnedForeignKeys对自己拥有的provider_logoref 表执行PRAGMA foreign_key_check——因为迁移全程foreign_keys = OFF(见 BaseMigrator.ts),插入期不会暴露外键错误,必须主动自检。若事务失败,已写盘的 WebP Logo 文件会被unlinkPreparedImages清理,避免重试时产生孤儿文件。

6.2 Logo 的两种迁移路径

v1 的自定义 provider Logo 存在 Dexie 的image://provider-<id>键下,取值有两种形态(源码注释 ProviderModelMigrator.ts):

  • data URL(base64 上传图或小体积内联图)→ 提升为磁盘上的 WebPfile_entry,由 ref 行引用(logoKey置 null),并打上delete_when_unreferenced清理策略——与运行时bindLogoImage路径一致,provider 被删或换 Logo 时可回收。测试用 1×1 PNG 验证了 WebP 落盘与 ref 行唯一性(ProviderModelMigrator.test.ts)。
  • 非 data: 值(v1PROVIDER_LOGO_MAP[id]的 hashed 构建资源路径)→ 在 v2 中已失效,直接写logoKey会渲染破图。因此从资源名反查品牌并重新表达为 v2 的icon:<catalogKey>引用(recoverV1ProviderLogoIconKey);无法识别的值回退为 null(内置 provider 按 id 取内置图标、自定义 provider 显示首字母头像)。测试覆盖了openai-a1b2c3d4.png → icon:openaimicrosoft.png → icon:azureai、字面量poe → icon:poe、未知值丢弃等场景(ProviderModelMigrator.test.ts)。

七、validate 阶段的完整性校验

validate()(ProviderModelMigrator.ts)执行三类核对:

  1. 行数核对user_provider非 CherryAI/非 CherryCloud 行数 === prepare 统计的 provider 数;user_model同理;pinentityType='model'行数 === 归一化后的置顶数。任一不匹配都会产出provider_count_mismatch/model_count_mismatch/pin_count_mismatch错误。
  2. API Key 抽查:对前 5 个 provider 抽样,若源有apiKey而目标apiKeys为空,且buildProviderApiKeys确实能产出可迁移条目,则报missing_api_key_<providerId>
  3. 统计输出:返回sourceCount/targetCount/skippedCount供引擎汇总。

八、实现文件速查

文件职责
ProviderModelMigrator.tsprepare/投影/事务/pin/校验 主逻辑
mappings/ProviderModelMappings.ts遗留 → v2 字段转换(端点、API Key、认证、能力、定价)
mappings/v1-provider-model-baseline.json钉死的 final-v1 归属基线(含sourceRevision
tests/ProviderModelMigrator.test.ts迁移与归属投影回归测试
mappings/tests/ProviderModelMappings.test.ts字段转换单测

九、小结:可复用的迁移设计模式

ProviderModelMigrator提供了一套值得借鉴的迁移范式:

  1. 钉死基线而非对照活 registry:用带sourceRevision出处信息的静态基线快照判断"历史默认 vs 用户改动",避免 registry 演化污染归属判定;
  2. delta 优先的稀疏落库:只存非默认差异,其余字段读时解析,让行数据随时间跟随 registry 演进;
  3. prepare 期集中清洗:非法/重复/退役数据在事务前剔除并聚合警告,保证 execute 阶段幂等、可控;
  4. 事务内依赖排序 + 外键自检:file_entry → owner 行 → ref 行,配合PRAGMA foreign_key_check主动兜底关闭外键约束的迁移期;
  5. 源侧一次性所有权pinned:models由本 migrator 独家迁移,防止 codegen 重复写入。

理解这套迁移逻辑,不仅能解释 v1 → v2 升级后"为什么我的自定义 provider 配置还在、而默认值悄悄让位给了目录",也能为你在 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

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 8:16:09

Claude Fable 5.1 端点配置与三级缓存验证指南

1. 这不是“换模型”&#xff0c;而是重构整个推理链路&#xff1a;Claude Code 到 Claude Fable 5.1 的本质差异 你搜“Claude Code 怎么换用 Claude Fable 5.1”&#xff0c;点进来的第一反应可能是——不就是改个 API key、换行 URL 吗&#xff1f;我试过&#xff0c;真这么…

作者头像 李华
网站建设 2026/9/12 8:13:32

Codex本地AI网关对接DeepSeek API的工程实践

1. 项目概述&#xff1a;这不是一个“软件安装”&#xff0c;而是一次本地AI开发环境的系统性重建Codex 这个名字&#xff0c;现在听上去有点复古了——它最早是 GitHub 在 2021 年推出的 AI 编程助手原型&#xff0c;后来被整合进 Copilot&#xff1b;但今天你搜到的“2026 Co…

作者头像 李华
网站建设 2026/9/12 8:11:47

程序员高效开发的50个核心工具网站

1. 这50个网站不是“收藏夹清灰清单”&#xff0c;而是程序员每天睁眼就该打开的生存工具箱 你有没有过这种经历&#xff1a;凌晨两点改完线上bug&#xff0c;刚合上笔记本&#xff0c;突然想起某个正则表达式边界条件没验证——结果翻遍浏览器历史、书签栏、微信收藏、Notion文…

作者头像 李华
网站建设 2026/9/12 8:07:15

永磁电机电磁噪声分析与优化实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 8:07:00

Python实现电影院购票系统:毕业设计实战指南

1. 项目背景与核心需求 电影院购票系统作为典型的电子商务应用场景&#xff0c;是计算机相关专业毕业设计的常见选题。这个基于Python的实现方案&#xff08;项目编号56604&#xff09;具有以下典型特征&#xff1a; 业务完整性 &#xff1a;涵盖用户管理、影片排期、座位选择…

作者头像 李华