first commit

This commit is contained in:
H
2026-04-03 13:01:19 +08:00
commit 538eced414
2575 changed files with 645911 additions and 0 deletions

View File

@@ -0,0 +1,118 @@
import { logEvent } from 'src/services/analytics/index.js'
import {
getCurrentProjectConfig,
saveCurrentProjectConfig,
} from '../utils/config.js'
import { logError } from '../utils/log.js'
import {
getSettingsForSource,
updateSettingsForSource,
} from '../utils/settings/settings.js'
/**
* 迁移:将 MCP 服务器批准字段从项目配置移到本地设置
* 这同时迁移 enableAllProjectMcpServers 和 enabledMcpjsonServers 到
* 设置系统以更好地管理和保持一致。
*/
export function migrateEnableAllProjectMcpServersToSettings(): void {
const projectConfig = getCurrentProjectConfig()
// 检查项目配置中是否存在任何字段
const hasEnableAll = projectConfig.enableAllProjectMcpServers !== undefined
const hasEnabledServers =
projectConfig.enabledMcpjsonServers &&
projectConfig.enabledMcpjsonServers.length > 0
const hasDisabledServers =
projectConfig.disabledMcpjsonServers &&
projectConfig.disabledMcpjsonServers.length > 0
if (!hasEnableAll && !hasEnabledServers && !hasDisabledServers) {
return
}
try {
const existingSettings = getSettingsForSource('localSettings') || {}
const updates: Partial<{
enableAllProjectMcpServers: boolean
enabledMcpjsonServers: string[]
disabledMcpjsonServers: string[]
}> = {}
const fieldsToRemove: Array<
| 'enableAllProjectMcpServers'
| 'enabledMcpjsonServers'
| 'disabledMcpjsonServers'
> = []
// 如果 enableAllProjectMcpServers 存在且尚未迁移,则迁移它
if (
hasEnableAll &&
existingSettings.enableAllProjectMcpServers === undefined
) {
updates.enableAllProjectMcpServers =
projectConfig.enableAllProjectMcpServers
fieldsToRemove.push('enableAllProjectMcpServers')
} else if (hasEnableAll) {
// 已经迁移,仅标记为移除
fieldsToRemove.push('enableAllProjectMcpServers')
}
// 如果 enabledMcpjsonServers 存在,则迁移它
if (hasEnabledServers && projectConfig.enabledMcpjsonServers) {
const existingEnabledServers =
existingSettings.enabledMcpjsonServers || []
// 合并服务器(避免重复)
updates.enabledMcpjsonServers = [
...new Set([
...existingEnabledServers,
...projectConfig.enabledMcpjsonServers,
]),
]
fieldsToRemove.push('enabledMcpjsonServers')
}
// 如果 disabledMcpjsonServers 存在,则迁移它
if (hasDisabledServers && projectConfig.disabledMcpjsonServers) {
const existingDisabledServers =
existingSettings.disabledMcpjsonServers || []
// 合并服务器(避免重复)
updates.disabledMcpjsonServers = [
...new Set([
...existingDisabledServers,
...projectConfig.disabledMcpjsonServers,
]),
]
fieldsToRemove.push('disabledMcpjsonServers')
}
// 如果有任何更新,则更新设置
if (Object.keys(updates).length > 0) {
updateSettingsForSource('localSettings', updates)
}
// 从项目配置中移除已迁移的字段
if (
fieldsToRemove.includes('enableAllProjectMcpServers') ||
fieldsToRemove.includes('enabledMcpjsonServers') ||
fieldsToRemove.includes('disabledMcpjsonServers')
) {
saveCurrentProjectConfig(current => {
const {
enableAllProjectMcpServers: _enableAll,
enabledMcpjsonServers: _enabledServers,
disabledMcpjsonServers: _disabledServers,
...configWithoutFields
} = current
return configWithoutFields
})
}
// 记录迁移事件
logEvent('tengu_migrate_mcp_approval_fields_success', {
migratedCount: fieldsToRemove.length,
})
} catch (e: unknown) {
// 记录迁移失败但不要抛出以避免破坏启动
logError(e)
logEvent('tengu_migrate_mcp_approval_fields_error', {})
}
}

View File

@@ -0,0 +1,40 @@
import { logEvent } from 'src/services/analytics/index.js'
import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js'
import { logError } from '../utils/log.js'
import {
hasSkipDangerousModePermissionPrompt,
updateSettingsForSource,
} from '../utils/settings/settings.js'
/**
* 迁移:将 bypassPermissionsModeAccepted 从全局配置移到 settings.json
* 作为 skipDangerousModePermissionPrompt。这是更好的归宿
* 因为 settings.json 是用户可配置的设置文件。
*/
export function migrateBypassPermissionsAcceptedToSettings(): void {
const globalConfig = getGlobalConfig()
if (!globalConfig.bypassPermissionsModeAccepted) {
return
}
try {
if (!hasSkipDangerousModePermissionPrompt()) {
updateSettingsForSource('userSettings', {
skipDangerousModePermissionPrompt: true,
})
}
logEvent('tengu_migrate_bypass_permissions_accepted', {})
saveGlobalConfig(current => {
if (!('bypassPermissionsModeAccepted' in current)) return current
const { bypassPermissionsModeAccepted: _, ...updatedConfig } = current
return updatedConfig
})
} catch (error) {
logError(
new Error(`Failed to migrate bypass permissions accepted: ${error}`),
)
}
}

View File

@@ -0,0 +1,45 @@
import {
getSettingsForSource,
updateSettingsForSource,
} from '../utils/settings/settings.js'
/**
* 将使用已删除的 fennec 模型别名的用户迁移到新的 Opus 4.6 别名。
* - fennec-latest → opus
* - fennec-latest[1m] → opus[1m]
* - fennec-fast-latest → opus[1m] + fast 模式
* - opus-4-5-fast → opus + fast 模式
*
* 仅触及 userSettings。读写相同源保持此操作
* 幂等而不需要完成标志。project/local/policy
* 设置中的 Fennec 别名保留不动 — 我们不能/不应该重写那些,
* 在这里读取合并设置会导致无限重新运行 + 静默全局提升。
*/
export function migrateFennecToOpus(): void {
if (process.env.USER_TYPE !== 'ant') {
return
}
const settings = getSettingsForSource('userSettings')
const model = settings?.model
if (typeof model === 'string') {
if (model.startsWith('fennec-latest[1m]')) {
updateSettingsForSource('userSettings', {
model: 'opus[1m]',
})
} else if (model.startsWith('fennec-latest')) {
updateSettingsForSource('userSettings', {
model: 'opus',
})
} else if (
model.startsWith('fennec-fast-latest') ||
model.startsWith('opus-4-5-fast')
) {
updateSettingsForSource('userSettings', {
model: 'opus[1m]',
fastMode: true,
})
}
}
}

View File

@@ -0,0 +1,56 @@
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from '../services/analytics/index.js'
import { saveGlobalConfig } from '../utils/config.js'
import { isLegacyModelRemapEnabled } from '../utils/model/model.js'
import { getAPIProvider } from '../utils/model/providers.js'
import {
getSettingsForSource,
updateSettingsForSource,
} from '../utils/settings/settings.js'
/**
* 将第一方用户从显式 Opus 4.0/4.1 模型字符串迁移出来。
*
* 'opus' 别名已经为 1P 解析为 Opus 4.6,所以任何仍在
* 使用显式 4.0/4.1 字符串的用户是在 4.5 发布之前在设置中固定它的。
* parseUserSpecifiedModel 现在无论如何都会在运行时静默重新映射这些 —
* 此迁移清理 settings.json 以便 /model 显示正确的内容,
* 并设置时间戳以便 REPL 可以显示一次性通知。
*
* 仅触及 userSettings。项目/本地/策略设置中的旧字符串
* 保持不变(我们不能/不应该重写那些),仍然在运行时由
* parseUserSpecifiedModel 重新映射。读写相同源
* 保持此操作的幂等性而不需要完成标志,并避免静默
* 将 'opus' 提升为仅在一个项目中固定它的用户的全局默认值。
*/
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
}
updateSettingsForSource('userSettings', { model: 'opus' })
saveGlobalConfig(current => ({
...current,
legacyOpusMigrationTimestamp: Date.now(),
}))
logEvent('tengu_legacy_opus_migration', {
from_model:
model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
}

View File

@@ -0,0 +1,43 @@
import { logEvent } from '../services/analytics/index.js'
import {
getDefaultMainLoopModelSetting,
isOpus1mMergeEnabled,
parseUserSpecifiedModel,
} from '../utils/model/model.js'
import {
getSettingsForSource,
updateSettingsForSource,
} from '../utils/settings/settings.js'
/**
* 当用户有资格获得合并的 Opus 1M 体验1P 上的 Max/Team Premium
* 将设置中固定了 'opus' 的用户迁移到 'opus[1m]'。
*
* 使用 --model opus 的 CLI 调用不受影响:该标志是运行时
* 覆盖,不触及 userSettings因此继续使用普通 Opus。
*
* Pro 订阅者被跳过 — 他们保留单独的 Opus 和 Opus 1M 选项。
* 3P 用户被跳过 — 他们的模型字符串是完整模型 ID不是别名。
*
* 幂等:仅在 userSettings.model 正好是 'opus' 时写入。
*/
export function migrateOpusToOpus1m(): void {
if (!isOpus1mMergeEnabled()) {
return
}
const model = getSettingsForSource('userSettings')?.model
if (model !== 'opus') {
return
}
const migrated = 'opus[1m]'
const modelToSet =
parseUserSpecifiedModel(migrated) ===
parseUserSpecifiedModel(getDefaultMainLoopModelSetting())
? undefined
: migrated
updateSettingsForSource('userSettings', { model: modelToSet })
logEvent('tengu_opus_to_opus1m_migration', {})
}

View File

@@ -0,0 +1,22 @@
import { saveGlobalConfig } from '../utils/config.js'
/**
* 将 `replBridgeEnabled` 配置键迁移到 `remoteControlAtStartup`。
*
* 旧键是泄漏到用户面向配置的实现细节。
* 此迁移将值复制到新键并删除旧键。
* 幂等 — 仅在旧键存在且新键不存在时执行。
*/
export function migrateReplBridgeEnabledToRemoteControlAtStartup(): void {
saveGlobalConfig(prev => {
// 旧键不再在 GlobalConfig 类型中,因此通过
// 无类型转换访问它。仅在旧键存在且新键
// 尚未设置时迁移。
const oldValue = (prev as Record<string, unknown>)['replBridgeEnabled']
if (oldValue === undefined) return prev
if (prev.remoteControlAtStartup !== undefined) return prev
const next = { ...prev, remoteControlAtStartup: Boolean(oldValue) }
delete (next as Record<string, unknown>)['replBridgeEnabled']
return next
})
}

View File

@@ -0,0 +1,48 @@
import {
getMainLoopModelOverride,
setMainLoopModelOverride,
} from '../bootstrap/state.js'
import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js'
import {
getSettingsForSource,
updateSettingsForSource,
} from '../utils/settings/settings.js'
/**
* 将保存了 "sonnet[1m]" 的用户迁移到显式 "sonnet-4-5-20250929[1m]"。
*
* "sonnet" 别名现在解析为 Sonnet 4.6,所以之前设置
* "sonnet[1m]"(以 Sonnet 4.5 1M 为目标)的用户需要固定到
* 显式版本以保留其intended model。
*
* 这是必需的,因为 Sonnet 4.6 1M 向不同组的用户提供了比
* Sonnet 4.5 1M所以我们需要将现有的 sonnet[1m] 用户固定到 Sonnet 4.5 1M。
*
* 专门从 userSettings 读取(不是合并设置)所以我们不会
* 将项目范围的 "sonnet[1m]" 提升为全局默认值。运行一次,
* 通过全局配置中的完成标志跟踪。
*/
export function migrateSonnet1mToSonnet45(): void {
const config = getGlobalConfig()
if (config.sonnet1m45MigrationComplete) {
return
}
const model = getSettingsForSource('userSettings')?.model
if (model === 'sonnet[1m]') {
updateSettingsForSource('userSettings', {
model: 'sonnet-4-5-20250929[1m]',
})
}
// 同样迁移已设置的内存覆盖
const override = getMainLoopModelOverride()
if (override === 'sonnet[1m]') {
setMainLoopModelOverride('sonnet-4-5-20250929[1m]')
}
saveGlobalConfig(current => ({
...current,
sonnet1m45MigrationComplete: true,
}))
}

View File

@@ -0,0 +1,67 @@
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from '../services/analytics/index.js'
import {
isMaxSubscriber,
isProSubscriber,
isTeamPremiumSubscriber,
} from '../utils/auth.js'
import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js'
import { getAPIProvider } from '../utils/model/providers.js'
import {
getSettingsForSource,
updateSettingsForSource,
} from '../utils/settings/settings.js'
/**
* 将 Pro/Max/Team Premium 第一方用户从显式 Sonnet 4.5
* 模型字符串迁移到 'sonnet' 别名(现在解析为 Sonnet 4.6)。
*
* 用户可能已通过以下方式固定到显式 Sonnet 4.5 字符串:
* - 较早的 migrateSonnet1mToSonnet45 迁移sonnet[1m] → 显式 4.5[1m]
* - 通过 /model 手动选择
*
* 专门读取 userSettings不是合并的所以我们只迁移 /model
* 写入的内容 — 项目/本地固定保留不动。
* 幂等:仅在 userSettings.model 匹配 Sonnet 4.5 字符串时写入。
*/
export function migrateSonnet45ToSonnet46(): void {
if (getAPIProvider() !== 'firstParty') {
return
}
if (!isProSubscriber() && !isMaxSubscriber() && !isTeamPremiumSubscriber()) {
return
}
const model = getSettingsForSource('userSettings')?.model
if (
model !== 'claude-sonnet-4-5-20250929' &&
model !== 'claude-sonnet-4-5-20250929[1m]' &&
model !== 'sonnet-4-5-20250929' &&
model !== 'sonnet-4-5-20250929[1m]'
) {
return
}
const has1m = model.endsWith('[1m]')
updateSettingsForSource('userSettings', {
model: has1m ? 'sonnet[1m]' : 'sonnet',
})
// 跳过新用户通知 — 他们从未体验过旧默认值
const config = getGlobalConfig()
if (config.numStartups > 1) {
saveGlobalConfig(current => ({
...current,
sonnet45To46MigrationTimestamp: Date.now(),
}))
}
logEvent('tengu_sonnet45_to_46_migration', {
from_model:
model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
has_1m: has1m,
})
}

View File

@@ -0,0 +1,51 @@
import { feature } from 'bun:bundle'
import { logEvent } from 'src/services/analytics/index.js'
import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js'
import { logError } from '../utils/log.js'
import { getAutoModeEnabledState } from '../utils/permissions/permissionSetup.js'
import {
getSettingsForSource,
updateSettingsForSource,
} from '../utils/settings/settings.js'
/**
* 一次性迁移:清除已接受旧 2 选项 AutoModeOptInDialog
* 但没有将 auto 作为默认值的用户的 skipAutoPermissionPrompt。
* 重新显示对话框,以便他们看到新的"使其成为我的默认模式"选项。
* 保护存在于 GlobalConfig~/.claude.json而不是 settings.json 中,
* 因此它在设置重置后保留并且不会重新武装自己。
*
* 仅在 tengu_auto_mode_config.enabled === 'enabled' 时运行。对于 'opt-in'
* 用户,清除 skipAutoPermissionPrompt 会从轮播中移除 auto
*permissionSetup.ts:988— 对话框将变得不可达并且
* 迁移会自我失败。实际上,约 40 个目标蚂蚁都是
* 'enabled'(他们通过 bare Shift+Tab 到达旧对话框,这需要
* 'enabled'),但保护使其无论如何都是安全的。
*/
export function resetAutoModeOptInForDefaultOffer(): void {
if (feature('TRANSCRIPT_CLASSIFIER')) {
const config = getGlobalConfig()
if (config.hasResetAutoModeOptInForDefaultOffer) return
if (getAutoModeEnabledState() !== 'enabled') return
try {
const user = getSettingsForSource('userSettings')
if (
user?.skipAutoPermissionPrompt &&
user?.permissions?.defaultMode !== 'auto'
) {
updateSettingsForSource('userSettings', {
skipAutoPermissionPrompt: undefined,
})
logEvent('tengu_migrate_reset_auto_opt_in_for_default_offer', {})
}
saveGlobalConfig(c => {
if (c.hasResetAutoModeOptInForDefaultOffer) return c
return { ...c, hasResetAutoModeOptInForDefaultOffer: true }
})
} catch (error) {
logError(new Error(`Failed to reset auto mode opt-in: ${error}`))
}
}
}

View File

@@ -0,0 +1,51 @@
import { logEvent } from 'src/services/analytics/index.js'
import { isProSubscriber } from '../utils/auth.js'
import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js'
import { getAPIProvider } from '../utils/model/providers.js'
import { getSettings_DEPRECATED } from '../utils/settings/settings.js'
export function resetProToOpusDefault(): void {
const config = getGlobalConfig()
if (config.opusProMigrationComplete) {
return
}
const apiProvider = getAPIProvider()
// 第一方的 Pro 用户自动迁移到 Opus 4.5 默认值
if (apiProvider !== 'firstParty' || !isProSubscriber()) {
saveGlobalConfig(current => ({
...current,
opusProMigrationComplete: true,
}))
logEvent('tengu_reset_pro_to_opus_default', { skipped: true })
return
}
const settings = getSettings_DEPRECATED()
// 仅在用户使用默认值时显示通知(没有自定义模型设置)
if (settings?.model === undefined) {
const opusProMigrationTimestamp = Date.now()
saveGlobalConfig(current => ({
...current,
opusProMigrationComplete: true,
opusProMigrationTimestamp,
}))
logEvent('tengu_reset_pro_to_opus_default', {
skipped: false,
had_custom_model: false,
})
} else {
// 用户有自定义模型设置,仅标记迁移完成
saveGlobalConfig(current => ({
...current,
opusProMigrationComplete: true,
}))
logEvent('tengu_reset_pro_to_opus_default', {
skipped: false,
had_custom_model: true,
})
}
}