// 供应商配置处理工具函数 import type { TemplateValueConfig } from "../config/claudeProviderPresets"; import { normalizeQuotes } from "@/utils/textNormalization"; const isPlainObject = (value: unknown): value is Record => { return Object.prototype.toString.call(value) === "[object Object]"; }; const deepMerge = ( target: Record, source: Record, ): Record => { Object.entries(source).forEach(([key, value]) => { if (isPlainObject(value)) { if (!isPlainObject(target[key])) { target[key] = {}; } deepMerge(target[key], value); } else { // 直接覆盖非对象字段(数组/基础类型) target[key] = value; } }); return target; }; 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; return Object.entries(source).every(([key, value]) => isSubset(target[key], value), ); } if (Array.isArray(source)) { if (!Array.isArray(target) || target.length !== source.length) return false; return source.every((item, index) => isSubset(target[index], item)); } return target === source; }; // 深拷贝函数 const deepClone = (obj: T): T => { if (obj === null || typeof obj !== "object") return obj; if (obj instanceof Date) return new Date(obj.getTime()) as T; if (obj instanceof Array) return obj.map((item) => deepClone(item)) as T; if (obj instanceof Object) { const clonedObj = {} as T; for (const key in obj) { if (obj.hasOwnProperty(key)) { clonedObj[key] = deepClone(obj[key]); } } return clonedObj; } return obj; }; export interface UpdateCommonConfigResult { updatedConfig: string; error?: string; } // 验证JSON配置格式 export const validateJsonConfig = ( value: string, fieldName: string = "配置", ): string => { if (!value.trim()) { return ""; } try { const parsed = JSON.parse(value); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { return `${fieldName}必须是 JSON 对象`; } return ""; } catch { return `${fieldName}JSON格式错误,请检查语法`; } }; // 将通用配置片段写入/移除 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, snippetString: string, ): boolean => { try { if (!snippetString.trim()) return false; const config = jsonString ? JSON.parse(jsonString) : {}; const snippet = JSON.parse(snippetString); if (!isPlainObject(snippet)) return false; return isSubset(config, snippet); } catch (err) { return false; } }; // 读取配置中的 API Key(支持 Claude, Codex, Gemini) export const getApiKeyFromConfig = ( jsonString: string, appType?: string, ): string => { try { const config = JSON.parse(jsonString); const env = config?.env; if (!env) return ""; // Gemini API Key if (appType === "gemini") { const geminiKey = env.GEMINI_API_KEY; return typeof geminiKey === "string" ? geminiKey : ""; } // Codex API Key if (appType === "codex") { const codexKey = env.CODEX_API_KEY; return typeof codexKey === "string" ? codexKey : ""; } // Claude API Key (优先 ANTHROPIC_AUTH_TOKEN,其次 ANTHROPIC_API_KEY) const token = env.ANTHROPIC_AUTH_TOKEN; const apiKey = env.ANTHROPIC_API_KEY; const value = typeof token === "string" ? token : typeof apiKey === "string" ? apiKey : ""; return value; } catch (err) { return ""; } }; // 模板变量替换 export const applyTemplateValues = ( config: any, templateValues: Record | undefined, ): any => { const resolvedValues = Object.fromEntries( Object.entries(templateValues ?? {}).map(([key, value]) => { const resolvedValue = value.editorValue !== undefined ? value.editorValue : (value.defaultValue ?? ""); return [key, resolvedValue]; }), ); const replaceInString = (str: string): string => { return Object.entries(resolvedValues).reduce((acc, [key, value]) => { const placeholder = `\${${key}}`; if (!acc.includes(placeholder)) { return acc; } return acc.split(placeholder).join(value ?? ""); }, str); }; const traverse = (obj: any): any => { if (typeof obj === "string") { return replaceInString(obj); } if (Array.isArray(obj)) { return obj.map(traverse); } if (obj && typeof obj === "object") { const result: any = {}; for (const [key, value] of Object.entries(obj)) { result[key] = traverse(value); } return result; } return obj; }; return traverse(config); }; // 判断配置中是否存在 API Key 字段 export const hasApiKeyField = ( jsonString: string, appType?: string, ): boolean => { try { const config = JSON.parse(jsonString); const env = config?.env ?? {}; if (appType === "gemini") { return Object.prototype.hasOwnProperty.call(env, "GEMINI_API_KEY"); } if (appType === "codex") { return Object.prototype.hasOwnProperty.call(env, "CODEX_API_KEY"); } return ( Object.prototype.hasOwnProperty.call(env, "ANTHROPIC_AUTH_TOKEN") || Object.prototype.hasOwnProperty.call(env, "ANTHROPIC_API_KEY") ); } catch (err) { return false; } }; // 写入/更新配置中的 API Key,默认不新增缺失字段 export const setApiKeyInConfig = ( jsonString: string, apiKey: string, options: { createIfMissing?: boolean; appType?: string } = {}, ): string => { const { createIfMissing = false, appType } = options; try { const config = JSON.parse(jsonString); if (!config.env) { if (!createIfMissing) return jsonString; config.env = {}; } const env = config.env as Record; // Gemini API Key if (appType === "gemini") { if ("GEMINI_API_KEY" in env) { env.GEMINI_API_KEY = apiKey; } else if (createIfMissing) { env.GEMINI_API_KEY = apiKey; } else { return jsonString; } return JSON.stringify(config, null, 2); } // Codex API Key if (appType === "codex") { if ("CODEX_API_KEY" in env) { env.CODEX_API_KEY = apiKey; } else if (createIfMissing) { env.CODEX_API_KEY = apiKey; } else { return jsonString; } return JSON.stringify(config, null, 2); } // Claude API Key (优先写入已存在的字段;若两者均不存在且允许创建,则默认创建 AUTH_TOKEN 字段) if ("ANTHROPIC_AUTH_TOKEN" in env) { env.ANTHROPIC_AUTH_TOKEN = apiKey; } else if ("ANTHROPIC_API_KEY" in env) { env.ANTHROPIC_API_KEY = apiKey; } else if (createIfMissing) { env.ANTHROPIC_AUTH_TOKEN = apiKey; } else { return jsonString; } return JSON.stringify(config, null, 2); } catch (err) { return jsonString; } }; // ========== TOML Config Utilities ========== export interface UpdateTomlCommonConfigResult { updatedConfig: string; error?: string; } // 保存之前的通用配置片段,用于替换操作 let previousCommonSnippet = ""; // 将通用配置片段写入/移除 TOML 配置 export const updateTomlCommonConfigSnippet = ( tomlString: string, snippetString: string, enabled: boolean, ): UpdateTomlCommonConfigResult => { if (!snippetString.trim()) { // 如果片段为空,直接返回原始配置 return { updatedConfig: tomlString, }; } if (enabled) { // 添加通用配置 // 先移除旧的通用配置(如果有) let updatedConfig = tomlString; if (previousCommonSnippet && tomlString.includes(previousCommonSnippet)) { updatedConfig = tomlString.replace(previousCommonSnippet, ""); } // 在文件末尾添加新的通用配置 // 确保有适当的换行 const needsNewline = updatedConfig && !updatedConfig.endsWith("\n"); updatedConfig = updatedConfig + (needsNewline ? "\n\n" : "\n") + snippetString; // 保存当前通用配置片段 previousCommonSnippet = snippetString; return { updatedConfig: updatedConfig.trim() + "\n", }; } else { // 移除通用配置 if (tomlString.includes(snippetString)) { const updatedConfig = tomlString.replace(snippetString, ""); // 清理多余的空行 const cleaned = updatedConfig.replace(/\n{3,}/g, "\n\n").trim(); // 清空保存的状态 previousCommonSnippet = ""; return { updatedConfig: cleaned ? cleaned + "\n" : "", }; } return { updatedConfig: tomlString, }; } }; // 检查 TOML 配置是否已包含通用配置片段 export const hasTomlCommonConfigSnippet = ( tomlString: string, snippetString: string, ): boolean => { if (!snippetString.trim()) return false; // 简单检查配置是否包含片段内容 // 去除空白字符后比较,避免格式差异影响 const normalizeWhitespace = (str: string) => str.replace(/\s+/g, " ").trim(); return normalizeWhitespace(tomlString).includes( normalizeWhitespace(snippetString), ); }; // ========== Codex base_url utils ========== // 从 Codex 的 TOML 配置文本中提取 base_url(支持单/双引号) export const extractCodexBaseUrl = ( configText: string | undefined | null, ): string | undefined => { try { const raw = typeof configText === "string" ? configText : ""; // 归一化中文/全角引号,避免正则提取失败 const text = normalizeQuotes(raw); if (!text) return undefined; const m = text.match(/base_url\s*=\s*(['"])([^'\"]+)\1/); return m && m[2] ? m[2] : undefined; } catch { return undefined; } }; // 从 Provider 对象中提取 Codex base_url(当 settingsConfig.config 为 TOML 字符串时) export const getCodexBaseUrl = ( provider: { settingsConfig?: Record } | undefined | null, ): string | undefined => { try { const text = typeof provider?.settingsConfig?.config === "string" ? (provider as any).settingsConfig.config : ""; return extractCodexBaseUrl(text); } catch { return undefined; } }; // 在 Codex 的 TOML 配置文本中写入或更新 base_url 字段 export const setCodexBaseUrl = ( configText: string, baseUrl: string, ): string => { const trimmed = baseUrl.trim(); if (!trimmed) { return configText; } // 归一化原文本中的引号(既能匹配,也能输出稳定格式) const normalizedText = normalizeQuotes(configText); const normalizedUrl = trimmed.replace(/\s+/g, "").replace(/\/+$/, ""); const replacementLine = `base_url = "${normalizedUrl}"`; const pattern = /base_url\s*=\s*(["'])([^"']+)\1/; if (pattern.test(normalizedText)) { return normalizedText.replace(pattern, replacementLine); } const prefix = normalizedText && !normalizedText.endsWith("\n") ? `${normalizedText}\n` : normalizedText; return `${prefix}${replacementLine}\n`; }; // ========== Codex model name utils ========== // 从 Codex 的 TOML 配置文本中提取 model 字段(支持单/双引号) export const extractCodexModelName = ( configText: string | undefined | null, ): string | undefined => { try { const raw = typeof configText === "string" ? configText : ""; // 归一化中文/全角引号,避免正则提取失败 const text = normalizeQuotes(raw); if (!text) return undefined; // 匹配 model = "xxx" 或 model = 'xxx' const m = text.match(/^model\s*=\s*(['"])([^'"]+)\1/m); return m && m[2] ? m[2] : undefined; } catch { return undefined; } }; // 在 Codex 的 TOML 配置文本中写入或更新 model 字段 export const setCodexModelName = ( configText: string, modelName: string, ): string => { const trimmed = modelName.trim(); if (!trimmed) { return configText; } // 归一化原文本中的引号(既能匹配,也能输出稳定格式) const normalizedText = normalizeQuotes(configText); const replacementLine = `model = "${trimmed}"`; const pattern = /^model\s*=\s*["']([^"']+)["']/m; if (pattern.test(normalizedText)) { return normalizedText.replace(pattern, replacementLine); } // 如果不存在 model 字段,尝试在 model_provider 之后插入 // 如果 model_provider 也不存在,则插入到开头 const providerPattern = /^model_provider\s*=\s*["'][^"']+["']/m; const match = normalizedText.match(providerPattern); if (match && match.index !== undefined) { // 在 model_provider 行之后插入 const endOfLine = normalizedText.indexOf("\n", match.index); if (endOfLine !== -1) { return ( normalizedText.slice(0, endOfLine + 1) + replacementLine + "\n" + normalizedText.slice(endOfLine + 1) ); } } // 在文件开头插入 const lines = normalizedText.split("\n"); return `${replacementLine}\n${lines.join("\n")}`; };