迁移机制
Claude Code 如何跨版本升级用户配置和模型设置 - 不会破坏任何内容或让用户感到惊讶。
每次启动 Claude Code 时,它可能会默默地修复过时的设置、重新映射已弃用的模型别名、在文件之间移动字段或重新显示 UI 对话框 - 所有这些都在您看到第一个提示之前进行。 这是 迁移系统:一组小型但精确设计的一次性函数,在内部运行 runMigrations() in main.tsx.
src/migrations/*.ts ·
src/main.tsx (第 323 课-352 行)·
src/utils/config.ts ·
src/utils/releaseNotes.ts
Claude Code 中的迁移是 不是数据库架构迁移。 没有迁移表,没有回滚,也没有运行器框架。 相反,每个迁移函数在设计上都是幂等的——它会检测自己的前置/后置条件,如果工作已经完成,则立即退出。 整个集合在每次启动时运行,并受到单个版本号的保护,一旦应用了所有迁移,该版本号就会短路整个块。
设置促销活动
将字段从 ~/.claude.json (全局配置)进入 settings.json
模型别名升级
将过时或已删除的模型字符串重新映射到当前别名 userSettings
配置键重命名
重命名泄漏到公共配置中的实现细节密钥
一键重置
当用户体验发生变化且用户需要第二次选择机会时,清除标志以重新显示对话框
异步文件迁移
将配置数据移至单独的文件(更改日志缓存)而不阻塞 UI
runMigrations()
所有同步迁移都包含在定义于的单个函数中 main.tsx。 在指挥官期间被称为 preAction hook — 加载配置后,REPL 启动之前。
// main.tsx — line 323
// @[MODEL LAUNCH]: Consider any migrations you may need for model strings.
// See migrateSonnet1mToSonnet45.ts for an example.
// Bump this when adding a new sync migration so existing users re-run the set.
const CURRENT_MIGRATION_VERSION = 11;
function runMigrations(): void {
if (getGlobalConfig().migrationVersion !== CURRENT_MIGRATION_VERSION) {
migrateAutoUpdatesToSettings();
migrateBypassPermissionsAcceptedToSettings();
migrateEnableAllProjectMcpServersToSettings();
resetProToOpusDefault();
migrateSonnet1mToSonnet45();
migrateLegacyOpusToCurrent();
migrateSonnet45ToSonnet46();
migrateOpusToOpus1m();
migrateReplBridgeEnabledToRemoteControlAtStartup();
if (feature('TRANSCRIPT_CLASSIFIER')) {
resetAutoModeOptInForDefaultOffer();
}
if ("external" === 'ant') { // internal Anthropic builds only
migrateFennecToOpus();
}
// Stamp the version so we skip next time
saveGlobalConfig(prev => prev.migrationVersion === CURRENT_MIGRATION_VERSION
? prev
: { ...prev, migrationVersion: CURRENT_MIGRATION_VERSION });
}
// Async migration — fire-and-forget, non-blocking
migrateChangelogFromConfig().catch(() => {});
}
这里有三个突出的设计决策:
- 版本门:
migrationVersion存储在~/.claude.json。 一旦等于CURRENT_MIGRATION_VERSION,整个同步块被跳过——避免 11 个冗余saveGlobalConfig每次启动时都会进行锁定+重新读取循环。 - 版本碰撞规则: 该评论明确指出——每当添加新的同步迁移时都会改变常量,以便现有用户重新运行全套。
- 异步分离:
migrateChangelogFromConfig是异步的并且涉及文件 I/O,因此它在同步块之后运行即发即弃。
runMigrations() 在指挥官中被称为 preAction 里面有钩子 main.tsx,直接在 init() 就在之前 loadRemoteManagedSettings()。 一个 profileCheckpoint('preAction_after_migrations') 调用立即跟随它以进行延迟跟踪。
目前有 11 个同步迁移函数和 1 个异步迁移函数。 这是每个文件的用途、幂等性机制以及它涉及的配置存储。
| File | Category | 它的作用 | 幂等性守卫 |
|---|---|---|---|
migrateAutoUpdatesToSettings.ts |
Settings | 移动用户禁用的 autoUpdates 标志来自 GlobalConfig into userSettings.env.DISABLE_AUTOUPDATER = "1"。 还设置 process.env 立即生效,无需重新启动即可生效。 |
跳过如果 globalConfig.autoUpdates !== false 或者如果该标志是由本机保护设置的。 成功后从 GlobalConfig 中删除该字段。 |
migrateBypassPermissionsAcceptedToSettings.ts |
Settings | Moves bypassPermissionsModeAccepted 从 GlobalConfig 进入 userSettings.skipDangerousModePermissionPrompt。 旧名称将实现细节泄露到面向用户的配置文件中。 |
跳过如果 bypassPermissionsModeAccepted 不在 GlobalConfig 中。 支票 hasSkipDangerousModePermissionPrompt() 在写入之前避免覆盖现有值。 |
migrateEnableAllProjectMcpServersToSettings.ts |
Settings | 移动三个 MCP 服务器批准字段(enableAllProjectMcpServers, enabledMcpjsonServers, disabledMcpjsonServers)从项目配置到 localSettings。 将数组字段与重复数据删除合并以避免丢失现有数据。 |
如果项目配置中不存在这三个字段,则跳过。 为了 enableAllProjectMcpServers,在写入之前检查目标设置是否已设置。 |
resetProToOpusDefault.ts |
Reset | 记录时间戳(opusProMigrationTimestamp)适用于没有设置自定义模型的第一方 Pro 订阅者,因此 UI 可以显示一次性通知,表明 Opus 4.5 现在是他们的默认设置。 |
完成标志: globalConfig.opusProMigrationComplete。 立即为非专业版或非firstParty 用户标记为完成。 |
migrateSonnet1mToSonnet45.ts |
Model | 固定拥有以下权限的用户 sonnet[1m] 保存在 userSettings 到明确的 sonnet-4-5-20250929[1m] 细绳。 之所以需要,是因为 Sonnet 4.6 1M 提供给与 Sonnet 4.5 1M 不同的用户组。 如果已设置,还会更新内存中主循环模型覆盖。 |
完成标志: globalConfig.sonnet1m45MigrationComplete. |
migrateLegacyOpusToCurrent.ts |
Model | 重写显式 Opus 4.0/4.1 模型字符串(claude-opus-4-20250514, claude-opus-4-1-20250805等)到 opus 别名在 userSettings。 还记录 legacyOpusMigrationTimestamp 这样 UI 就可以显示一次性通知。 仅针对启用了旧版重映射的第一方用户运行。 |
读取和写入相同的源(userSettings),使其成为自幂等 - 迁移后模型字符串不再匹配,因此它会提前退出。 |
migrateSonnet45ToSonnet46.ts |
Model | 将固定到 Sonnet 4.5 显式字符串的 Pro/Max/Team Premium 用户升级回 sonnet (or sonnet[1m]) 别名,现在解析为 4.6。 跳过全新用户(numStartups <= 1)以避免向从未使用过 4.5 的人显示通知。 |
自幂等:仅在以下情况下写入 userSettings.model 匹配 Sonnet 4.5 字符串。 Gate:仅限第一方 + Pro/Max/TeamPremium。 |
migrateOpusToOpus1m.ts |
Model | 对于第一方的 Max/Team Premium 订阅者,升级 userSettings.model = 'opus' to 'opus[1m]' 当启用 Opus 1M 合并时。 如果 opus[1m] 解析为与默认模型相同的模型,它会清除该字段(没有不必要的引脚)。 |
自幂等:仅当模型完全正确时才写入 'opus'。 门: isOpus1mMergeEnabled(). |
migrateReplBridgeEnabledToRemoteControlAtStartup.ts |
Config | 重命名内部配置键 replBridgeEnabled to remoteControlAtStartup。 旧名称将实现细节泄露到公共配置文件中。 使用无类型强制转换来访问不再属于 TypeScript 类型的键。 |
跳过如果 replBridgeEnabled 不存在,或者如果 remoteControlAtStartup 已设置。 |
resetAutoModeOptInForDefaultOffer.ts |
Reset | Clears skipAutoPermissionPrompt 适用于接受旧的 2 选项自动模式对话框但从未将自动设置为默认模式的用户。 这会重新显示对话框,以便他们看到新的“使其成为我的默认值”选项。 仅当以下情况时才会触发 TRANSCRIPT_CLASSIFIER 功能标志处于活动状态。 |
完成标志: globalConfig.hasResetAutoModeOptInForDefaultOffer。 也被守护着 getAutoModeEnabledState() === 'enabled'. |
migrateFennecToOpus.ts |
Model | 仅限内部 Anthropic(USER_TYPE === 'ant')。 迁移删除了“fennec”模型别名(fennec-latest, fennec-latest[1m], fennec-fast-latest, opus-4-5-fast)到其 Opus 等效项,包括快速模式。 |
自幂等:仅当模型以 fennec 前缀开头时才起作用。 只写 userSettings. |
releaseNotes.ts: migrateChangelogFromConfig() |
配置(异步) | 将缓存的变更日志字符串从 globalConfig.cachedChangelog 到磁盘上的一个单独的文件中。 异步运行,因此不会阻塞 UI。 用途 wx write 标志,因此它仅在文件不存在时创建该文件。 |
跳过如果 globalConfig.cachedChangelog 未设置。 文件写入用途 flag: 'wx' (仅创建)以避免破坏现有数据。 |
每次迁移都必须能够安全地多次调用。 代码库中出现了两种模式:
模式 A — GlobalConfig 中的完成标志
当迁移需要运行一次但“已完成”状态从数据中不是不言而喻时使用(例如 resetProToOpusDefault, migrateSonnet1mToSonnet45)。 布尔值或时间戳被写入 ~/.claude.json 并在函数顶部进行检查。
// migrateSonnet1mToSonnet45.ts — completion flag pattern
export function migrateSonnet1mToSonnet45(): void {
const config = getGlobalConfig()
if (config.sonnet1m45MigrationComplete) {
return // already done — exit immediately
}
const model = getSettingsForSource('userSettings')?.model
if (model === 'sonnet[1m]') {
updateSettingsForSource('userSettings', {
model: 'sonnet-4-5-20250929[1m]',
})
}
saveGlobalConfig(current => ({
...current,
sonnet1m45MigrationComplete: true,
}))
}
模式 B——自幂等(数据不言自明)
当迁移条件是对当前值的直接检查时使用 - 如果数据已经迁移,则检查简单地返回 false 并且不写入任何内容(例如所有模型别名迁移)。 读取和写入相同的设置源(userSettings) 是这种模式的关键 - 中的注释 migrateLegacyOpusToCurrent.ts explains:
“读取和写入相同的源可以在没有完成标志的情况下保持幂等性,并且避免为仅将其固定在一个项目中的用户默默地将‘opus’提升为全局默认值。”
// migrateLegacyOpusToCurrent.ts — self-idempotent pattern
export function migrateLegacyOpusToCurrent(): void {
if (getAPIProvider() !== 'firstParty') return
if (!isLegacyModelRemapEnabled()) return
const model = getSettingsForSource('userSettings')?.model
if (
model !== 'claude-opus-4-20250514' &&
model !== 'claude-opus-4-1-20250805' &&
model !== 'claude-opus-4-0' &&
model !== 'claude-opus-4-1'
) {
return // data already clean — nothing to do
}
updateSettingsForSource('userSettings', { model: 'opus' })
saveGlobalConfig(current => ({
...current,
legacyOpusMigrationTimestamp: Date.now(),
}))
logEvent('tengu_legacy_opus_migration', { from_model: model })
}
Claude Code 具有多个按优先级顺序合并的设置源:
userSettings → projectSettings → localSettings → policySettings。 迁移的范围被故意限制为仅接触 userSettings (并且偶尔 localSettings)。 几乎每个模型迁移文件中的注释都重复相同的理由:
parseUserSpecifiedModel。 读取和写入相同的源可以在没有完成标志的情况下保持幂等性,并避免为仅将其固定在一个项目中的用户默默地将“opus”提升为全局默认值。”
此约束可以防止一类微妙的错误:如果迁移从 merged
设置,它可能会看到项目范围的设置并“有帮助地”将该值写入全局 userSettings,突然使每个项目的首选项成为所有地方的新默认值。
除了启动迁移功能之外, config.ts 包含一个较低级别的
migrateConfigFields() 每次配置文件运行时运行 从磁盘读取。 这处理最旧的架构更改——在当前版本化迁移系统存在之前。
// config.ts — runs on every config read
function migrateConfigFields(config: GlobalConfig): GlobalConfig {
if (config.installMethod !== undefined) {
return config // already migrated
}
// autoUpdaterStatus is removed from the type but may exist in old configs
const legacy = config as GlobalConfig & {
autoUpdaterStatus?: 'migrated' | 'installed' | 'disabled' | 'enabled' | ...
}
switch (legacy.autoUpdaterStatus) {
case 'migrated': installMethod = 'local'; break
case 'installed': installMethod = 'native'; break
case 'disabled': autoUpdates = false; break
...
}
return { ...config, installMethod, autoUpdates }
}
还有一个 removeProjectHistory() 剥离旧内联的函数
history 每次读取时项目配置中的字段 - 该字段已迁移到单独的 history.jsonl 文件,但旧的配置仍然包含该字段。
migrateConfigFields) 用于非常旧的架构更改,其中旧字段名称不再存在于 TypeScript 类型中 - 需要非类型化强制转换才能访问它。 新的迁移进入 migrations/ 目录并通过以下方式运行 runMigrations().
迁移是可观察到的。 大多数函数调用 logEvent() 记录 Anthropic 的遥测管道发生的情况。 这可以让团队知道何时仍在将迁移应用于野外用户以及何时可以安全地删除迁移代码。
| 活动名称 | Migration |
|---|---|
tengu_migrate_autoupdates_to_settings | migrateAutoUpdatesToSettings |
tengu_migrate_autoupdates_error | migrateAutoUpdatesToSettings(错误路径) |
tengu_migrate_bypass_permissions_accepted | migrateBypassPermissionsAcceptedToSettings |
tengu_migrate_mcp_approval_fields_success | migrateEnableAllProjectMcpServersToSettings |
tengu_migrate_mcp_approval_fields_error | migrateEnableAllProjectMcpServersToSettings(错误路径) |
tengu_reset_pro_to_opus_default | resetProToOpusDefault |
tengu_legacy_opus_migration | migrateLegacyOpusToCurrent |
tengu_sonnet45_to_46_migration | migrateSonnet45ToSonnet46 |
tengu_opus_to_opus1m_migration | migrateOpusToOpus1m |
tengu_migrate_reset_auto_opt_in_for_default_offer | resetAutoModeOptInForDefaultOffer |
尤其, migrateSonnet1mToSonnet45 and migrateReplBridgeEnabledToRemoteControlAtStartup
不发出分析事件——它们被认为是风险较低的内务处理。
源代码甚至包含一条注释,为未来的工程师指明了正确的方向:
// @[MODEL LAUNCH]: Consider any migrations you may need for model strings. See migrateSonnet1mToSonnet45.ts for an example.
现有代码库的配方:
- Create
src/migrations/myNewMigration.ts具有单个导出函数。 - 选择幂等策略:GlobalConfig 中的完成标志,或自幂等数据检查。
- 只能读取和写入
userSettings(orlocalSettings对于项目范围的数据)。 切勿读取合并的设置。 - Call
logEvent('tengu_my_migration_name', {...})与相关元数据。 - 将主体包裹在 try/catch 中并调用
logError需要注意的是——迁移绝不能抛出或破坏启动。 - 将函数导入到
main.tsx并将其添加到if (migrationVersion !== CURRENT_MIGRATION_VERSION)block. - Bump
CURRENT_MIGRATION_VERSION因此现有用户重新运行更新后的设置。
// Template: src/migrations/myNewMigration.ts
import { logEvent } from '../services/analytics/index.js'
import { logError } from '../utils/log.js'
import {
getSettingsForSource,
updateSettingsForSource,
} from '../utils/settings/settings.js'
export function myNewMigration(): void {
// self-idempotent guard: check if work is already done
const model = getSettingsForSource('userSettings')?.model
if (model !== 'old-alias') return
try {
updateSettingsForSource('userSettings', { model: 'new-alias' })
logEvent('tengu_my_new_migration', {})
} catch (error) {
logError(new Error(`Failed to run myNewMigration: ${error}`))
}
}
要点
runMigrations()inmain.tsx是单一入口点——由以下函数保护的函数调用的平面列表CURRENT_MIGRATION_VERSION = 11.- 版本门在
globalConfig.migrationVersion应用所有迁移后,可防止每次启动时重新运行 11 配置保存。 - 两种幂等策略: 完成标志 在 GlobalConfig 中用于不言而喻的一次性操作,以及 自幂等数据检查 对于数据不言自明的情况。
- 所有模型迁移仅触及
userSettings(从不合并设置)以避免意外全球化项目范围的模型首选项。 - 迁移绝不能抛出错误——错误会被捕获、记录并默默地吞掉,以避免破坏启动。
- 大多数迁移调用
logEvent()因此 Anthropic 可以跟踪旧数据形状何时从已安装的基础上完全消失。 - 添加迁移需要碰撞
CURRENT_MIGRATION_VERSION因此,已经通过该关卡的现有用户将重新运行更新后的集合。 - 异步迁移(文件 I/O 类似
migrateChangelogFromConfig)在同步块之后即发即弃。
知识检查
getGlobalConfig().migrationVersion === CURRENT_MIGRATION_VERSION?userSettings 直接而不是来自合并的设置?migrateAutoUpdatesToSettings calls process.env.DISABLE_AUTOUPDATER = '1' 写信给之后 userSettings。 为什么?