ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Cherry Studio AiUsageRecordMigrator 深度解析:v1 消息用量向 v2 不可变 AI 用量记录的迁移

2026/9/12 17:48:13 拓冰建站 浏览量
Cherry Studio AiUsageRecordMigrator 深度解析:v1 消息用量向 v2 不可变 AI 用量记录的迁移 Cherry Studio AiUsageRecordMigrator 深度解析v1 消息用量向 v2 不可变 AI 用量记录的迁移【免费下载链接】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 数据迁移管线中的AiUsageRecordMigratorAI 用量记录迁移器讲解如何把 v1 历史助手消息与 Agent 会话消息中的 token 用量、请求次数估算与成本信息迁移为ai_usage_record表中不可变的legacy-aggregate聚合记录。读完本文你将掌握该迁移器的数据来源与目标结构、关键变换规则请求次数估算、成本货币语义、身份快照、幂等键设计、keyset 分批读取、失败重试与校验契约并理解它如何在不读取当前提供商/模型/定价配置的前提下保证历史数据忠实回放。迁移背景与定位Cherry Studio 的 v2 迁移src/main/data/migration/v2由一系列领域迁移器Migrator组成每个迁移器负责一个业务域的 prepare / execute / validate 三阶段处理基类见 BaseMigrator.ts。AiUsageRecordMigrator类定义于 AiUsageRecordMigrator.ts负责的领域是把 v1 时代沉淀在消息上的用量/成本数据回放成 v2 的独立 AI 用量记录。它的执行顺序为order 4.1源码第 188 行必须在ChatMigrator与AgentsMigrator之后运行因为它的数据源正是这两个迁移器产出的 SQLitemessage与agent_session_message行。测试用例也验证了这一点is registered after chat migration见 AiUsageRecordMigrator.test.ts断言chat.order usage.order。数据来源Sources迁移器只消费两类候选行全部来自已完成的 v1 迁移产物来源表产生方候选条件SQLitemessageChatMigrator普通对话消息role assistant且stats非空SQLiteagent_session_messageAgentsMigratorAgent 会话消息role assistant且stats非空候选统计函数countCandidateRows源码第 41-55 行对两张表分别执行count(*)并求和readChatCandidateRows与readAgentSessionCandidateRows第 57-105 行则按同样的role assistant AND stats IS NOT NULL条件读取候选行并分别标记messageKind为chat与agent-session。值得注意的是只有带用量的助手消息才是候选。迁移器不读取当前的 provider、model、assistant、agent、API key 或定价状态——这一点在文档与源码中一以贯之迁移器只查询message/agent_session_message两张表不触碰任何配置表。用量信号判定单条消息是否真的带用量由hasUsageSignal源码第 28-39 行判定只要满足以下任一条件即视为候选inputTokens/outputTokens/totalTokens存在inputTokenDetails.noCacheTokens/cacheReadTokens/cacheWriteTokens存在outputTokenDetails.reasoningTokens存在costs数组非空。没有任何用量/成本信号的消息会在toLegacyAggregate中返回null并被跳过第 139-141 行。目标表结构不可变的 legacy-aggregate 记录目标是 SQLiteai_usage_record表schema 见 aiUsageRecord.ts。该表同时服务两类记录recordKind见 types/aiUsageRecord.ts 中的AiUsageRecordKindSchemainvocation运行时逐次记录的单次真实调用requestCount 1且必须有providerId与modelIdlegacy-aggregate本次迁移器写入的历史聚合记录requestCount 1且messageKind、messageId非空。schema 中的ai_usage_record_kind_identity_checkCHECK 约束aiUsageRecord.ts从数据库层面强制了这两类记录的形态差异。表注释明确指出历史 v1 助手消息聚合与运行时调用共享同一张表所有身份字段都是请求时刻的快照刻意不设外键这样后续的改名或删除不会改变历史归属。每条迁移记录的核心字段字段说明requestId稳定幂等键legacy:message-kind:message-id有唯一索引ai_usage_record_request_id_idxrequestCount请求次数估算可能代表多次估算的 provider 调用不表述为单次调用messageKind/messageId来源消息的类型与 IDproviderId/modelId/providerName/modelName迁移后的模型身份可为 nullsourceType/sourceId/sourceName/sourceIcon来源身份快照assistant / agenttoken 字段inputTokens、outputTokens、totalTokens、reasoningTokens、noCacheTokens、cacheReadTokens、cacheWriteTokenscost 元组cost、costCurrency、costSourceprovider/computed、costBreakdownapiKeyAttribution对历史记录固定为unknown时序字段timeFirstTokenMs、timeCompletionMs、timeThinkingMs恒为 nullcreatedAt消息创建时间戳legacyToRowAiUsageRecordService.ts展示了从LegacyAggregateInput到数据库行的完整映射apiKeyAttribution: unknown、时序三字段硬编码为null、modality默认language、costSource继承来源信号的provider/computed语义。关键变换规则1. Token 用量直接复制toLegacyAggregateAiUsageRecordMigrator.ts把迁移后MessageStats中的 token 字段原样复制到记录的 usage 对象输入/输出/总计 token、推理 token来自outputTokenDetails.reasoningTokens、以及缓存三件套noCacheTokens、cacheReadTokens、cacheWriteTokens。只复制存在的字段不补零。2. 请求次数估算requestCount的语义requestCount使用ChatMigrator/AgentsMigrator持久化下来的估算值第 147 行Math.max(1, stats.estimatedRequestCount ?? stats.requestCount ?? 1)。该估算的起点为 1每当出现一组连续的工具调用之后又跟着更多模型输出就加 1。估算规则要点并行工具调用算一组引用citation/文件/来源块不会拆分工具组末尾的工具组不增加计数。这条规则直接决定了 legacy-aggregate 的requestCount可能大于 1——它是历史消息在多次调用场景下的尽力而为聚合因此文档明确提醒ItsrequestCountmay represent multiple estimated provider calls; it is not presented as a single invocation.3. 成本语义只保留显式存储的 v1 成本迁移器只保留显式存储的 v1 成本语义缺失的历史成本不会用当前定价重算resolveLegacyCost第 129-137 行只从stats.costs中取排序后的第一条。对迁移后的模型定价映射规则是仅当货币为缺失或$时映射为 USD¥/映射为 CNY其他不支持的遗留货币符号直接丢弃而不是猜测相关货币枚举见 types/model.ts 的CURRENCY。costSource依据providerReportedRequestCount 0判定为provider或computed源码第 135 行保留成本是提供商上报还是本地计算的历史语义。4. 来源身份只来自不可变快照来源身份assistant/agent 的名称、头像只从不可变的messageSnapshot复制第 151-160 行绝不从当前配置反查。provider/model 身份在迁移行已携带时予以保留resolveLegacyModel第 107-127 行优先取messageSnapshot.model否则尝试parseUniqueModelId解析modelId解析失败则退化为只保留原始modelId。历史无法识别身份时两者都可能保持 null不会用可变配置回填。5. 时序字段保持 nulltimeFirstTokenMs、timeCompletionMs、timeThinkingMs在记录上恒为 nulllegacyToRow第 1371-1373 行因为历史时序描述的是整条消息的耗时而不是其中某一次估算的 provider 调用。测试用例也验证了这一点源消息 stats 里明明有timeFirstTokenMs: 100, timeCompletionMs: 500, timeThinkingMs: 80迁移出的记录这三项仍为 nullAiUsageRecordMigrator.test.ts。6. 反向重建 MessageStats 用量投影插入记录后迁移器通过aiUsageRecordService.recordLegacyAggregatesTx内的rebuildMessageUsageProjectionTxAiUsageRecordService.ts反向重建源消息的MessageStats用量/成本/请求字段按messageKind messageId汇总ai_usage_record中的记录重新生成 token 明细、requestCount、estimatedRequestCount、unpricedRequestCount、costs与providerPerformancegetMessageUsageProjectionTx第 1052-1150 行同时保留历史的消息级时序mergeMessageUsageProjection只删用量投影键与记录自有键见第 1018-1035 行。测试断言迁移后messageTable.stats仍保留timeFirstTokenMs: 100, timeCompletionMs: 500, timeThinkingMs: 80测试第 174-180 行。7. 字段映射总表来源目标message kind idrequestId、messageKind、messageId持久化的估算值requestCount迁移后的模型身份可空的providerId、modelIdmessageSnapshot可空的来源快照sourceType/sourceId/sourceName/sourceIconmessage 用量token/缓存字段显式存储的 v1 成本cost 元组message 创建时间戳createdAt不可用的按调用时序null 的调用指标字段进度、重试与校验Progress, Retry, ValidationAiUsageRecordMigrator继承BaseMigrator的 prepare / execute / validate 三阶段契约具体行为如下。prepare()候选计数prepare源码第 202-205 行调用countCandidateRows统计 chat 与 agent-session 两类的候选消息总数返回{ success: true, itemCount }供上层展示与进度计算。execute()keyset 分批 事务回退execute第 207-271 行是核心keyset 游标对两类来源分别用升序 id keyset 游标afterId ? gt(messageTable.id, afterId)分批读取每批 500 条batchSize 500而非使用OFFSET。这避免了深分页在大表上的性能退化也保证了断点续跑时游标稳定。进度上报每个批次处理完后按sourceCount / preparedCount计算进度百分比并reportProgress第 262-263 行。唯一幂等插入批量插入走recordLegacyAggregatesTx内部使用onConflictDoNothing()AiUsageRecordService.ts依赖唯一requestId永不更新已存在的行——重跑迁移不会覆盖历史记录。测试 uses a stable request id and never updates an existing legacy row on rerun测试第 183-217 行验证源消息 stats 被改成totalTokens: 999后重跑表里仍只有一条totalTokens: 5, requestCount: 1的旧记录。失败批次回退重试如果整批事务抛出异常先记录 warning然后逐行重试第 235-258 行单行仍失败才跳过并累计skippedCount。这样一条畸形源数据行不会中止整个用户数据迁移。状态持久化请求次数估算存放在迁移后的MessageStats中execute 从源表读取因此恢复的迁移不依赖内存交接——即使进程中断重启后 keyset 游标与源数据仍在可以继续推进。外键完整性执行完成后调用assertOwnedForeignKeys(ctx.db, [aiUsageRecordTable])第 268 行。由于迁移期间foreign_keys OFF见 BaseMigrator.ts 的说明该调用用PRAGMA foreign_key_check针对本迁移器拥有的表做定向核查尽早暴露引用错误。validate()计数与完整性契约validate第 273-297 行统计目标表中recordKind legacy-aggregate的记录数targetCount期望值 sourceCount - skippedCount若targetCount expectedCount则成功否则返回带expected/actual/message的错误项ai-usage-record.count同时输出statssourceCount / targetCount / skippedCount与diagnosticsinsertedCount便于审计。这构成了文档所述标准 owned table 完整性契约目标记录数不得低于可迁移的带用量消息数。幂等键与数据不可变性设计requestId legacy:message-kind:message-id是整个迁移器可靠性的基石幂等同一条消息无论执行多少次生成的requestId都相同配合ON CONFLICT DO NOTHING保证重复执行安全可追溯messageKindmessageId直接编码在键中可以精确反查来源消息不可变schema 注释与 CHECK 约束确保 legacy-aggregate 记录写入后不被修改历史归属永远定格在迁移时刻。同时schema 的ai_usage_record_cost_tuple_checkaiUsageRecord.ts强制 cost 四元组要么全空、要么全有杜绝半成品成本数据ai_usage_record_nonnegative_check与ai_usage_record_integer_check第 185-218 行则从数据库层面兜底非负性与整数类型legacyToRow侧的requiredCount/requiredAmount/optionalCount校验AiUsageRecordService.ts在写入前就把非法值挡在门外。迁移结果如何被消费迁移写入的 legacy-aggregate 记录与运行时 invocation 记录共存于ai_usage_record表统一由 AiUsageRecordService.ts 提供查询能力list()按createdAt/totalTokens/cost/timeFirstTokenMs/tokensPerSecond排序的 keyset 分页列表stats()按 provider / model / source / apiKey 分组的聚合指标其中estimatedRequestCount专门用recordKind legacy-aggregate的条件区分历史估算与真实调用timeline()按日聚合的时间线同样区分estimatedRequestCount与unpricedRequestCount。也就是说历史迁移数据与实时调用数据在统计口径上同表同构但分型标记真实的逐次调用invocation是精确事实历史聚合legacy-aggregate是尽力而为的估算报表层通过recordKind明确区分二者不会把估算当成精确调用数。关键源码路径速查迁移器实现AiUsageRecordMigrator.ts迁移器文档README-AiUsageRecordMigrator.md目标表 schemaaiUsageRecord.ts领域类型与枚举types/aiUsageRecord.ts写入与查询服务AiUsageRecordService.ts迁移基类prepare/execute/validate 契约BaseMigrator.ts上游数据源chat 消息迁移README-ChatMigrator.md测试验证AiUsageRecordMigrator.test.ts小结AiUsageRecordMigrator的设计哲学可以概括为三句话回放而非重算不读当前配置、不重算历史成本、快照而非引用身份全部来自不可变快照刻意无外键、估算而非断言requestCount是尽力而为的聚合估算用recordKind与真实调用区分。配合稳定幂等键、keyset 分批、失败降级逐行重试与三阶段校验它确保了大规模历史用量数据能够安全、可恢复、可验证地进入 v2 的 AI 用量分析体系。【免费下载链接】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),仅供参考