ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Cherry Studio v2 迁移系列:PaintingMigrator 绘画面板历史数据从 Redux 到 SQLite 的迁移设计

Cherry Studio v2 迁移系列:PaintingMigrator 绘画面板历史数据从 Redux 到 SQLite 的迁移设计 Cherry Studio v2 迁移系列PaintingMigrator 绘画面板历史数据从 Redux 到 SQLite 的迁移设计【免费下载链接】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 一次性迁移框架中的PaintingMigrator展开深入剖析绘画面板历史数据image generation / edit / remix / upscale 记录如何从 v1 的 Reduxpaintingsslice 迁入 v2 的 SQLitepainting表与painting_file_ref表。你将理解迁移器的数据源与目标表、命名空间归一化与orderKey全局排序规则、输出/输入文件引用为何必须以独立关联表承载、悬空引用如何被预过滤以避免PRAGMA foreign_key_check失败以及 modelId 与user_model的跨提供方校验逻辑。源码级实现与测试用例PaintingMigrator.ts 与 PaintingMigrator.test.ts将作为全文的事实依据。一、PaintingMigrator 在 v2 迁移体系中的定位Cherry Studio v2 将 v1 时代存储在 Redux运行时状态与 Dexie文件元数据中的数据一次性迁移进 SQLite 新库。所有迁移器由 migratorRegistry.ts 按固定顺序组装执行PaintingMigrator的注册信息如下属性值含义idpainting迁移器唯一标识namePainting展示名descriptionMigrate painting history from Redux to SQLite职责描述order4.5在FileMigrator(2.7) 之后、TranslateMigrator之前执行这个order 4.5并非随意取值它决定了 PaintingMigrator 可以依赖哪些先行的迁移结果。从 migratorRegistry.ts 可见ProviderModelMigratororder 1.75与FileMigratororder 2.7都排在它前面——这正是本迁移器两个核心校验modelId 对账、文件引用预过滤能够成立的前提下文会分别展开。与BaseMigrator的约定一致PaintingMigrator实现了标准的三个生命周期阶段prepare(ctx)从 Redux 读取数据、逐条归一化、生成警告但不写库execute(ctx)在单个 SQLite 事务内分批插入painting行与painting_file_ref行validate(ctx)核对目标表行数是否与 prepare 阶段准备的行数一致。迁移器内部维护sourceCount、skippedCount、preparedPaintings、preparedFileRefs、droppedFileRefs与warnings六个私有状态reset()会全部清空保证同一实例可安全重跑。二、数据源与目标从 Redux slice 到两张 SQLite 表2.1 数据源按 README-PaintingMigrator.md 的定义PaintingMigrator只读取 Redux 的paintingsslice通过ctx.sources.reduxState.getCategory(paintings)获取不依赖 Dexie作为主迁移载荷来源。这与 FileMigratorDexiefiles表形成明确分工——绘画面板的文件元数据早已由 FileMigrator 迁入file_entryPaintingMigrator 只关心绘画记录本身。prepare()中对 state 的空值防护同样清晰若paintings分类不存在或不是对象直接返回success: true, itemCount: 0并附带警告No painting Redux state found - skipping painting migration——空数据源不是失败而是合法跳过。2.2 目标表迁移目标为两张 SQLite 表painting表painting.ts列类型说明iduuidPrimaryKey()主键UUIDprovider_idtext, NOT NULL提供方 IDmodel_idtext, 可空模型 ID可为空prompttext, NOT NULL提示词orderKey三列text排序键orderKeyColumnscreatedAt/updatedAtinteger时间戳架构注释给出了设计意图painting 行是已完成图像生成的冻结凭证frozen receipt。输出/输入文件不存储在行上而是通过零条或多条painting_file_ref行sourceIdpainting.id,roleoutput|input关联删除 painting 行时关联行由数据库级联删除。冻结凭证形状刻意不携带可变表单状态mode、size、seed 等——活跃的绘画草稿保存在渲染进程的 React 状态中应用退出即丢弃。painting_file_ref表fileRelations.ts列说明idUUID 主键file_entry_idNOT NULL外键 →file_entry.idonDelete: cascadesource_idNOT NULL外键 →painting.idonDelete: cascaderoleNOT NULL枚举output/inputcreatedAt/updatedAt时间戳并带有三个约束pfr_entry_id_idx、pfr_source_id_idx两个索引(file_entry_id, source_id, role)唯一索引pfr_unique_idx以及pfr_role_check角色校验。三、命名空间归一化13 个 legacy namespace → providerId modev1 的paintingsslice 是按提供方 × 动作类型分 namespace 存储的数组。PaintingMappings.ts中定义了完整的 legacy 命名空间清单PaintingMappings.tsexport const LEGACY_PAINTING_NAMESPACES [ siliconflow_paintings, dmxapi_paintings, tokenflux_paintings, // Provider retired in v2但历史是冻结凭证prompt 本地输出文件仍可读 zhipu_paintings, aihubmix_image_generate, aihubmix_image_remix, aihubmix_image_edit, aihubmix_image_upscale, openai_image_generate, openai_image_edit, ovms_paintings, ppio_draw, ppio_edit ] as constgetPaintingFilter()将每个 namespace 映射为{ providerId, mode }二元组PaintingMappings.tsNamespaceproviderIdmodesiliconflow_paintingssilicongeneratetokenflux_paintingstokenfluxgeneratezhipu_paintingszhipugenerateaihubmix_image_generateaihubmixgenerateaihubmix_image_remixaihubmixremixaihubmix_image_editaihubmixeditaihubmix_image_upscaleaihubmixupscaleopenai_image_generaterecord.providerId ?? new-apigenerateopenai_image_editrecord.providerId ?? new-apieditovms_paintingsovmsgenerateppio_drawppiodrawppio_editppioeditdmxapi_paintingsdmxapi按record.generationModeedit/merge/ 默认generate需要注意三点细节tokenflux_paintings是已退役提供方v2 中该提供方已下线见src/main/data/retiredProviders.ts但其历史绘画记录仍被迁移因为记录本质是冻结凭证——prompt 与本地输出文件保持可读。OpenAI 兼容命名空间动态解析 providerIdopenai_image_generate/openai_image_edit优先读取记录自身的providerId字段缺失时回退到new-api并产生警告Defaulted missing OpenAI-compatible providerId to new-api。dmxapi_paintings的 mode 由generationMode字段驱动edit/merge/ 默认generate。关键规则归一化后的providerId与运行时mode中只有providerId持久化到行上mode不落库见 README-PaintingMigrator.md 的 Key Rules——v2 的绘画行是冻结凭证mode 属于可丢弃的可变表单状态。四、记录变换id / modelId / prompt / 文件引用的提取规则transformLegacyPaintingRecord()PaintingMappings.ts是单条记录的核心变换函数其输出为{ ok, value, files, warnings }4.1 行字段value迁移后的行只保留四类字段id、providerId、modelId、prompt外加 prepare 阶段统一附加的orderKey。id必须是非空字符串否则记为missing_id失败skippedCount并警告promptrecord.prompt字符串或空字符串modelId归一化逻辑见下节。4.2 空占位过滤v2 painting 行是冻结凭证prompt 输出文件是用户唯一可见的产物。因此当prompt为空、且输出与输入文件列表均为空时该记录被判定为empty_placeholderv1 中仅持有taskId、无任何可恢复状态的挂起任务直接过滤。但仅含输入文件、无输出的 edit 记录仍然通过——输入文件会作为 ref 被保留。4.3 modelId 归一化的三级策略normalizeLegacyModelId()PaintingMappings.ts处理 v1 中模型引用的各种形态若modelId是对象LegacyModelRef 形态先经legacyModelToUniqueId()转换若得到的是已是 unique id形如providerId::modelId经UniqueModelIdSchema校验直接解析复用否则以createUniqueModelId(providerId, rawModelId)按当前绘画的 providerId 重新作用域化生成providerId::modelId形态的 v2 unique id。resolveLegacyPaintingModelId()同时回退检查record.model字段。任何一步失败非法模型 id都会产生警告并返回null。4.4 输出 / 输入文件引用提取输出文件getFileIds(record.files)——v1 的files数组内每个对象取其id字段输入文件buildInputFileIds()PaintingMappings.ts——dmxapi_paintings读imageFiles其他命名空间依次尝试imageFiles数组、单个imageFile对象内存态引用丢弃若输入引用只是内存字符串object URL / base64 字段即getFileId()无法从对象中取出id时产生警告Dropped legacy input image reference because only an in-memory string/object URL was available并丢弃——这类引用无法从持久化文件元数据重建。五、跨命名空间 ID 冲突重写为全新 uuidv4 而非复合字符串v1 各 namespace 独立编号同一绘画 id 可能出现在多个 namespace 中。prepare()用seenIds集合检测冲突PaintingMigrator.tsif (seenIds.has(normalized.id)) { const duplicateId normalized.id normalized.id uuidv4() // 重写为全新 v4 UUID this.warnings.push(Rewrote duplicate painting id ${duplicateId} to ${normalized.id} during migration) }这里有一个必须使用uuidv4()而非复合字符串的深层原因迁移器随后要为每个绘画行发射painting_file_ref行其sourceId在共享类型paintingFileRefSchema中校验为z.uuidv4()。若按早期方案重写为${id}_${ns}_${i}形式的复合字符串该 ref 行的sourceId将无法通过 schema 校验。测试用例rewrites a cross-namespace duplicate id to a fresh uuidv4 so painting_file_refs validatePaintingMigrator.test.ts专门验证了这一行为两个 namespace 中出现相同 id 时第二行被重写为独立 UUID且每一条发射出的 ref 行包括被重写的 sourceId都能通过paintingFileRefSchema.parse()。此外prepare()按 namespace 分组后再按顺序展平groupedRecords→normalizedRows保证输出顺序稳定可预期。六、全局顺序排序assignOrderKeysInSequence 与 fractional-indexingv2 的排序键基于fractional-indexingbase62 小数索引。迁移层有两条专用工具函数migration/v2/utils/orderKey.ts函数语义assignOrderKeysInSequence(rows)按输入顺序对整组行分配连续递增的 orderKeyassignOrderKeysByScope(rows, getScope)按 scope 分桶桶内独立分配 key 空间PaintingMigrator使用的是前者assignOrderKeysInSequencePaintingMigrator.ts含义明确orderKey 在整个迁移集上按源顺序全局分配不做 per-providerId 作用域切分也没有数值型sortOrder列。底层键生成复用运行时的generateOrderKeySequence()services/utils/orderKey.ts该文件是fractional-indexing包的唯一合法导入点迁移层文件禁止直接导入该包。assignOrderKeysInSequence是纯函数不触碰数据库返回新数组而不修改输入。七、文件引用预过滤让 PRAGMA foreign_key_check 永不中断7.1 问题背景v1 绘画记录的files/imageFile中携带的file_entry.id未必全部存在于 v2 的file_entry表中——FileMigrator 会跳过 v1 中畸形malformed的行。若 PaintingMigrator 对这些悬空 id 照单全收地插入painting_file_ref外键约束将在引擎最终的PRAGMA foreign_key_check阶段引爆导致整次迁移中止。7.2 execute 阶段的解决路径execute()在事务内分四步处理PaintingMigrator.ts收集去重遍历preparedFileRefs将全部 output/input id 汇入allFileIds集合分块对账按INARRAY_CHUNK 500分批执行SELECT id FROM file_entry WHERE id IN (...)规避 SQLite 单条语句参数上限约 999将命中的 id 记入existingIds逐条筛选发射对每条绘画记录的 output/input id仅当存在于existingIds时才生成painting_file_ref行含role: output | input、sourceId: painting.id、UUID 主键与时间戳不存在的 id 计数到droppedFileRefs并跳过去重插入与清理标记ref 行按INSERT_BATCH_SIZE 100分批onConflictDoNothing()插入随后对所有被引用的fileEntryId调用markEntriesAutoCleanup()。markEntriesAutoCleanup()FileMigrator.ts将file_entry.cleanup_policy翻转为delete_when_unreferencedFileMigrator 插入所有 v1 文件时一律标manual因为它无从知晓谁将引用这些文件而 Chat / Painting 等迁移器写入 ref 后被引用的条目才具备自动清理资格。该函数按CLEANUP_UPDATE_CHUNK_SIZE分块 UPDATE幂等安全且必须在与 ref 插入相同的事务内调用保证被引用与否与清理策略原子提交。7.3 测试如何守护这条不变量PaintingMigrator.test.ts 通过setupTestDatabase()以真实 SQLite 生产迁移 开启foreign_keys ON的方式集成测试——比迁移运行时引擎在verifyForeignKeys之前保持外键关闭更严格一旦悬空防护回归插入瞬间就会触发 FK 约束并返回successfalse。典型用例混合悬空一个 painting 引用存在输出 缺失输出另一个 edit painting 仅引用缺失输入——两条绘画行照常迁移但只发射 1 条 refdroppedFileRefs 2最终PRAGMA foreign_key_check返回空PaintingMigrator.test.ts全部悬空不 seed 任何file_entry全部引用被丢弃droppedFileRefs如实计数绘画行仍成功迁移PaintingMigrator.test.ts超过参数上限1200 个文件 id 触发INARRAY_CHUNK分块所有 ref 正常发射且 1200 个被引用条目全部翻转为delete_when_unreferenced——测试注释特别强调若markEntriesAutoCleanup分块出现 off-by-one部分文件会停留在manual而永远不会被回收PaintingMigrator.test.ts。7.4 清理策略翻转的可见性第二个测试用例PaintingMigrator.test.ts验证被 painting 引用的文件cleanup_policy变为delete_when_unreferenced而仅存在于file_entry、未被任何绘画引用的文件保持manual——这正是文件清理系统按引用状态分类的核心语义。八、modelId 对账清除悬空与跨提供方引用8.1 为什么必须对账如果 painting 行的 modelId 在user_model表中没有对应行composer 将显示一个无法修复的禁用发送按钮。PaintingMigrator的 order 为 4.5此时ProviderModelMigratororder 1.75已填充user_model表因此可以在 prepare 阶段直接对账。8.2 双重校验id 存在 providerId 匹配user_model.id的形态是providerId::modelId。特殊之处在于painting 的modelId可能携带与 painting 自身providerId不同的提供方前缀例如 legacyaihubmix绘画引用了gemini::imagen。渲染进程是按 painting 的 providerId 解析模型的因此跨提供方引用与完全缺失同样悬空。对账逻辑PaintingMigrator.tsfor (const row of normalizedRows) { if (row.modelId) { const modelProviderId existingModelProviders.get(row.modelId) if (modelProviderId undefined) { // 无对应 user_model 行 → 清空 row.modelId null } else if (modelProviderId ! row.providerId) { // user_model 行属于其他 provider → 同样清空 row.modelId null } } }两种情形都会产生对应警告no matching user_model row exists或user_model row belongs to provider ...但不阻塞迁移——modelId被置空后绘画回退到模型选择流程。8.3 测试佐证用例nulls modelId when it has no user_model row or matches a different providerPaintingMigrator.test.ts预置两个 user_modelaihubmix::gemini-imagen属 aihubmix、gemini::imagen属 gemini再让三条 aihubmix 绘画分别引用匹配 id 保留原值、跨提供方 id 清空、不存在 id 清空——prepare 返回 2 条对账警告最终三行全部成功迁移。九、丢弃字段全景v2 行不携带什么综合 README-PaintingMigrator.md 的 Dropped Fields / Columns 与源码被显式丢弃的 v1 字段包括丢弃项原因 / 去向JSONfiles列由painting_file_ref行取代一对多关联mediaType图片 vs 视频在展示时从文件派生无需持久化运行时态urls仅存在于内存无可持久化价值异步任务 idgenerationId/taskId不持久化在凭证行上legacy 绘画树父字段v2 无树结构UI 状态字段status/ppioStatus属于可变 UI 状态无法从持久化文件元数据重建的输入图片引用已在上文内存态引用丢弃说明这印证了 v2 的设计哲学painting 行是展示层回放的冻结凭证而非可变的工作区状态。活跃表单状态mode、size、seed、草稿由渲染进程 React 状态承载并在退出时丢弃。十、批次策略与进度报告execute()的两个批次常量常量值用途INSERT_BATCH_SIZE100painting与painting_file_ref的插入批次大小INARRAY_CHUNK500file_entryid 对账查询的 IN 子句分块大小绘画行插入每完成一批即调用reportProgress()上报百分比进度Migrated x/y painting records并在整个 execute 过程使用单一事务ctx.db.transaction保证 painting 行、ref 行与清理策略翻转原子提交。十一、验证行为与错误处理validate()核对SELECT count(*) FROM painting是否等于preparedPaintings.length不一致时产生painting_count_mismatch错误正常时返回stats: { sourceCount, targetCount, skippedCount }。混跑测试PaintingMigrator.test.ts验证了sourceCount: 2, targetCount: 2, skippedCount: 0的结果。错误处理遵循失败语义的三种形态场景处理缺 id 记录skippedCount警告Skipped {ns}[{i}] because it has no id空占位记录skippedCount警告Skipped {ns}[{i}] because it is an empty placeholder插入异常DB 约束、磁盘满等事务抛出execute()返回success: false 错误信息所有跳过与重写都会进入warnings列表随PrepareResult透出供引擎汇总展示给用户。十二、小结PaintingMigrator 的迁移不变量回顾全文PaintingMigrator 用五条不变量保证了绘画历史的无损、一致迁移凭证化行只携带id/providerId/modelId/promptorderKey可变状态全部剥离全局排序orderKey经assignOrderKeysInSequence全量顺序分配无 per-provider 作用域引用外置文件 id 不落绘画行通过(sourceId, role)关联表表达悬空预过滤ref 发射前对账file_entrydroppedFileRefs计数PRAGMA foreign_key_check永不中断模型可解析modelId对账user_model存在 provider 匹配悬空则置空回退。从 PaintingMigrator.ts 的实现到 PaintingMigrator.test.ts 的九组集成用例这套设计既有清晰的职责边界Redux 读取、SQLite 写入、与 FileMigrator / ProviderModelMigrator 的协作时序又有严格的可验证性真实外键、真实分块、真实 schema 校验。对于需要在 Cherry Studio 上二次开发或研究其数据迁移架构的开发者而言PaintingMigrator 是理解旧状态如何变成新凭证的绝佳样本。延伸阅读迁移框架核心见 v2 迁移 README文件迁移与清理策略衔接见 FileMigrator 文档 与 file-entry-cleanup排序键运行时实现见 services/utils/orderKey.ts。【免费下载链接】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),仅供参考
返回列表