fix: make Codex TOML base_url editing section-aware

Rewrite setCodexBaseUrl/extractCodexBaseUrl to understand TOML section
boundaries, ensuring base_url is written into the correct
[model_providers.<name>] section instead of being appended to file end.

- Add section-aware TOML helpers in providerConfigUtils.ts
- Extract shared update_codex_toml_field/remove_codex_toml_base_url_if
  in codex_config.rs, deduplicate proxy.rs TOML editing logic
- Replace scattered inline base_url regexes with extractCodexBaseUrl()
- Add comprehensive tests for both Rust and TypeScript implementations
This commit is contained in:
Jason
2026-03-12 22:20:59 +08:00
parent 7ca33ff901
commit 3568c98f57
10 changed files with 825 additions and 190 deletions
+348 -64
View File
@@ -1,7 +1,7 @@
// 供应商配置处理工具函数
import type { TemplateValueConfig } from "../config/claudeProviderPresets";
import { normalizeQuotes, normalizeTomlText } from "@/utils/textNormalization";
import { normalizeTomlText } from "@/utils/textNormalization";
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
const isPlainObject = (value: unknown): value is Record<string, any> => {
@@ -414,17 +414,234 @@ export const hasTomlCommonConfigSnippet = (
// ========== 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]+)\1\s*(?:#.*)?$/;
const TOML_MODEL_PATTERN = /^\s*model\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/;
const TOML_MODEL_PROVIDER_LINE_PATTERN =
/^\s*model_provider\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/;
const TOML_MODEL_PROVIDER_PATTERN =
/^\s*model_provider\s*=\s*(["'])([^"'\r\n]+)\1\s*(?:#.*)?$/m;
interface TomlSectionRange {
bodyEndIndex: number;
bodyStartIndex: 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,
};
}
if (headerLineIndex === -1) {
return undefined;
}
return {
bodyStartIndex: headerLineIndex + 1,
bodyEndIndex: lines.length,
};
};
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 match = configText.match(TOML_MODEL_PROVIDER_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 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);
if (match?.[2]) {
return {
index,
sectionName,
value: match[2],
};
}
}
return undefined;
};
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);
if (!match?.[2]) {
return;
}
assignments.push({
index,
sectionName: currentSectionName,
value: match[2],
});
});
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 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;
};
// 从 Codex 的 TOML 配置文本中提取 base_url(支持单/双引号)
export const extractCodexBaseUrl = (
configText: string | undefined | null,
): string | undefined => {
try {
const raw = typeof configText === "string" ? configText : "";
// 归一化中文/全角引号,避免正则提取失败
const text = normalizeQuotes(raw);
const text = normalizeTomlText(raw);
if (!text) return undefined;
const m = text.match(/base_url\s*=\s*(['"])([^'\"]+)\1/);
return m && m[2] ? m[2] : 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;
}
@@ -451,36 +668,107 @@ export const setCodexBaseUrl = (
baseUrl: string,
): string => {
const trimmed = baseUrl.trim();
// 归一化原文本中的引号(既能匹配,也能输出稳定格式)
const normalizedText = normalizeQuotes(configText);
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,
);
// 允许清空:当 baseUrl 为空时,移除 base_url 行
if (!trimmed) {
if (!normalizedText) return normalizedText;
const next = normalizedText
.split("\n")
.filter((line) => !/^\s*base_url\s*=/.test(line))
.join("\n")
// 避免移除后留下过多空行
.replace(/\n{3,}/g, "\n\n")
// 避免开头出现空行
.replace(/^\n+/, "");
return next;
if (targetSectionName) {
const sectionRange = getTomlSectionRange(lines, targetSectionName);
const targetMatch = sectionRange
? findTomlAssignmentInRange(
lines,
TOML_BASE_URL_PATTERN,
sectionRange.bodyStartIndex,
sectionRange.bodyEndIndex,
targetSectionName,
)
: undefined;
if (targetMatch) {
lines.splice(targetMatch.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}"`;
const pattern = /base_url\s*=\s*(["'])([^"']+)\1/;
if (pattern.test(normalizedText)) {
return normalizedText.replace(pattern, replacementLine);
if (targetSectionName) {
let targetSectionRange = getTomlSectionRange(lines, targetSectionName);
const targetMatch = targetSectionRange
? findTomlAssignmentInRange(
lines,
TOML_BASE_URL_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 prefix =
normalizedText && !normalizedText.endsWith("\n")
? `${normalizedText}\n`
: normalizedText;
return `${prefix}${replacementLine}\n`;
const topLevelEndIndex = getTopLevelEndIndex(lines);
const topLevelMatch = findTomlAssignmentInRange(
lines,
TOML_BASE_URL_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`;
}
const insertIndex = topLevelEndIndex;
lines.splice(insertIndex, 0, replacementLine);
return finalizeTomlText(lines);
};
// ========== Codex model name utils ==========
@@ -491,13 +779,16 @@ export const extractCodexModelName = (
): string | undefined => {
try {
const raw = typeof configText === "string" ? configText : "";
// 归一化中文/全角引号,避免正则提取失败
const text = normalizeQuotes(raw);
const text = normalizeTomlText(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;
const lines = text.split("\n");
const topLevelMatch = findTomlAssignmentInRange(
lines,
TOML_MODEL_PATTERN,
0,
getTopLevelEndIndex(lines),
);
return topLevelMatch?.value;
} catch {
return undefined;
}
@@ -509,47 +800,40 @@ export const setCodexModelName = (
modelName: string,
): string => {
const trimmed = modelName.trim();
// 归一化原文本中的引号(既能匹配,也能输出稳定格式)
const normalizedText = normalizeQuotes(configText);
const normalizedText = normalizeTomlText(configText);
const lines = normalizedText ? normalizedText.split("\n") : [];
const topLevelEndIndex = getTopLevelEndIndex(lines);
const topLevelMatch = findTomlAssignmentInRange(
lines,
TOML_MODEL_PATTERN,
0,
topLevelEndIndex,
);
// 允许清空:当 modelName 为空时,移除 model 行
if (!trimmed) {
if (!normalizedText) return normalizedText;
const next = normalizedText
.split("\n")
.filter((line) => !/^\s*model\s*=/.test(line))
.join("\n")
.replace(/\n{3,}/g, "\n\n")
.replace(/^\n+/, "");
return next;
if (topLevelMatch) {
lines.splice(topLevelMatch.index, 1);
}
return finalizeTomlText(lines);
}
const replacementLine = `model = "${trimmed}"`;
const pattern = /^model\s*=\s*["']([^"']+)["']/m;
if (pattern.test(normalizedText)) {
return normalizedText.replace(pattern, replacementLine);
if (topLevelMatch) {
lines[topLevelMatch.index] = replacementLine;
return finalizeTomlText(lines);
}
// 如果不存在 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 modelProviderIndex = getTopLevelModelProviderLineIndex(lines);
if (modelProviderIndex !== -1) {
lines.splice(modelProviderIndex + 1, 0, replacementLine);
return finalizeTomlText(lines);
}
// 在文件开头插入
const lines = normalizedText.split("\n");
return `${replacementLine}\n${lines.join("\n")}`;
if (lines.length === 0) {
return `${replacementLine}\n`;
}
lines.splice(topLevelEndIndex, 0, replacementLine);
return finalizeTomlText(lines);
};