diff --git a/src/lib/api/config.ts b/src/lib/api/config.ts index 2b7820670..1262014ca 100644 --- a/src/lib/api/config.ts +++ b/src/lib/api/config.ts @@ -66,27 +66,6 @@ const pendingSyncParams: Record< const SYNC_DEBOUNCE_MS = 500; -/** - * 获取 Claude 通用配置片段(已废弃,使用 getCommonConfigSnippet) - * @returns 通用配置片段(JSON 字符串),如果不存在则返回 null - * @deprecated 使用 getCommonConfigSnippet('claude') 替代 - */ -export async function getClaudeCommonConfigSnippet(): Promise { - return invoke("get_claude_common_config_snippet"); -} - -/** - * 设置 Claude 通用配置片段(已废弃,使用 setCommonConfigSnippet) - * @param snippet - 通用配置片段(JSON 字符串) - * @throws 如果 JSON 格式无效 - * @deprecated 使用 setCommonConfigSnippet('claude', snippet) 替代 - */ -export async function setClaudeCommonConfigSnippet( - snippet: string, -): Promise { - return invoke("set_claude_common_config_snippet", { snippet }); -} - /** * 获取通用配置片段(统一接口) * @param appType - 应用类型(claude/codex/gemini) @@ -453,7 +432,6 @@ function getSettingsConfigString( case "gemini": // Gemini: settingsConfig 是包含 env 字段的 JSON 对象 - // replaceGeminiCommonConfigSnippet 期望接收完整的 settingsConfig JSON return typeof config === "string" ? config : JSON.stringify(config); default: diff --git a/src/utils/providerConfigUtils.ts b/src/utils/providerConfigUtils.ts index 8a643b521..9274d6c05 100644 --- a/src/utils/providerConfigUtils.ts +++ b/src/utils/providerConfigUtils.ts @@ -2,7 +2,7 @@ import type { TemplateValueConfig } from "../config/claudeProviderPresets"; import { normalizeQuotes } from "@/utils/textNormalization"; -import { isPlainObject, deepClone, deepMerge } from "@/utils/configMerge"; +import { isPlainObject } from "@/utils/configMerge"; // Gemini 通用配置禁止的键(共享常量,供 hook 和同步逻辑复用) export const GEMINI_COMMON_ENV_FORBIDDEN_KEYS = [ @@ -12,26 +12,6 @@ export const GEMINI_COMMON_ENV_FORBIDDEN_KEYS = [ export type GeminiForbiddenEnvKey = (typeof GEMINI_COMMON_ENV_FORBIDDEN_KEYS)[number]; -const deepRemove = ( - target: Record, - source: Record, -) => { - Object.entries(source).forEach(([key, value]) => { - if (!(key in target)) return; - - if (isPlainObject(value) && isPlainObject(target[key])) { - // 只移除完全匹配的嵌套属性 - deepRemove(target[key], value); - if (Object.keys(target[key]).length === 0) { - delete target[key]; - } - } else if (isSubset(target[key], value)) { - // 只有当值完全匹配时才删除 - delete target[key]; - } - }); -}; - const isSubset = (target: any, source: any): boolean => { if (isPlainObject(source)) { if (!isPlainObject(target)) return false; @@ -48,11 +28,6 @@ const isSubset = (target: any, source: any): boolean => { return target === source; }; -export interface UpdateCommonConfigResult { - updatedConfig: string; - error?: string; -} - // 验证JSON配置格式 export const validateJsonConfig = ( value: string, @@ -72,53 +47,6 @@ export const validateJsonConfig = ( } }; -// 将通用配置片段写入/移除 settingsConfig -export const updateCommonConfigSnippet = ( - jsonString: string, - snippetString: string, - enabled: boolean, -): UpdateCommonConfigResult => { - let config: Record; - try { - config = jsonString ? JSON.parse(jsonString) : {}; - } catch (err) { - return { - updatedConfig: jsonString, - error: "配置 JSON 解析失败,无法写入通用配置", - }; - } - - if (!snippetString.trim()) { - return { - updatedConfig: JSON.stringify(config, null, 2), - }; - } - - // 使用统一的验证函数 - const snippetError = validateJsonConfig(snippetString, "通用配置片段"); - if (snippetError) { - return { - updatedConfig: JSON.stringify(config, null, 2), - error: snippetError, - }; - } - - const snippet = JSON.parse(snippetString) as Record; - - if (enabled) { - const merged = deepMerge(deepClone(config), snippet); - return { - updatedConfig: JSON.stringify(merged, null, 2), - }; - } - - const cloned = deepClone(config); - deepRemove(cloned, snippet); - return { - updatedConfig: JSON.stringify(cloned, null, 2), - }; -}; - // 检查当前配置是否已包含通用配置片段 export const hasCommonConfigSnippet = ( jsonString: string, @@ -180,122 +108,6 @@ export const hasGeminiCommonConfigSnippet = ( } }; -/** - * 替换通用配置片段(用于同步更新) - * 先移除旧的通用配置,再添加新的通用配置 - */ -export const replaceCommonConfigSnippet = ( - jsonString: string, - oldSnippet: string, - newSnippet: string, -): UpdateCommonConfigResult => { - // 先移除旧的通用配置 - const removeResult = updateCommonConfigSnippet(jsonString, oldSnippet, false); - if (removeResult.error) { - return removeResult; - } - - // 再添加新的通用配置 - return updateCommonConfigSnippet( - removeResult.updatedConfig, - newSnippet, - true, - ); -}; - -/** - * 替换 Gemini 通用配置片段(用于同步更新) - * Gemini 的通用配置是 JSON 格式的 env 对象 - */ -export const replaceGeminiCommonConfigSnippet = ( - jsonString: string, - oldSnippet: string, - newSnippet: string, -): UpdateCommonConfigResult => { - try { - const config = jsonString ? JSON.parse(jsonString) : {}; - - // 校验 config 是对象类型 - if (!isPlainObject(config)) { - return { - updatedConfig: jsonString, - error: "CONFIG_NOT_OBJECT", - }; - } - - const env = config.env ?? {}; - - // 校验 env 是对象类型 - if (!isPlainObject(env)) { - return { - updatedConfig: jsonString, - error: "ENV_NOT_OBJECT", - }; - } - - // 解析旧的通用配置 - let oldEnv: Record = {}; - if (oldSnippet.trim()) { - try { - const parsed = JSON.parse(oldSnippet); - if (isPlainObject(parsed)) { - oldEnv = parsed as Record; - } - } catch { - // ignore parse error - } - } - - // 解析新的通用配置 - let newEnv: Record = {}; - if (newSnippet.trim()) { - try { - const parsed = JSON.parse(newSnippet); - if (isPlainObject(parsed)) { - // 过滤掉禁止的键 - for (const [key, value] of Object.entries(parsed)) { - if ( - typeof value === "string" && - !GEMINI_COMMON_ENV_FORBIDDEN_KEYS.includes( - key as GeminiForbiddenEnvKey, - ) - ) { - newEnv[key] = value; - } - } - } - } catch { - return { - updatedConfig: jsonString, - error: "COMMON_CONFIG_JSON_INVALID", - }; - } - } - - // 移除旧的通用配置键值对 - const updatedEnv = { ...env }; - for (const [key, value] of Object.entries(oldEnv)) { - if (updatedEnv[key] === value) { - delete updatedEnv[key]; - } - } - - // 添加新的通用配置键值对 - for (const [key, value] of Object.entries(newEnv)) { - updatedEnv[key] = value; - } - - return { - updatedConfig: JSON.stringify({ ...config, env: updatedEnv }, null, 2), - }; - } catch (err) { - return { - updatedConfig: jsonString, - error: "CONFIG_JSON_PARSE_FAILED", - }; - } -}; - // 读取配置中的 API Key(支持 Claude, Codex, Gemini) export const getApiKeyFromConfig = ( jsonString: string, @@ -460,389 +272,6 @@ export const setApiKeyInConfig = ( } }; -// ========== TOML Config Utilities ========== - -export interface UpdateTomlCommonConfigResult { - updatedConfig: string; - error?: string; -} - -const TOML_COMMON_CONFIG_START = "# cc-switch:common-config:start"; -const TOML_COMMON_CONFIG_END = "# cc-switch:common-config:end"; - -/** - * 移除旧版标记块(兼容性清理) - * 仅用于清理历史遗留的标记,新版本不再写入标记 - */ -const stripTomlCommonConfigBlock = (tomlString: string): string => { - const startIndex = tomlString.indexOf(TOML_COMMON_CONFIG_START); - const endIndex = tomlString.indexOf(TOML_COMMON_CONFIG_END); - if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) { - return tomlString; - } - const before = tomlString.slice(0, startIndex); - const after = tomlString.slice(endIndex + TOML_COMMON_CONFIG_END.length); - return `${before}${after}`; -}; - -/** - * 解析 TOML 文本为段落结构 - * 返回 { topLevel: string[], sections: Map } - * - topLevel: 顶级键值对行(在任何 [section] 之前) - * - sections: 每个 [section] 及其内容行 - */ -interface TomlParsedStructure { - topLevel: string[]; - sections: Map; - sectionOrder: string[]; -} - -function parseTomlStructure(tomlText: string): TomlParsedStructure { - const lines = tomlText.split("\n"); - const topLevel: string[] = []; - const sections = new Map(); - const sectionOrder: string[] = []; - - let currentSection: string | null = null; - - for (const line of lines) { - const trimmed = line.trim(); - - // 检查是否是 section 头(如 [mcp_servers.mcpServers]) - const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/); - if (sectionMatch) { - currentSection = sectionMatch[1]; - if (!sections.has(currentSection)) { - sections.set(currentSection, []); - sectionOrder.push(currentSection); - } - continue; - } - - if (currentSection === null) { - // 顶级内容 - topLevel.push(line); - } else { - // section 内容 - const sectionLines = sections.get(currentSection)!; - sectionLines.push(line); - } - } - - return { topLevel, sections, sectionOrder }; -} - -/** - * 从顶级行中提取键名 - */ -function extractKeyFromLine(line: string): string | null { - const trimmed = line.trim(); - // 跳过空行和注释 - if (!trimmed || trimmed.startsWith("#")) { - return null; - } - // 匹配 key = value 格式 - const match = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*=/); - return match ? match[1] : null; -} - -/** - * 智能合并 TOML 配置 - * - 顶级键值对:snippet 中的键会覆盖或添加到 base 的顶级区域 - * - sections:同名 section 会合并内容,不同 section 会追加 - */ -function mergeTomlConfigs(baseText: string, snippetText: string): string { - const base = parseTomlStructure(baseText); - const snippet = parseTomlStructure(snippetText); - - // 1. 合并顶级键值对 - // 提取 snippet 中的顶级键 - const snippetTopLevelKeys = new Set(); - for (const line of snippet.topLevel) { - const key = extractKeyFromLine(line); - if (key) { - snippetTopLevelKeys.add(key); - } - } - - // 过滤 base 中被 snippet 覆盖的键 - const filteredBaseTopLevel = base.topLevel.filter((line) => { - const key = extractKeyFromLine(line); - return key === null || !snippetTopLevelKeys.has(key); - }); - - // 合并顶级内容:base 的非覆盖行 + snippet 的顶级行 - const mergedTopLevel = [...filteredBaseTopLevel]; - // 添加 snippet 的顶级行(跳过空行,除非是有意义的) - for (const line of snippet.topLevel) { - const trimmed = line.trim(); - if (trimmed) { - mergedTopLevel.push(line); - } - } - - // 2. 合并 sections - const mergedSections = new Map(); - const mergedSectionOrder: string[] = []; - - // 先添加 base 的 sections - for (const sectionName of base.sectionOrder) { - mergedSections.set(sectionName, [...base.sections.get(sectionName)!]); - mergedSectionOrder.push(sectionName); - } - - // 合并 snippet 的 sections - for (const sectionName of snippet.sectionOrder) { - const snippetLines = snippet.sections.get(sectionName)!; - - if (mergedSections.has(sectionName)) { - // 同名 section:合并内容 - const existingLines = mergedSections.get(sectionName)!; - - // 提取 snippet section 中的键 - const snippetSectionKeys = new Set(); - for (const line of snippetLines) { - const key = extractKeyFromLine(line); - if (key) { - snippetSectionKeys.add(key); - } - } - - // 过滤 existing 中被覆盖的键 - const filteredExisting = existingLines.filter((line) => { - const key = extractKeyFromLine(line); - return key === null || !snippetSectionKeys.has(key); - }); - - // 合并:existing 的非覆盖行 + snippet 的行 - const merged = [...filteredExisting]; - for (const line of snippetLines) { - const trimmed = line.trim(); - if (trimmed) { - merged.push(line); - } - } - mergedSections.set(sectionName, merged); - } else { - // 新 section:直接添加 - mergedSections.set(sectionName, [...snippetLines]); - mergedSectionOrder.push(sectionName); - } - } - - // 3. 重建 TOML 文本 - const resultLines: string[] = []; - - // 添加顶级内容(清理多余空行) - let lastWasEmpty = true; - for (const line of mergedTopLevel) { - const isEmpty = !line.trim(); - if (isEmpty && lastWasEmpty) { - continue; // 跳过连续空行 - } - resultLines.push(line); - lastWasEmpty = isEmpty; - } - - // 添加 sections - for (const sectionName of mergedSectionOrder) { - const sectionLines = mergedSections.get(sectionName)!; - - // 确保 section 前有空行分隔 - if (resultLines.length > 0 && resultLines[resultLines.length - 1].trim()) { - resultLines.push(""); - } - - // 添加 section 头 - resultLines.push(`[${sectionName}]`); - - // 添加 section 内容(清理多余空行) - lastWasEmpty = false; - for (const line of sectionLines) { - const isEmpty = !line.trim(); - if (isEmpty && lastWasEmpty) { - continue; - } - resultLines.push(line); - lastWasEmpty = isEmpty; - } - } - - // 清理末尾空行,确保以单个换行结尾 - while ( - resultLines.length > 0 && - !resultLines[resultLines.length - 1].trim() - ) { - resultLines.pop(); - } - - return resultLines.join("\n") + "\n"; -} - -/** - * 从 TOML 配置中移除指定的片段内容 - * - 移除 snippet 中定义的顶级键 - * - 移除 snippet 中定义的 section 内的键(如果 section 变空则移除整个 section) - */ -function removeTomlSnippet(baseText: string, snippetText: string): string { - const base = parseTomlStructure(baseText); - const snippet = parseTomlStructure(snippetText); - - // 1. 从顶级移除 snippet 的键 - const snippetTopLevelKeys = new Set(); - for (const line of snippet.topLevel) { - const key = extractKeyFromLine(line); - if (key) { - snippetTopLevelKeys.add(key); - } - } - - const filteredTopLevel = base.topLevel.filter((line) => { - const key = extractKeyFromLine(line); - return key === null || !snippetTopLevelKeys.has(key); - }); - - // 2. 从 sections 移除 snippet 的键 - const filteredSections = new Map(); - const filteredSectionOrder: string[] = []; - - for (const sectionName of base.sectionOrder) { - const baseLines = base.sections.get(sectionName)!; - - // 检查 snippet 是否有这个 section - if (snippet.sections.has(sectionName)) { - const snippetLines = snippet.sections.get(sectionName)!; - - // 提取 snippet section 中的键 - const snippetSectionKeys = new Set(); - for (const line of snippetLines) { - const key = extractKeyFromLine(line); - if (key) { - snippetSectionKeys.add(key); - } - } - - // 过滤掉 snippet 中的键 - const filtered = baseLines.filter((line) => { - const key = extractKeyFromLine(line); - return key === null || !snippetSectionKeys.has(key); - }); - - // 检查过滤后是否还有实质内容 - const hasContent = filtered.some((line) => { - const trimmed = line.trim(); - return trimmed && !trimmed.startsWith("#"); - }); - - if (hasContent) { - filteredSections.set(sectionName, filtered); - filteredSectionOrder.push(sectionName); - } - // 如果没有实质内容,整个 section 被移除 - } else { - // snippet 中没有这个 section,保留 - filteredSections.set(sectionName, baseLines); - filteredSectionOrder.push(sectionName); - } - } - - // 3. 重建 TOML 文本 - const resultLines: string[] = []; - - // 添加顶级内容 - let lastWasEmpty = true; - for (const line of filteredTopLevel) { - const isEmpty = !line.trim(); - if (isEmpty && lastWasEmpty) { - continue; - } - resultLines.push(line); - lastWasEmpty = isEmpty; - } - - // 添加 sections - for (const sectionName of filteredSectionOrder) { - const sectionLines = filteredSections.get(sectionName)!; - - if (resultLines.length > 0 && resultLines[resultLines.length - 1].trim()) { - resultLines.push(""); - } - - resultLines.push(`[${sectionName}]`); - - lastWasEmpty = false; - for (const line of sectionLines) { - const isEmpty = !line.trim(); - if (isEmpty && lastWasEmpty) { - continue; - } - resultLines.push(line); - lastWasEmpty = isEmpty; - } - } - - while ( - resultLines.length > 0 && - !resultLines[resultLines.length - 1].trim() - ) { - resultLines.pop(); - } - - return resultLines.length > 0 ? resultLines.join("\n") + "\n" : ""; -} - -// 将通用配置片段写入/移除 TOML 配置 -// 使用智能合并,避免重复定义 section -export const updateTomlCommonConfigSnippet = ( - tomlString: string, - snippetString: string, - enabled: boolean, -): UpdateTomlCommonConfigResult => { - if (enabled) { - if (!snippetString.trim()) { - return { - updatedConfig: tomlString, - }; - } - - // 先清理旧版标记块(兼容性) - let baseConfig = stripTomlCommonConfigBlock(tomlString); - baseConfig = baseConfig.replace(/\n{3,}/g, "\n\n").trim(); - if (baseConfig) { - baseConfig += "\n"; - } - - // 使用智能合并 - const merged = mergeTomlConfigs(baseConfig, snippetString); - - return { - updatedConfig: merged, - }; - } else { - // 移除通用配置 - // 先清理旧版标记块(兼容性) - const strippedConfig = stripTomlCommonConfigBlock(tomlString); - if (strippedConfig !== tomlString) { - const cleaned = strippedConfig.replace(/\n{3,}/g, "\n\n").trim(); - return { - updatedConfig: cleaned ? cleaned + "\n" : "", - }; - } - - // 使用智能移除 - if (snippetString.trim()) { - const removed = removeTomlSnippet(tomlString, snippetString); - return { - updatedConfig: removed, - }; - } - - return { - updatedConfig: tomlString, - }; - } -}; - // 检查 TOML 配置是否已包含通用配置片段 export const hasTomlCommonConfigSnippet = ( tomlString: string, @@ -859,32 +288,6 @@ export const hasTomlCommonConfigSnippet = ( ); }; -/** - * 替换 TOML 通用配置片段(用于同步更新) - * 先移除旧的通用配置,再添加新的通用配置 - */ -export const replaceTomlCommonConfigSnippet = ( - tomlString: string, - oldSnippet: string, - newSnippet: string, -): UpdateTomlCommonConfigResult => { - // 先清理旧版标记块(兼容性) - let configWithoutOld = stripTomlCommonConfigBlock(tomlString); - - // 通过内容匹配移除旧片段 - if (oldSnippet.trim() && configWithoutOld.includes(oldSnippet.trim())) { - configWithoutOld = configWithoutOld.replace(oldSnippet.trim(), ""); - // 清理多余的空行 - configWithoutOld = configWithoutOld.replace(/\n{3,}/g, "\n\n").trim(); - if (configWithoutOld) { - configWithoutOld += "\n"; - } - } - - // 添加新的通用配置 - return updateTomlCommonConfigSnippet(configWithoutOld, newSnippet, true); -}; - // ========== Codex base_url utils ========== // 从 Codex 的 TOML 配置文本中提取 base_url(支持单/双引号)