// 供应商配置处理工具函数 import type { TemplateValueConfig } from "../config/claudeProviderPresets"; import { deepClone } from "@/utils/deepClone"; import { normalizeTomlText } from "@/utils/textNormalization"; import { parse as parseToml, stringify as stringifyToml } from "smol-toml"; 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; }; 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); // 优先检查顶层 apiKey 字段(用于 Bedrock API Key 等预设) if ( typeof config?.apiKey === "string" && config.apiKey && !config.apiKey.includes("${") ) { return config.apiKey; } 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); // 检查顶层 apiKey 字段(用于 Bedrock API Key 等预设) if (Object.prototype.hasOwnProperty.call(config, "apiKey")) { return true; } 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; apiKeyField?: string; } = {}, ): string => { const { createIfMissing = false, appType, apiKeyField } = options; try { const config = JSON.parse(jsonString); // 优先检查顶层 apiKey 字段(用于 Bedrock API Key 等预设) if (Object.prototype.hasOwnProperty.call(config, "apiKey")) { config.apiKey = apiKey; return JSON.stringify(config, null, 2); } 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 (优先写入已存在的字段;若两者均不存在且允许创建,则使用 apiKeyField 或默认 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[apiKeyField ?? "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; } // Write/remove common config snippet to/from TOML config (structural merge) export const updateTomlCommonConfigSnippet = ( tomlString: string, snippetString: string, enabled: boolean, ): UpdateTomlCommonConfigResult => { if (!snippetString.trim()) { return { updatedConfig: tomlString }; } try { const config = parseToml(normalizeTomlText(tomlString || "")); const snippet = parseToml(normalizeTomlText(snippetString)); if (enabled) { const merged = deepMerge( deepClone(config) as Record, deepClone(snippet) as Record, ); return { updatedConfig: stringifyToml(merged) }; } else { const result = deepClone(config) as Record; deepRemove(result, snippet as Record); return { updatedConfig: stringifyToml(result) }; } } catch (e) { return { updatedConfig: tomlString, error: String(e) }; } }; // Check if TOML config already contains the common config snippet (structural subset check) export const hasTomlCommonConfigSnippet = ( tomlString: string, snippetString: string, ): boolean => { if (!snippetString.trim()) return false; try { const config = parseToml(normalizeTomlText(tomlString || "")); const snippet = parseToml(normalizeTomlText(snippetString)); return isSubset(config, snippet); } catch { // Fallback to text-based matching if TOML parsing fails const norm = (s: string) => s.replace(/\s+/g, " ").trim(); return norm(tomlString).includes(norm(snippetString)); } }; // ========== Codex base_url utils ========== const TOML_SECTION_HEADER_PATTERN = /^\s*\[([^\]\r\n]+)\]\s*$/; const TOML_BASE_URL_PATTERN = /^\s*base_url\s*=\s*(?:"((?:\\.|[^"\\\r\n])*)"|'([^'\r\n]*)')\s*(?:#.*)?$/; const TOML_EXPERIMENTAL_BEARER_TOKEN_PATTERN = /^\s*experimental_bearer_token\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/; const TOML_EXPERIMENTAL_BEARER_TOKEN_REPLACE_PATTERN = /^(\s*experimental_bearer_token\s*=\s*)(?:"(?:\\.|[^"\\\r\n])*"|'[^'\r\n]*')(\s*(?:#.*)?)$/; const TOML_MODEL_PATTERN = /^\s*model\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/; const TOML_WIRE_API_PATTERN = /^\s*wire_api\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/; const TOML_MODEL_PROVIDER_LINE_PATTERN = /^\s*model_provider\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/; const TOML_PROVIDER_NAME_PATTERN = /^\s*name\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/; const TOML_PROVIDER_NAME_REPLACE_PATTERN = /^(\s*name\s*=\s*)(?:"(?:\\.|[^"\\\r\n])*"|'[^'\r\n]*')(\s*(?:#.*)?)$/; const TOML_GOALS_FEATURE_PATTERN = /^\s*goals\s*=\s*(true|false)\s*(?:#.*)?$/; const TOML_GOALS_FEATURE_REPLACE_PATTERN = /^(\s*goals\s*=\s*)(true|false)(\s*(?:#.*)?)$/; const CODEX_RESERVED_MODEL_PROVIDER_IDS = new Set([ "amazon-bedrock", "openai", "ollama", "lmstudio", "oss", "ollama-chat", ]); interface TomlSectionRange { bodyEndIndex: number; bodyStartIndex: number; headerLineIndex: number; } interface TomlAssignmentMatch { index: number; sectionName?: string; value: string; } const finalizeTomlText = (lines: string[]): string => lines .join("\n") .replace(/\n{3,}/g, "\n\n") .replace(/^\n+/, ""); const getTomlSectionRange = ( lines: string[], sectionName: string, ): TomlSectionRange | undefined => { let headerLineIndex = -1; for (let index = 0; index < lines.length; index += 1) { const match = lines[index].match(TOML_SECTION_HEADER_PATTERN); if (!match) { continue; } if (headerLineIndex === -1) { if (match[1] === sectionName) { headerLineIndex = index; } continue; } return { bodyStartIndex: headerLineIndex + 1, bodyEndIndex: index, headerLineIndex, }; } if (headerLineIndex === -1) { return undefined; } return { bodyStartIndex: headerLineIndex + 1, bodyEndIndex: lines.length, headerLineIndex, }; }; const getTopLevelEndIndex = (lines: string[]): number => { const firstSectionIndex = lines.findIndex((line) => TOML_SECTION_HEADER_PATTERN.test(line), ); return firstSectionIndex === -1 ? lines.length : firstSectionIndex; }; const getTomlSectionInsertIndex = ( lines: string[], sectionRange: TomlSectionRange, ): number => { let insertIndex = sectionRange.bodyEndIndex; while ( insertIndex > sectionRange.bodyStartIndex && lines[insertIndex - 1].trim() === "" ) { insertIndex -= 1; } return insertIndex; }; const getCodexModelProviderName = (configText: string): string | undefined => { const normalized = normalizeTomlText(configText); try { const parsed = parseToml(normalized) as Record; const providerName = typeof parsed.model_provider === "string" ? parsed.model_provider.trim() : undefined; if (providerName) return providerName; } catch { // Fall back to a top-level line scan while the user is editing invalid TOML. } const lines = normalized.split("\n"); const index = getTopLevelModelProviderLineIndex(lines); if (index === -1) return undefined; const match = lines[index].match(TOML_MODEL_PROVIDER_LINE_PATTERN); const providerName = match?.[2]?.trim(); return providerName || undefined; }; const getCodexProviderSectionName = ( configText: string, ): string | undefined => { const providerName = getCodexModelProviderName(configText); return providerName ? `model_providers.${providerName}` : undefined; }; const isCustomCodexModelProviderId = (providerName: string): boolean => { const id = providerName.trim().toLowerCase(); return Boolean(id) && !CODEX_RESERVED_MODEL_PROVIDER_IDS.has(id); }; const getCodexCustomProviderSectionName = ( configText: string, ): string | undefined => { const providerName = getCodexModelProviderName(configText); return providerName && isCustomCodexModelProviderId(providerName) ? `model_providers.${providerName}` : undefined; }; const findTomlAssignmentInRange = ( lines: string[], pattern: RegExp, startIndex: number, endIndex: number, sectionName?: string, ): TomlAssignmentMatch | undefined => { for (let index = startIndex; index < endIndex; index += 1) { const match = lines[index].match(pattern); const value = match?.[2] ?? match?.[1]; if (value) { return { index, sectionName, value, }; } } return undefined; }; const findTomlAssignmentsInRange = ( lines: string[], pattern: RegExp, startIndex: number, endIndex: number, sectionName?: string, ): TomlAssignmentMatch[] => { const matches: TomlAssignmentMatch[] = []; for (let index = startIndex; index < endIndex; index += 1) { const match = lines[index].match(pattern); const value = match?.[2] ?? match?.[1]; if (value) { matches.push({ index, sectionName, value }); } } return matches; }; const findTomlLineInRange = ( lines: string[], pattern: RegExp, startIndex: number, endIndex: number, ): number => { for (let index = startIndex; index < endIndex; index += 1) { if (pattern.test(lines[index])) { return index; } } return -1; }; const findTomlAssignments = ( lines: string[], pattern: RegExp, ): TomlAssignmentMatch[] => { const assignments: TomlAssignmentMatch[] = []; let currentSectionName: string | undefined; lines.forEach((line, index) => { const sectionMatch = line.match(TOML_SECTION_HEADER_PATTERN); if (sectionMatch) { currentSectionName = sectionMatch[1]; return; } const match = line.match(pattern); const value = match?.[2] ?? match?.[1]; if (!value) { return; } assignments.push({ index, sectionName: currentSectionName, value, }); }); return assignments; }; const isMcpServerSection = (sectionName?: string): boolean => sectionName === "mcp_servers" || sectionName?.startsWith("mcp_servers.") === true; const isOtherProviderSection = ( sectionName: string | undefined, targetSectionName: string | undefined, ): boolean => Boolean( sectionName && sectionName !== targetSectionName && (sectionName === "model_providers" || sectionName.startsWith("model_providers.")), ); const getRecoverableBaseUrlAssignments = ( assignments: TomlAssignmentMatch[], targetSectionName: string | undefined, ): TomlAssignmentMatch[] => assignments.filter( ({ sectionName }) => sectionName !== targetSectionName && !isMcpServerSection(sectionName) && !isOtherProviderSection(sectionName, targetSectionName), ); const getRecoverableCodexProviderAssignments = getRecoverableBaseUrlAssignments; const getTopLevelModelProviderLineIndex = (lines: string[]): number => { const topLevelEndIndex = getTopLevelEndIndex(lines); for (let index = 0; index < topLevelEndIndex; index += 1) { if (TOML_MODEL_PROVIDER_LINE_PATTERN.test(lines[index])) { return index; } } return -1; }; const hasTomlSectionBodyContent = ( lines: string[], sectionRange: TomlSectionRange, ): boolean => lines .slice(sectionRange.bodyStartIndex, sectionRange.bodyEndIndex) .some((line) => line.trim() !== ""); const TOML_BASIC_STRING_ESCAPES: Record = { '"': '\\"', "\\": "\\\\", "\b": "\\b", "\t": "\\t", "\n": "\\n", "\f": "\\f", "\r": "\\r", }; const escapeTomlBasicString = (value: string): string => value.replace(/["\\\u0000-\u001f]/g, (ch) => { const escaped = TOML_BASIC_STRING_ESCAPES[ch]; if (escaped) return escaped; return `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`; }); const tomlBasicString = (value: string): string => `"${escapeTomlBasicString(value)}"`; const CODEX_CHAT_WIRE_API_VALUES = new Set([ "chat", "chat_completions", "chat-completions", "openai_chat", "openai-chat", "openai_chat_completions", ]); // 判断给定的 wire_api 字符串是否表示 Codex 的 Chat Completions 协议 export const isCodexChatWireApi = ( wireApi: string | undefined | null, ): boolean => CODEX_CHAT_WIRE_API_VALUES.has((wireApi ?? "").trim().toLowerCase()); // 从 Codex 的 TOML 配置文本中提取 wire_api(支持单/双引号) export const extractCodexWireApi = ( configText: string | undefined | null, ): string | undefined => { try { const raw = typeof configText === "string" ? configText : ""; const text = normalizeTomlText(raw); if (!text) return undefined; const lines = text.split("\n"); const targetSectionName = getCodexProviderSectionName(text); if (targetSectionName) { const sectionRange = getTomlSectionRange(lines, targetSectionName); if (sectionRange) { const match = findTomlAssignmentInRange( lines, TOML_WIRE_API_PATTERN, sectionRange.bodyStartIndex, sectionRange.bodyEndIndex, targetSectionName, ); if (match?.value) { return match.value; } } } const topLevelMatch = findTomlAssignmentInRange( lines, TOML_WIRE_API_PATTERN, 0, getTopLevelEndIndex(lines), ); if (topLevelMatch?.value) { return topLevelMatch.value; } const fallbackAssignments = getRecoverableCodexProviderAssignments( findTomlAssignments(lines, TOML_WIRE_API_PATTERN), targetSectionName, ); return fallbackAssignments.length === 1 ? fallbackAssignments[0].value : undefined; } catch { return undefined; } }; // 在 Codex 的 TOML 配置文本中写入或更新 wire_api 字段 export const setCodexWireApi = ( configText: string, wireApi: "responses" | "chat", ): string => { const normalizedText = normalizeTomlText(configText); const lines = normalizedText ? normalizedText.split("\n") : []; const targetSectionName = getCodexProviderSectionName(normalizedText); const replacementLine = `wire_api = "${wireApi}"`; const allAssignments = findTomlAssignments(lines, TOML_WIRE_API_PATTERN); const recoverableAssignments = getRecoverableCodexProviderAssignments( allAssignments, targetSectionName, ); if (targetSectionName) { let targetSectionRange = getTomlSectionRange(lines, targetSectionName); const targetMatch = targetSectionRange ? findTomlAssignmentInRange( lines, TOML_WIRE_API_PATTERN, targetSectionRange.bodyStartIndex, targetSectionRange.bodyEndIndex, targetSectionName, ) : undefined; if (targetMatch) { lines[targetMatch.index] = replacementLine; return finalizeTomlText(lines); } if (recoverableAssignments.length === 1) { lines.splice(recoverableAssignments[0].index, 1); targetSectionRange = getTomlSectionRange(lines, targetSectionName); } if (targetSectionRange) { const insertIndex = getTomlSectionInsertIndex(lines, targetSectionRange); lines.splice(insertIndex, 0, replacementLine); return finalizeTomlText(lines); } if (lines.length > 0 && lines[lines.length - 1].trim() !== "") { lines.push(""); } lines.push(`[${targetSectionName}]`, replacementLine); return finalizeTomlText(lines); } const topLevelEndIndex = getTopLevelEndIndex(lines); const topLevelMatch = findTomlAssignmentInRange( lines, TOML_WIRE_API_PATTERN, 0, topLevelEndIndex, ); if (topLevelMatch) { lines[topLevelMatch.index] = replacementLine; return finalizeTomlText(lines); } const modelProviderIndex = getTopLevelModelProviderLineIndex(lines); if (modelProviderIndex !== -1) { lines.splice(modelProviderIndex + 1, 0, replacementLine); return finalizeTomlText(lines); } if (lines.length === 0) { return `${replacementLine}\n`; } lines.splice(topLevelEndIndex, 0, replacementLine); return finalizeTomlText(lines); }; export const isCodexGoalModeEnabled = ( configText: string | undefined | null, ): boolean => { try { const raw = typeof configText === "string" ? configText : ""; const text = normalizeTomlText(raw); if (!text) return false; try { const parsed = parseToml(text) as Record; return parsed.features?.goals === true; } catch { // Fall back to line scanning while the user is editing invalid TOML. } const lines = text.split("\n"); const featureRange = getTomlSectionRange(lines, "features"); if (!featureRange) return false; const index = findTomlLineInRange( lines, TOML_GOALS_FEATURE_PATTERN, featureRange.bodyStartIndex, featureRange.bodyEndIndex, ); if (index === -1) return false; return lines[index].match(TOML_GOALS_FEATURE_PATTERN)?.[1] === "true"; } catch { return false; } }; export const setCodexGoalMode = ( configText: string, enabled: boolean, ): string => { const normalizedText = normalizeTomlText(configText); const lines = normalizedText ? normalizedText.split("\n") : []; let featureRange = getTomlSectionRange(lines, "features"); if (featureRange) { const goalLineIndex = findTomlLineInRange( lines, TOML_GOALS_FEATURE_REPLACE_PATTERN, featureRange.bodyStartIndex, featureRange.bodyEndIndex, ); if (enabled) { if (goalLineIndex !== -1) { lines[goalLineIndex] = lines[goalLineIndex].replace( TOML_GOALS_FEATURE_REPLACE_PATTERN, "$1true$3", ); } else { lines.splice( getTomlSectionInsertIndex(lines, featureRange), 0, "goals = true", ); } return finalizeTomlText(lines); } if (goalLineIndex !== -1) { lines.splice(goalLineIndex, 1); featureRange = getTomlSectionRange(lines, "features"); if (featureRange && !hasTomlSectionBodyContent(lines, featureRange)) { lines.splice( featureRange.headerLineIndex, featureRange.bodyEndIndex - featureRange.headerLineIndex, ); } } return finalizeTomlText(lines); } if (!enabled) return normalizedText; const topLevelEndIndex = getTopLevelEndIndex(lines); const sectionLines: string[] = []; if (topLevelEndIndex > 0 && lines[topLevelEndIndex - 1].trim() !== "") { sectionLines.push(""); } sectionLines.push("[features]", "goals = true"); if ( topLevelEndIndex < lines.length && lines[topLevelEndIndex]?.trim() !== "" ) { sectionLines.push(""); } lines.splice(topLevelEndIndex, 0, ...sectionLines); return finalizeTomlText(lines); }; export const isCodexRemoteCompactionEnabled = ( configText: string | undefined | null, ): boolean => { try { const raw = typeof configText === "string" ? configText : ""; const text = normalizeTomlText(raw); if (!text) return false; try { const parsed = parseToml(text) as Record; const providerId = typeof parsed.model_provider === "string" ? parsed.model_provider.trim() : ""; if (!providerId || !isCustomCodexModelProviderId(providerId)) { return false; } return parsed.model_providers?.[providerId]?.name === "OpenAI"; } catch { // Fall back to line scanning while the user is editing invalid TOML. } const lines = text.split("\n"); const targetSectionName = getCodexCustomProviderSectionName(text); if (!targetSectionName) return false; const sectionRange = getTomlSectionRange(lines, targetSectionName); if (!sectionRange) return false; const nameAssignment = findTomlAssignmentInRange( lines, TOML_PROVIDER_NAME_PATTERN, sectionRange.bodyStartIndex, sectionRange.bodyEndIndex, targetSectionName, ); return nameAssignment?.value === "OpenAI"; } catch { return false; } }; export const setCodexRemoteCompaction = ( configText: string, enabled: boolean, fallbackProviderName?: string, ): string => { const normalizedText = normalizeTomlText(configText); const lines = normalizedText ? normalizedText.split("\n") : []; const targetSectionName = getCodexCustomProviderSectionName(normalizedText); if (!targetSectionName) { return normalizedText; } let targetSectionRange = getTomlSectionRange(lines, targetSectionName); const replacementName = enabled ? "OpenAI" : fallbackProviderName?.trim() || getCodexModelProviderName(normalizedText) || "custom"; const replacementLine = `name = ${tomlBasicString(replacementName)}`; if (targetSectionRange) { const nameLine = findTomlLineInRange( lines, TOML_PROVIDER_NAME_REPLACE_PATTERN, targetSectionRange.bodyStartIndex, targetSectionRange.bodyEndIndex, ); if (nameLine !== -1) { lines[nameLine] = lines[nameLine].replace( TOML_PROVIDER_NAME_REPLACE_PATTERN, `$1${tomlBasicString(replacementName)}$2`, ); return finalizeTomlText(lines); } lines.splice( getTomlSectionInsertIndex(lines, targetSectionRange), 0, replacementLine, ); return finalizeTomlText(lines); } if (!enabled) return normalizedText; if (lines.length > 0 && lines[lines.length - 1].trim() !== "") { lines.push(""); } lines.push(`[${targetSectionName}]`, replacementLine); targetSectionRange = getTomlSectionRange(lines, targetSectionName); return targetSectionRange ? finalizeTomlText(lines) : normalizedText; }; // 从 Codex 的 TOML 配置文本中提取 base_url(支持单/双引号) export const extractCodexBaseUrl = ( configText: string | undefined | null, ): string | undefined => { try { const raw = typeof configText === "string" ? configText : ""; const text = normalizeTomlText(raw); if (!text) return undefined; const lines = text.split("\n"); const targetSectionName = getCodexProviderSectionName(text); if (targetSectionName) { const sectionRange = getTomlSectionRange(lines, targetSectionName); if (sectionRange) { const match = findTomlAssignmentInRange( lines, TOML_BASE_URL_PATTERN, sectionRange.bodyStartIndex, sectionRange.bodyEndIndex, targetSectionName, ); if (match?.value) { return match.value; } } } const topLevelMatch = findTomlAssignmentInRange( lines, TOML_BASE_URL_PATTERN, 0, getTopLevelEndIndex(lines), ); if (topLevelMatch?.value) { return topLevelMatch.value; } const fallbackAssignments = getRecoverableBaseUrlAssignments( findTomlAssignments(lines, TOML_BASE_URL_PATTERN), targetSectionName, ); return fallbackAssignments.length === 1 ? fallbackAssignments[0].value : undefined; } catch { return undefined; } }; // 从 Codex 的 TOML 配置文本中提取 experimental_bearer_token(兼容 Mobile 模式) export const extractCodexExperimentalBearerToken = ( configText: string | undefined | null, ): string | undefined => { try { const raw = typeof configText === "string" ? configText : ""; const text = normalizeTomlText(raw); if (!text) return undefined; try { const parsed = parseToml(text) as Record; const providerName = typeof parsed.model_provider === "string" ? parsed.model_provider.trim() : undefined; const providerToken = providerName && isCustomCodexModelProviderId(providerName) && parsed.model_providers && typeof parsed.model_providers === "object" && typeof parsed.model_providers[providerName] ?.experimental_bearer_token === "string" ? parsed.model_providers[ providerName ].experimental_bearer_token.trim() : undefined; if (providerToken) return providerToken; const topLevelToken = typeof parsed.experimental_bearer_token === "string" ? parsed.experimental_bearer_token.trim() : undefined; if (topLevelToken) return topLevelToken; } catch { // Fall back to the line scanner for partially edited TOML. } const lines = text.split("\n"); const targetSectionName = getCodexCustomProviderSectionName(text); if (targetSectionName) { const sectionRange = getTomlSectionRange(lines, targetSectionName); if (sectionRange) { const match = findTomlAssignmentInRange( lines, TOML_EXPERIMENTAL_BEARER_TOKEN_PATTERN, sectionRange.bodyStartIndex, sectionRange.bodyEndIndex, targetSectionName, ); if (match?.value) { return match.value; } } } const topLevelMatch = findTomlAssignmentInRange( lines, TOML_EXPERIMENTAL_BEARER_TOKEN_PATTERN, 0, getTopLevelEndIndex(lines), ); return topLevelMatch?.value; } catch { return undefined; } }; // 同步更新 Codex config.toml 中已有的 experimental_bearer_token // 仅修改已存在的条目, 不主动新增——避免破坏未使用 Mobile 兼容模式的普通 third-party 配置 // token 为空时删除该行 (让用户能真正清空 API key, 而不是被 pickCodexApiKey 的 fallback 又填回去) export const updateCodexExperimentalBearerToken = ( configText: string, token: string, ): string => { const normalizedText = normalizeTomlText(configText); if ( !normalizedText || !normalizedText.includes("experimental_bearer_token") ) { return configText; } const lines = normalizedText.split("\n"); const targetSectionName = getCodexCustomProviderSectionName(normalizedText); let tokenLineIndex = -1; if (targetSectionName) { const sectionRange = getTomlSectionRange(lines, targetSectionName); if (sectionRange) { const index = findTomlLineInRange( lines, TOML_EXPERIMENTAL_BEARER_TOKEN_REPLACE_PATTERN, sectionRange.bodyStartIndex, sectionRange.bodyEndIndex, ); if (index !== -1) tokenLineIndex = index; } } if (tokenLineIndex === -1) { const topLevelIndex = findTomlLineInRange( lines, TOML_EXPERIMENTAL_BEARER_TOKEN_REPLACE_PATTERN, 0, getTopLevelEndIndex(lines), ); if (topLevelIndex !== -1) tokenLineIndex = topLevelIndex; } if (tokenLineIndex === -1) return configText; const trimmed = token.trim(); if (!trimmed) { lines.splice(tokenLineIndex, 1); } else { const escaped = escapeTomlBasicString(trimmed); const existingLine = lines[tokenLineIndex]; lines[tokenLineIndex] = TOML_EXPERIMENTAL_BEARER_TOKEN_REPLACE_PATTERN.test( existingLine, ) ? existingLine.replace( TOML_EXPERIMENTAL_BEARER_TOKEN_REPLACE_PATTERN, `$1"${escaped}"$2`, ) : `experimental_bearer_token = "${escaped}"`; } return finalizeTomlText(lines); }; // 从 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(); const normalizedText = normalizeTomlText(configText); const lines = normalizedText ? normalizedText.split("\n") : []; const targetSectionName = getCodexProviderSectionName(normalizedText); const allAssignments = findTomlAssignments(lines, TOML_BASE_URL_PATTERN); const recoverableAssignments = getRecoverableBaseUrlAssignments( allAssignments, targetSectionName, ); if (!trimmed) { if (!normalizedText) return normalizedText; if (targetSectionName) { const sectionRange = getTomlSectionRange(lines, targetSectionName); const targetMatches = sectionRange ? findTomlAssignmentsInRange( lines, TOML_BASE_URL_PATTERN, sectionRange.bodyStartIndex, sectionRange.bodyEndIndex, targetSectionName, ) : []; if (targetMatches.length > 0) { for (const match of [...targetMatches].reverse()) { lines.splice(match.index, 1); } return finalizeTomlText(lines); } } if (recoverableAssignments.length === 1) { lines.splice(recoverableAssignments[0].index, 1); return finalizeTomlText(lines); } return finalizeTomlText(lines); } const normalizedUrl = trimmed.replace(/\s+/g, ""); const replacementLine = `base_url = "${normalizedUrl}"`; if (targetSectionName) { let targetSectionRange = getTomlSectionRange(lines, targetSectionName); const targetMatches = targetSectionRange ? findTomlAssignmentsInRange( lines, TOML_BASE_URL_PATTERN, targetSectionRange.bodyStartIndex, targetSectionRange.bodyEndIndex, targetSectionName, ) : []; if (targetMatches.length > 0) { const [firstMatch, ...duplicateMatches] = targetMatches; lines[firstMatch.index] = replacementLine; for (const match of [...duplicateMatches].reverse()) { lines.splice(match.index, 1); } return finalizeTomlText(lines); } if (recoverableAssignments.length === 1) { lines.splice(recoverableAssignments[0].index, 1); targetSectionRange = getTomlSectionRange(lines, targetSectionName); } if (targetSectionRange) { const insertIndex = getTomlSectionInsertIndex(lines, targetSectionRange); lines.splice(insertIndex, 0, replacementLine); return finalizeTomlText(lines); } if (lines.length > 0 && lines[lines.length - 1].trim() !== "") { lines.push(""); } lines.push(`[${targetSectionName}]`, replacementLine); return finalizeTomlText(lines); } const topLevelEndIndex = getTopLevelEndIndex(lines); const topLevelMatches = findTomlAssignmentsInRange( lines, TOML_BASE_URL_PATTERN, 0, topLevelEndIndex, ); if (topLevelMatches.length > 0) { const [firstMatch, ...duplicateMatches] = topLevelMatches; lines[firstMatch.index] = replacementLine; for (const match of [...duplicateMatches].reverse()) { lines.splice(match.index, 1); } return finalizeTomlText(lines); } const modelProviderIndex = getTopLevelModelProviderLineIndex(lines); if (modelProviderIndex !== -1) { lines.splice(modelProviderIndex + 1, 0, replacementLine); return finalizeTomlText(lines); } if (lines.length === 0) { return `${replacementLine}\n`; } const insertIndex = topLevelEndIndex; lines.splice(insertIndex, 0, replacementLine); return finalizeTomlText(lines); }; // ========== 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 = normalizeTomlText(raw); if (!text) return undefined; const lines = text.split("\n"); const topLevelMatch = findTomlAssignmentInRange( lines, TOML_MODEL_PATTERN, 0, getTopLevelEndIndex(lines), ); return topLevelMatch?.value; } catch { return undefined; } }; // 在 Codex 的 TOML 配置文本中写入或更新 model 字段 export const setCodexModelName = ( configText: string, modelName: string, ): string => { const trimmed = modelName.trim(); const normalizedText = normalizeTomlText(configText); const lines = normalizedText ? normalizedText.split("\n") : []; const topLevelEndIndex = getTopLevelEndIndex(lines); const topLevelMatch = findTomlAssignmentInRange( lines, TOML_MODEL_PATTERN, 0, topLevelEndIndex, ); if (!trimmed) { if (!normalizedText) return normalizedText; if (topLevelMatch) { lines.splice(topLevelMatch.index, 1); } return finalizeTomlText(lines); } const replacementLine = `model = "${trimmed}"`; if (topLevelMatch) { lines[topLevelMatch.index] = replacementLine; return finalizeTomlText(lines); } const modelProviderIndex = getTopLevelModelProviderLineIndex(lines); if (modelProviderIndex !== -1) { lines.splice(modelProviderIndex + 1, 0, replacementLine); return finalizeTomlText(lines); } if (lines.length === 0) { return `${replacementLine}\n`; } lines.splice(topLevelEndIndex, 0, replacementLine); return finalizeTomlText(lines); }; // ========== Codex top-level integer field utils ========== const tomlTopLevelIntPattern = (field: string) => new RegExp(`^\\s*${field}\\s*=\\s*(\\d+)\\s*(?:#.*)?$`); const findTopLevelIntMatch = ( lines: string[], fieldName: string, topLevelEndIndex: number, ): { index: number; value: number } | undefined => { const pattern = tomlTopLevelIntPattern(fieldName); for (let i = 0; i < topLevelEndIndex; i += 1) { const m = lines[i].match(pattern); if (m) { return { index: i, value: Number(m[1]) }; } } return undefined; }; // 从 Codex TOML 配置中提取顶级整数字段 export const extractCodexTopLevelInt = ( configText: string | undefined | null, fieldName: string, ): number | undefined => { try { const raw = typeof configText === "string" ? configText : ""; const text = normalizeTomlText(raw); if (!text) return undefined; const lines = text.split("\n"); return findTopLevelIntMatch(lines, fieldName, getTopLevelEndIndex(lines)) ?.value; } catch { return undefined; } }; // 在 Codex TOML 配置中设置或更新顶级整数字段 export const setCodexTopLevelInt = ( configText: string, fieldName: string, value: number, ): string => { const normalizedText = normalizeTomlText(configText); const lines = normalizedText ? normalizedText.split("\n") : []; const topLevelEndIndex = getTopLevelEndIndex(lines); const existing = findTopLevelIntMatch(lines, fieldName, topLevelEndIndex); const replacementLine = `${fieldName} = ${value}`; if (existing) { lines[existing.index] = replacementLine; return finalizeTomlText(lines); } // 插入位置:顶级区域末尾(section header 之前) if (lines.length === 0) { return `${replacementLine}\n`; } lines.splice(topLevelEndIndex, 0, replacementLine); return finalizeTomlText(lines); }; // 从 Codex TOML 配置中移除顶级字段行 export const removeCodexTopLevelField = ( configText: string, fieldName: string, ): string => { const normalizedText = normalizeTomlText(configText); if (!normalizedText) return normalizedText; const lines = normalizedText.split("\n"); const topLevelEndIndex = getTopLevelEndIndex(lines); const existing = findTopLevelIntMatch(lines, fieldName, topLevelEndIndex); if (existing) { lines.splice(existing.index, 1); } return finalizeTomlText(lines); };