跳转到内容

状态管理与数据迁移策略

学习目标

  • 理解 Cherry Studio 基于 Redux Toolkit + redux-persist + Dexie IndexedDB 的三层状态管理架构
  • 掌握 20+ Redux Slice 的组织方式与各自职责边界
  • 理解 redux-persist 持久化配置与黑/白名单策略
  • 掌握 Redux 状态迁移(migrate.ts)的编写模式
  • 掌握 Dexie IndexedDB Schema 版本升级与数据迁移流程
  • 理解主进程与渲染进程之间的 StoreSyncService 同步机制
  • 理解 v2 重构冻结策略的设计动机与取舍

项目实践

三层状态管理架构概览

Cherry Studio 的状态管理分为三层,每层承担不同的职责:

三层架构的分工如下:

层级技术方案职责数据规模访问模式
第一层Redux Toolkit运行时内存状态、UI 渲染驱动小(KB 级)高频读写
第二层redux-persist轻量配置持久化、跨会话恢复中(MB 级)低频写入、启动时读取
第三层Dexie IndexedDB大容量结构化数据持久存储大(百 MB 级)按需查询、批量写入

第一层:Redux Store 与 Slice 组织

Store 初始化模式

Store 初始化采用 configureStore 结合 persistReducer 的标准模式:

configureStore({
reducer: persistReducer(persistConfig, rootReducer),
middleware: [
persistMiddleware,
thunkMiddleware,
自定义序列化中间件(处理二进制 / 特殊类型)
],
serializableCheck: { 忽略 redux-persist 相关 action 路径 }
})

持久化配置核心参数:

persistConfig = {
key: "cherry-studio",
storage: localStorage 适配器,
whitelist: ["settings", "llm", "assistants", ...], // 明确列出需要持久化的 slice
blacklist: ["runtime", "selectionStore", ...], // 运行时态不持久化
serialize: 自定义序列化函数(处理 Set、Map、Buffer 等),
deserialize: 自定义反序列化函数
}

Slice 职责矩阵

20+ Slice 按功能域分为以下类别:

各 Slice 的关键设计要点:

Slice核心 State 结构典型 Action特殊处理
assistants助手列表 + 当前选中 ID增删改查、排序与消息系统联动
settings嵌套配置对象(外观 / 行为 / 通知等)单项更新、批量导入导出白名单持久化
llm多提供商配置树切换提供商、模型列表刷新敏感信息加密存储
mcp服务器列表 + 连接状态添加 / 删除 / 重启服务器异步连接状态同步
messageBlock消息块渲染类型映射注册自定义渲染器热插拔扩展点
knowledge知识库条目列表 + 索引状态添加条目、重建索引大容量数据下沉 IndexedDB
paintings图像生成任务队列提交任务、轮询状态图片 Blob 不存入 Redux
memory记忆策略配置切换策略、清除记忆与 LLM 上下文窗口联动
websearch搜索引擎配置 + API Key切换引擎、测试连接Key 加密存储
shortcuts快捷键映射表自定义快捷键、恢复默认与系统快捷键冲突检测
tabs标签页列表 + 激活状态打开 / 关闭 / 切换标签窗口级状态
backup备份策略 + 远程目标触发备份、恢复数据与坚果云等集成
selectionStore当前选中文本 + 位置捕获选区、清除不持久化
runtime运行时标志位(加载中 / 错误等)状态切换不持久化
preprocess预处理管道配置启用 / 禁用预处理步骤管道式链调用
note笔记列表 + 编辑状态增删改笔记大容量下沉 IndexedDB
ocrOCR 引擎配置切换引擎、设置语言图片临时缓存
codeTools代码工具链配置配置解释器路径路径校验
nutstore坚果云 OAuth 状态授权 / 撤销 / 同步OAuth 流程管理
toolPermissions工具调用权限表授予 / 撤销权限安全策略
migrate迁移进度与状态触发迁移、回滚迁移完成后自动清除

第二层:redux-persist 持久化配置

持久化策略

Cherry Studio 对不同的 Slice 采取差异化的持久化策略:

白名单 Slice(需要持久化的配置类数据):

  • settingsllmassistantsmcpknowledgepaintingsmemorywebsearchshortcutstabsbackuppreprocessnoteocrcodeToolsnutstoretoolPermissions

黑名单 Slice(运行时态,不持久化):

  • runtimeselectionStore

自定义序列化器

针对非标准 JSON 类型,Cherry Studio 实现自定义序列化/反序列化逻辑:

序列化伪代码:
function customSerialize(state):
for each key, value in state:
if value is Set:
state[key] = { __type: "Set", __value: Array.from(value) }
else if value is Map:
state[key] = { __type: "Map", __value: Array.from(value.entries()) }
else if value is Buffer or ArrayBuffer:
state[key] = { __type: "Buffer", __value: base64Encode(value) }
else if value is Object:
customSerialize(value) // 递归
return state
反序列化伪代码:
function customDeserialize(state):
for each key, value in state:
if value is Object with __type field:
if value.__type == "Set":
state[key] = new Set(value.__value)
else if value.__type == "Map":
state[key] = new Map(value.__value)
else if value.__type == "Buffer":
state[key] = base64Decode(value.__value)
else if value is Object:
customDeserialize(value) // 递归
return state

Redux 状态迁移(migrate.ts)

迁移模式

migrate.ts 负责在 Redux 持久化状态 schema 发生变更时,安全地将旧版本状态升级到新版本:

迁移函数的典型结构:

迁移函数伪代码:
function migrate(state, currentVersion):
targetVersion = 最新版本号
if state._persist.version < targetVersion:
if state._persist.version < 2:
state = migrateV1ToV2(state)
if state._persist.version < 3:
state = migrateV2ToV3(state)
if state._persist.version < 4:
state = migrateV3ToV4(state)
// ... 依此类推
state._persist.version = targetVersion
return state

单个版本迁移的典型操作:

migrateV2ToV3(state) 示例伪代码:
// 操作 1:字段重命名
state.settings.oldFieldName -> state.settings.newFieldName
// 操作 2:新增字段默认值
state.llm.newProviderSettings = 默认配置对象
// 操作 3:数据结构转换
state.assistants.list = 将旧数组格式转换为新对象映射格式
// 操作 4:废弃数据清理
delete state.deprecatedSlice
// 操作 5:嵌套结构重构
state.mcp.servers = 将扁平列表重构为分组树结构
return state

迁移安全策略

关键安全措施:

  1. try-catch 包裹:每个版本迁移函数独立 try-catch,单一版本迁移失败不阻塞后续版本
  2. 防御性默认值:迁移后的状态必须满足类型约束,所有新增字段提供合理默认值
  3. 版本标记_persist.version 字段标识当前持久化状态版本
  4. 降级兜底:迁移失败时回退到默认初始状态,不阻塞应用启动

第三层:Dexie IndexedDB 结构化存储

数据库 Schema 定义

Dexie 数据库初始化采用版本递进的 Schema 定义模式:

Dexie 数据库初始化伪代码:
db = new Dexie("CherryStudio")
db.version(1).stores({
files: "++id, name, path, size, createdAt",
topics: "++id, title, assistantId, createdAt, updatedAt"
})
db.version(2).stores({
files: "++id, name, path, size, createdAt, mimeType", // 新增 mimeType 索引
topics: "++id, title, assistantId, createdAt, updatedAt",
settings: "++id, key, value, updatedAt" // 新增表
})
db.version(5).stores({
files: "++id, name, path, size, createdAt, mimeType, hash",
topics: "++id, title, assistantId, createdAt, updatedAt",
settings: "++id, key, value, updatedAt",
knowledge_notes: "++id, knowledgeBaseId, title, content, createdAt",
translate_history: "++id, sourceLang, targetLang, sourceText, translatedText, createdAt",
quick_phrases: "++id, category, content, usageCount"
}).upgrade(upgradeToV5)
db.version(7).stores({
files: "++id, name, path, size, createdAt, mimeType, hash",
topics: "++id, title, assistantId, createdAt, updatedAt",
settings: "++id, key, value, updatedAt",
knowledge_notes: "++id, knowledgeBaseId, title, content, createdAt, tags",
translate_history: "++id, sourceLang, targetLang, sourceText, translatedText, createdAt",
quick_phrases: "++id, category, content, usageCount",
message_blocks: "++id, messageId, type, content, createdAt"
}).upgrade(upgradeToV7)
db.version(8).stores({
files: "++id, name, path, size, createdAt, mimeType, hash, embedding",
topics: "++id, title, assistantId, createdAt, updatedAt",
settings: "++id, key, value, updatedAt",
knowledge_notes: "++id, knowledgeBaseId, title, content, createdAt, tags, embedding",
translate_history: "++id, sourceLang, targetLang, sourceText, translatedText, createdAt",
quick_phrases: "++id, category, content, usageCount",
message_blocks: "++id, messageId, type, content, createdAt",
translate_languages: "++id, code, name, usageCount"
}).upgrade(upgradeToV8)

表结构与索引设计

表名主键核心索引数据量级访问频率
files++idname, path, hash百 - 千级
topics++idassistantId, updatedAt千级
settings++idkey百级
knowledge_notes++idknowledgeBaseId, tags千 - 万级
translate_history++idsourceLang, targetLang千 - 万级
quick_phrases++idcategory百级
message_blocks++idmessageId, type万级
translate_languages++idcode百级

Schema 升级函数模式

每个 upgrade 函数接收一个 upgradeTransaction 对象,可在迁移过程中操作旧数据:

典型升级函数实现模式:

upgradeToV5 伪代码:
function upgradeToV5(transaction):
// 场景 1:新增字段,为已有记录填充默认值
filesTable.eachEntry(entry):
if entry.mimeType is undefined:
entry.mimeType = detectMimeType(entry.name)
transaction.files.put(entry)
// 场景 2:新表初始化
// knowledge_notes 和 translate_history 为新表,无需数据迁移
upgradeToV7 伪代码:
function upgradeToV7(transaction):
// 场景 1:为已有表新增索引字段
knowledgeNotesTable.eachEntry(entry):
if entry.tags is undefined:
entry.tags = [] // 默认空标签
transaction.knowledge_notes.put(entry)
// 场景 2:新建 message_blocks 表
// 从历史消息中提取 block 数据(如果需要)
upgradeToV8 伪代码:
function upgradeToV8(transaction):
// 场景 1:添加 embedding 向量字段
filesTable.eachEntry(entry):
if entry.embedding is undefined:
entry.embedding = null // 懒加载,按需生成
transaction.files.put(entry)
knowledgeNotesTable.eachEntry(entry):
if entry.embedding is undefined:
entry.embedding = null
transaction.knowledge_notes.put(entry)
// 场景 2:新增 translate_languages 字典表
// 预填充常用语言列表

StoreSyncService:主进程与渲染进程同步

IPC 同步架构

Electron 架构中,主进程(Main Process)和渲染进程(Renderer Process)之间的状态通过 StoreSyncService 进行同步:

同步策略

StoreSyncService 核心逻辑伪代码:
class StoreSyncService:
// 渲染进程侧
function init(store):
// 订阅 Redux Store 变更
store.subscribe(() =>
changedKeys = 计算变更的 key 集合
if changedKeys 在白名单中:
通过 IPC 发送变更到主进程
)
// 监听主进程推送的更新
IPC.on("STORE_UPDATED", (payload) =>
store.dispatch(同步 Action, 合并主进程数据)
)
// 主进程侧
function init():
// 监听渲染进程变更
IPC.on("SYNC_STORE", (payload) =>
写入本地 store.json 文件
广播到其他渲染进程窗口
)
// 启动时加载持久化状态
savedState = 从 store.json 读取
通过 IPC 发送给渲染进程

v2 重构冻结策略

冻结背景

Cherry Studio 计划在 v2.0.0 进行一次大规模架构重构。在此背景下,项目对 Redux Slice 和 IndexedDB Schema 采取了冻结策略

冻结标记规范

在源码中使用标准 JSDoc 标记废弃组件:

@deprecated 标记模式:
/**
* @deprecated Scheduled for removal in v2.0.0
* 原因:v2 将使用新的状态管理方案替代
* 替代方案:(如有则说明)
*/

被标记的组件在 v2 之前的版本中继续维护但不扩展

  • 修复 Bug:允许
  • 性能优化:允许
  • 新增功能 / 修改 Schema:禁止
  • 重构内部实现:禁止(除非存在严重安全漏洞)

问题与规避

问题一:redux-persist 序列化失败

症状:控制台报 TypeError: state is not serializable 错误,应用启动时状态丢失。

根因:Slice 状态中包含 SetMapBufferDate、自定义类实例等非标准 JSON 类型,redux-persist 默认序列化器无法处理。

规避方案

  1. serializableCheck.ignoredPaths 中排除包含非标准类型的路径
  2. 实现自定义序列化/反序列化函数(见上文”自定义序列化器”部分)
  3. 尽量在 Slice 中使用纯 JSON 兼容类型,避免在 Redux 状态中存储类实例

问题二:迁移函数遗漏字段导致运行时错误

症状:迁移后应用崩溃,报 Cannot read properties of undefined

根因:迁移函数未为新增字段提供默认值,导致下游组件访问 undefined

规避方案

  1. 每个迁移函数编写前,先列出所有新增字段及其默认值清单
  2. 迁移函数末尾执行完整性校验(invariant 或手动断言)
  3. 在迁移函数外围包裹 try-catch,失败时回退到默认初始状态
  4. 为每个迁移函数编写单元测试,覆盖旧版本状态输入

问题三:Dexie 升级中断导致数据库损坏

症状:IndexedDB 打开失败,报 UnknownErrorVersionError

根因upgrade 函数中抛出未捕获异常,导致事务回滚,数据库处于中间状态。

规避方案

  1. upgrade 函数中使用 try-catch 捕获所有异常
  2. 使用事务的原子性:所有写操作在同一个事务中,任一失败则全部回滚
  3. 为每个版本升级编写独立测试,使用内存 IndexedDB 模拟(如 fake-indexeddb
  4. 保留降级脚本:在极端情况下可以从新版本回退到旧版本

问题四:StoreSyncService 同步冲突

症状:多窗口场景下状态不一致,一个窗口的修改在另一个窗口未生效或被覆盖。

根因:两个窗口同时修改同一字段,后到达的写操作覆盖了先到达的。

规避方案

  1. 采用乐观更新 + 冲突检测:每个状态变更附带时间戳或向量时钟
  2. 主进程按时间戳排序,保留最新的变更
  3. 对关键字段(如助手配置)采用锁定机制,同一时刻只允许一个窗口写入
  4. 用户可见的冲突提示:当检测到冲突时通知用户手动选择

问题五:localStorage 容量超限

症状QuotaExceededError,redux-persist 写入失败。

根因:白名单中包含了大容量数据(如图片 Base64、大量消息记录),超出 localStorage 5-10MB 限制。

规避方案

  1. 严格审查白名单,仅包含轻量配置数据
  2. 大容量数据(图片、文件元数据、消息记录)下沉到 Dexie IndexedDB
  3. 实现自动清理策略:定期清理过期的持久化数据
  4. 监控 localStorage 使用量,接近上限时发出警告

设计取舍

为什么选择三层架构而非单一方案?

方案优势劣势Cherry Studio 的取舍
纯 Redux(无持久化)简单一致,开发体验好刷新页面丢失状态不可接受,用户配置必须持久化
Redux + localStorage 手动存取简单,无额外依赖序列化复杂,容易遗漏不可扩展,20+ Slice 手动管理不现实
Redux + redux-persist自动化持久化,版本迁移仅支持 localStorage,容量受限接受,用于轻量配置持久化
Redux + Dexie 全量存储容量无限制,支持复杂查询异步操作复杂,与 Redux 同步模式不一致折中:大容量数据下沉,小配置仍走 redux-persist
三层架构(最终方案)各层职责清晰,容量与性能兼顾架构复杂度高,维护成本大采用:Redux 驱动 UI,redux-persist 持久化配置,Dexie 存储大容量数据

为什么冻结 v1 的 Slice 和 Schema?

方案优势劣势Cherry Studio 的取舍
持续迭代 v1渐进式改进,风险低历史债务累积,架构越来越臃肿v2 将重构状态管理方案,继续迭代会增加迁移成本
立即废弃 v1 转向 v2无历史债务v2 开发周期长,中间版本功能停滞v2 尚未就绪,不可立即切换
冻结 v1 + 并行开发 v2v1 稳定运行,v2 不受约束维护两套代码,短期成本高采用:v1 仅修 Bug 不新增,v2 完成后一次性切换

冻结策略的核心逻辑:在 v2 完成之前,任何对 v1 状态管理方案的修改都将成为 v2 迁移时的额外负担。通过冻结,确保 v1 到 v2 的迁移路径清晰、可控。

为什么 Dexie Schema 逐步递进而非一步到位?

方案优势劣势Cherry Studio 的取舍
一次性定义完整 Schema无版本升级逻辑,代码简洁无法适应需求变化,已安装用户数据丢失不可行,用户需求持续变化
逐步递进版本(最终方案)平滑升级,已有用户数据无损迁移升级逻辑复杂,需要维护多个版本迁移函数采用:每个功能迭代对应一个版本升级
按需动态 Schema最灵活Dexie 不支持运行时 Schema 变更不可行

参考来源