feat: 发布热更 1.5.94,修复中转历史会话并支持一键清空

This commit is contained in:
mac
2026-09-11 09:06:41 +08:00
parent 0446d09b3b
commit a74576c3e1
28 changed files with 3217 additions and 155 deletions
+302 -29
View File
@@ -111,6 +111,7 @@ function restoreFileState(file, state) {
function writeCodexConfigPair(configText, authText) {
const configFile = path.join(CODEX_HOME, "config.toml");
const authFile = path.join(CODEX_HOME, "auth.json");
assertManagedConfigComplete(configText);
const beforeConfig = readFileState(configFile);
const beforeAuth = readFileState(authFile);
try {
@@ -372,7 +373,7 @@ function currentRootModel() {
}
function extractProviderBlock(text, prov) {
const esc = prov.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const re = new RegExp(`\\[model_providers\\.${esc}\\][\\s\\S]*?(?=\\n\\[|$)`, "m");
const re = new RegExp(`\\[model_providers\\.${esc}\\][\\s\\S]*?(?=\\n\\[|$)`);
const m = text.match(re);
return m ? m[0].trimEnd() + "\n" : "";
}
@@ -442,6 +443,46 @@ function removeTomlSection(text, header) {
function isManagedProviderId(id) {
return id === PROVIDER_ID || id === DIRECT_PROVIDER_ID;
}
function providerBlockIsComplete(text, id) {
const block = extractProviderBlock(text, id);
return !!(block && /^\s*base_url\s*=/m.test(block) && /^\s*wire_api\s*=\s*["']responses["']/m.test(block));
}
function managedCompatibilityProviderIds() {
const def = config && Array.isArray(config.groups) ? config.groups.find((g) => g && g.isDefault) : null;
const id = String((def && def.modelProvider) || "").trim();
// openai 是 Codex 内置 provider;托管的两个 ID 已由固定兼容段覆盖。
// 这里只接管安全的 TOML bare key,避免历史异常值生成非法 section。
if (!id || id === "openai" || isManagedProviderId(id) || !/^[A-Za-z0-9_-]+$/.test(id)) return [];
return [id];
}
function managedProviderBlocksAreComplete(text) {
return [PROVIDER_ID, DIRECT_PROVIDER_ID, ...managedCompatibilityProviderIds()]
.every((id) => providerBlockIsComplete(text, id));
}
function removeManagedProviderSections(text, includeCompatibility = true) {
let out = removeTomlSection(text, `model_providers.${PROVIDER_ID}`);
out = removeTomlSection(out, `model_providers.${DIRECT_PROVIDER_ID}`);
if (includeCompatibility) {
for (const id of managedCompatibilityProviderIds()) {
out = removeTomlSection(out, `model_providers.${id}`);
}
}
return out;
}
function providerBlockValue(text, id, key) {
const block = extractProviderBlock(text, id);
if (!block) return "";
const esc = String(key).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const m = block.match(new RegExp(`^\\s*${esc}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, "m"));
return m ? (m[1] !== undefined ? m[1] : m[2]) : "";
}
function assertManagedConfigComplete(text) {
const provider = modelProviderFromText(text);
if (!isManagedProviderId(provider)) return;
if (managedProviderBlocksAreComplete(text)) return;
const required = [PROVIDER_ID, DIRECT_PROVIDER_ID, ...managedCompatibilityProviderIds()].join("、");
throw new Error(`拒绝写入不完整的 Codex config.toml:托管模式缺少 provider 兼容段(${required})`);
}
function configLooksManaged(text) {
const s = String(text || "");
if (!s.trim()) return false;
@@ -449,12 +490,92 @@ function configLooksManaged(text) {
return s.includes(`[model_providers.${PROVIDER_ID}]`) || s.includes(`[model_providers.${DIRECT_PROVIDER_ID}]`);
}
function codexConfigRepairPlan() {
load();
const text = readConfigToml();
const provider = modelProviderFromText(text);
if (provider === PROVIDER_ID && !managedProviderBlocksAreComplete(text)) {
return { needed: true, mode: "proxy", reason: "托管 provider 兼容段缺失或不完整" };
}
if (provider === DIRECT_PROVIDER_ID && !managedProviderBlocksAreComplete(text)) {
const direct = config.groups.find((g) => g.id === config.activeId && !g.isDefault && !isToolOnlyGroup(g) && !isClaudeGroup(g) && g.mode !== "chat" && g.key)
|| config.groups.find((g) => !g.isDefault && !isToolOnlyGroup(g) && !isClaudeGroup(g) && g.mode !== "chat" && g.key === readAuthKey());
return {
needed: true,
mode: "direct",
groupId: direct && direct.id,
reason: "托管 provider 兼容段缺失或不完整",
};
}
if (String(text || "").trim()) return { needed: false };
const authText = readTextIfExists(path.join(CODEX_HOME, "auth.json"));
let auth = {};
try { auth = JSON.parse(authText || "{}"); } catch {}
const key = auth.OPENAI_API_KEY || "";
if (!key) return { needed: false };
if (key === config.routerKey) {
return { needed: true, mode: "proxy", reason: "config.toml 为空但 auth.json 仍是本地路由 Key" };
}
const direct = config.groups.find((g) => !g.isDefault && !isToolOnlyGroup(g) && !isClaudeGroup(g) && g.mode !== "chat" && g.key === key)
|| config.groups.find((g) => g.id === config.activeId && !g.isDefault && !isToolOnlyGroup(g) && !isClaudeGroup(g) && g.mode !== "chat" && g.key);
if (direct && authLooksManaged(authText)) {
return { needed: true, mode: "direct", groupId: direct.id, reason: "config.toml 为空但 auth.json 仍是中转 Key" };
}
return { needed: false };
}
function repairCodexConfigIfNeeded() {
const plan = codexConfigRepairPlan();
if (!plan.needed) return { repaired: false };
const currentText = readConfigToml();
const currentProvider = modelProviderFromText(currentText);
if (isManagedProviderId(currentProvider) && String(currentText || "").trim()) {
const fallbackGroup = config.groups.find((g) => g.id === plan.groupId)
|| config.groups.find((g) => g.id === config.activeId);
const sourceId = providerBlockIsComplete(currentText, currentProvider)
? currentProvider
: (providerBlockIsComplete(currentText, PROVIDER_ID) ? PROVIDER_ID : DIRECT_PROVIDER_ID);
const fallbackBase = currentProvider === PROVIDER_ID
? `http://127.0.0.1:${config.port}`
: (fallbackGroup && fallbackGroup.baseUrl ? responsesProviderBase(fallbackGroup.baseUrl) : "");
const baseUrl = providerBlockValue(currentText, sourceId, "base_url") || fallbackBase;
if (!baseUrl) return { repaired: false, error: "没有可用于修复兼容 provider 的 API 地址" };
const name = providerBlockValue(currentText, sourceId, "name")
|| (fallbackGroup && fallbackGroup.name)
|| (currentProvider === PROVIDER_ID ? "本地路由" : "中转直连");
let repairedText = removeManagedProviderSections(currentText);
repairedText = repairedText.trimEnd() + "\n\n" + managedProviderBlocks(name, baseUrl);
backupConfig(currentText);
writeCodexConfigPair(repairedText, readTextForUpdate(path.join(CODEX_HOME, "auth.json")));
return {
repaired: true,
mode: currentProvider === PROVIDER_ID ? "proxy" : "direct",
reason: plan.reason,
groupId: plan.groupId,
preservedModel: currentRootModel(),
};
}
if (plan.mode === "proxy") {
applyToCodex();
return { repaired: true, mode: "proxy", reason: plan.reason };
}
if (plan.mode === "direct" && plan.groupId) {
const result = applyDirectToCodex(plan.groupId);
if (result && result.ok) return { repaired: true, mode: "direct", reason: plan.reason, groupId: plan.groupId };
return { repaired: false, error: result && result.error ? result.error : "直连分组不可用" };
}
return { repaired: false, error: "没有可用于修复的中转分组" };
}
// 当前 Codex 是否仍保留“本地路由”接管痕迹。这个状态只用于页面显示和取消按钮,
// 不把直连中转(td-direct)误显示成已启用本地路由。
function codexRouteApplied() {
try { repairCodexConfigIfNeeded(); } catch {}
const text = readConfigToml();
if (modelProviderFromText(text) === PROVIDER_ID) return true;
if (extractProviderBlock(text, PROVIDER_ID)) return true;
const provider = modelProviderFromText(text);
if (provider === PROVIDER_ID) return true;
if (provider === DIRECT_PROVIDER_ID) return false;
try {
const auth = JSON.parse(readTextIfExists(path.join(CODEX_HOME, "auth.json")) || "{}");
return auth.OPENAI_API_KEY === config.routerKey;
@@ -665,6 +786,31 @@ function catalogPath() {
return path.isAbsolute(name) ? name : path.join(CODEX_HOME, name);
}
function catalogSyncStatePath(catalogFile) {
return `${catalogFile}.td-router-state.json`;
}
function readCatalogManagedSlugs(catalogFile) {
try {
const state = JSON.parse(fs.readFileSync(catalogSyncStatePath(catalogFile), "utf-8"));
return new Set(Array.isArray(state.managedSlugs) ? state.managedSlugs.filter((slug) => typeof slug === "string") : []);
} catch {
return new Set();
}
}
function writeCatalogManagedSlugs(catalogFile, slugs) {
safeWriteFileSync(
catalogSyncStatePath(catalogFile),
JSON.stringify({ managedSlugs: [...slugs].sort() }, null, 2),
"utf-8"
);
}
function isToolOwnedCatalog(catalogFile) {
return path.resolve(catalogFile) === path.resolve(path.join(CODEX_HOME, "cc-switch-model-catalog.json"));
}
// 国产模型的实际可用窗口通常小于 Codex 模板模型。分组未填写时给出保守值,
// 让 Codex 在上游拒绝请求前预留足够空间完成自动压缩。用户填写的值始终优先
// (≥1024 即生效;过小的值多半也是有意为之,比如想强制尽早压缩)。
@@ -754,12 +900,33 @@ function applyModelCatalog(group) {
} catch {}
safeWriteFileSync(p, JSON.stringify(cat, null, 2), "utf-8");
}
// 【根治「切模型丢会话」】把分组中转站 /models 返回的全部文本模型,先登记进 Codex 模型目录。
// 关键:用户是在 Codex 本体的模型选择器里切模型的,那条路径不经过本 App,App 的 applyModelCatalog
// 不会触发;若目标 slug 不在目录里,Codex 会以未知模型启动、侧栏历史还原失败 = 会话"消失"。
// 因此必须【先回填】:把该中转能用的模型全部登记为 visibility=list,用户在 Codex 里选哪个都不丢。
// 非文本模型(画图/审阅/语音等)不是对话模型,排除;任何网络/解析失败都不动既有目录(离线安全)。
const NON_TEXT_MODEL_RE = /(image|imagine|whisper|audio|voice|tts|stt|embed|rerank|review|moderation|vision-ocr)/i;
// Codex 模型目录只公开当前分组可用的同族文本模型。中转站的 /models 往往返回
// GPT、Claude、国产模型和多媒体模型的混合库存,不能把整站库存灌进选择器。
const NON_TEXT_MODEL_RE = /(image|imagine|whisper|audio|voice|speech|tts|stt|embed|rerank|review|moderation|vision-ocr|video|seedance|veo|sora|banana|kling|hailuo)/i;
function configuredCatalogModels(group) {
const values = [
group && group.model,
...(Array.isArray(group && group.models) ? group.models : []),
...(Array.isArray(group && group.routeModels) ? group.routeModels : []),
];
return Array.from(new Set(values.filter((slug) => typeof slug === "string" && slug.trim()).map((slug) => slug.trim())));
}
function allowedCatalogModels(group, providerIds) {
const available = Array.from(new Set(
(providerIds || []).filter((slug) => typeof slug === "string" && slug.trim() && !NON_TEXT_MODEL_RE.test(slug))
));
const configured = configuredCatalogModels(group).filter((slug) => !NON_TEXT_MODEL_RE.test(slug));
const rootModel = currentRootModel();
const anchor = configured[0] || rootModel || "";
if (/^gpt-/i.test(anchor)) {
const allowed = available.filter((slug) => /^gpt-/i.test(slug));
for (const slug of [...configured, rootModel]) {
if (/^gpt-/i.test(slug) && !NON_TEXT_MODEL_RE.test(slug) && !allowed.includes(slug)) allowed.push(slug);
}
return allowed;
}
return configured;
}
async function syncCatalogFromProvider(group) {
try {
if (!group || group.isDefault || !group.baseUrl || !group.key) return { ok: false, skipped: true };
@@ -770,8 +937,8 @@ async function syncCatalogFromProvider(group) {
} catch (e) {
return { ok: false, error: (e && e.message) || String(e) }; // 网络/接口失败 → 不动既有目录
}
const textIds = ids.filter((s) => !NON_TEXT_MODEL_RE.test(s));
if (!textIds.length) return { ok: false, error: "无可登记的文本模型" };
const allowedIds = allowedCatalogModels(group, ids);
if (!allowedIds.length) return { ok: false, error: "无可登记的分组文本模型" };
const p = catalogPath();
let cat;
try {
@@ -788,7 +955,18 @@ async function syncCatalogFromProvider(group) {
let added = 0;
let revealed = 0;
let refreshed = 0;
for (const slug of textIds) {
let hidden = 0;
const allowed = new Set(allowedIds);
const managedSlugs = readCatalogManagedSlugs(p);
const hideAllStale = isToolOwnedCatalog(p);
for (const model of cat.models) {
if (!model || allowed.has(model.slug) || model.visibility !== "list") continue;
if (hideAllStale || managedSlugs.has(model.slug)) {
model.visibility = "hide";
hidden++;
}
}
for (const slug of allowedIds) {
const ex = cat.models.find((m) => m.slug === slug);
if (ex) {
if (ex.visibility !== "list") { ex.visibility = "list"; revealed++; }
@@ -812,11 +990,13 @@ async function syncCatalogFromProvider(group) {
cat.models.push(t);
added++;
}
if (added || revealed || refreshed) {
for (const slug of allowedIds) managedSlugs.add(slug);
if (added || revealed || refreshed || hidden) {
try { fs.writeFileSync(p + ".bak-router", fs.readFileSync(p)); } catch {}
safeWriteFileSync(p, JSON.stringify(cat, null, 2), "utf-8");
}
return { ok: true, added, revealed, refreshed, total: textIds.length };
writeCatalogManagedSlugs(p, managedSlugs);
return { ok: true, added, revealed, refreshed, hidden, total: allowedIds.length };
} catch (e) {
return { ok: false, error: (e && e.message) || String(e) };
}
@@ -875,6 +1055,22 @@ function setRootModelInText(text, model) {
return `model = "${model}"\n${text}`;
}
function setRootTomlValueInText(text, key, value) {
const rendered = typeof value === "boolean" ? String(value) : JSON.stringify(String(value));
const lines = text.split("\n");
const firstSection = lines.findIndex((line) => /^\s*\[/.test(line));
const rootEnd = firstSection === -1 ? lines.length : firstSection;
const existingIndex = lines.slice(0, rootEnd).findIndex(
(line) => line.trim().startsWith(`${key}=`) || line.trim().startsWith(`${key} =`)
);
if (existingIndex >= 0) {
lines[existingIndex] = `${key} = ${rendered}`;
} else {
lines.splice(rootEnd, 0, `${key} = ${rendered}`);
}
return lines.join("\n");
}
// 恢复到默认(把 config.toml/auth.json 还原成客户原始配置,Codex 直连自己账号)
function restoreDirect() {
const def = config.groups.find((g) => g.isDefault);
@@ -1019,6 +1215,26 @@ function knownModelsForBase(baseUrl) {
return [];
}
// Codex Responses provider 的 base_url 语义是“后续自动追加 /responses”。
// 模型列表探测允许用户填站点根(自动试 /v1/models),因此直连与路由转发也必须
// 把站点根规范到 /v1;已填 /v1、/v2 或完整 /responses 地址时不重复改写。
function responsesProviderBase(baseUrl) {
const base = String(baseUrl || "").replace(/\/+$/, "");
try {
const u = new URL(base);
const pathname = u.pathname.replace(/\/+$/, "");
if (/\/responses$/i.test(pathname)) {
u.pathname = pathname.replace(/\/responses$/i, "");
return u.href.replace(/\/+$/, "");
}
if (/\/v\d+$/i.test(pathname)) return base;
u.pathname = pathname === "/" ? "/v1" : `${pathname}/v1`;
return u.href.replace(/\/+$/, "");
} catch {
return base;
}
}
// 拉取某个中转 API 的模型列表(试 /v1/models 再 /models)
async function fetchModels(baseUrl, key) {
if (!baseUrl) throw new Error("请先填 API 地址");
@@ -1294,7 +1510,16 @@ function handle(req, res) {
let target;
try {
target = new URL(g.baseUrl.replace(/\/$/, "") + req.url);
const openAIUpstream = g.mode !== "chat" && !isClaudeGroup(g);
// Codex 的 provider base_url 可能带 /v1;本地路由自身统一挂在根路径。
// 先去掉入口里的版本段,再拼上游 base,避免 /v1/v1/responses。
const upstreamPath = openAIUpstream && /^\/v\d+\/responses\b/i.test(req.url || "")
? req.url.replace(/^\/v\d+/i, "")
: req.url;
const upstreamBase = !openAIUpstream
? g.baseUrl.replace(/\/$/, "")
: responsesProviderBase(g.baseUrl);
target = new URL(upstreamBase + upstreamPath);
} catch {
res.writeHead(502);
res.end("分组 API 地址无效");
@@ -1367,6 +1592,7 @@ function handle(req, res) {
function start() {
load();
try { repairCodexConfigIfNeeded(); } catch {}
if (server) return { running: true, port: config.port };
return new Promise((resolve, reject) => {
server = http.createServer(handle);
@@ -1400,6 +1626,10 @@ function releaseCodexTakeover() {
text = removeTomlSection(text, `model_providers.${PROVIDER_ID}`);
text = removeTomlSection(text, `model_providers.${DIRECT_PROVIDER_ID}`);
if (managedProvider) {
// 托管期间为旧会话补的 provider 别名也要一起清理;用户已手改 provider 时则完全保留。
for (const id of managedCompatibilityProviderIds()) {
text = removeTomlSection(text, `model_providers.${id}`);
}
// 根级还指向本工具,才删根级 model_provider 和根级 model;否则不碰用户配置。
text = text.replace(/^\s*model_provider\s*=.*$\n?/m, "");
text = text.replace(/^\s*model\s*=.*$\n?/m, "");
@@ -1446,6 +1676,8 @@ function isRunning() {
function status() {
load();
let repair = { repaired: false };
try { repair = repairCodexConfigIfNeeded(); } catch (e) { repair = { repaired: false, error: String((e && e.message) || e) }; }
return {
running: isRunning(),
port: config.port,
@@ -1453,6 +1685,7 @@ function status() {
address: `http://127.0.0.1:${config.port}`,
activeId: config.activeId,
activeClaudeId: config.activeClaudeId,
repair,
claudeApplied: claudeApplied(),
codexApplied: codexRouteApplied(),
groups: config.groups.map((g) => ({
@@ -1470,12 +1703,15 @@ function status() {
sttModel: g.sttModel || "",
multimodal: !!g.multimodal,
oneMillionModels: Array.isArray(g.oneMillionModels) ? g.oneMillionModels : [],
source: g.source || "",
sourceGroupId: g.sourceGroupId || "",
})),
};
}
function defaultConfigInfo() {
load();
try { repairCodexConfigIfNeeded(); } catch {}
const def = config.groups.find((g) => g.isDefault) || null;
if (def && snapshotOfficialLogin(def)) save();
const currentConfigToml = readConfigToml();
@@ -1626,6 +1862,24 @@ function upsertGroup(g) {
}
return st;
}
function importSub2apiProvider(input) {
load();
const existing = config.groups.find(
(g) => !g.isDefault && g.source === "sub2api" && String(g.sourceGroupId) === String(input.sourceGroupId)
);
const result = upsertGroup({
...(existing ? { id: existing.id } : {}),
name: input.name,
baseUrl: input.endpoint,
key: input.apiKey,
model: input.model,
mode: "responses",
source: "sub2api",
sourceGroupId: String(input.sourceGroupId),
});
return { ...result, _imported: true, _updated: !!existing };
}
function deleteGroup(id) {
load();
const g = config.groups.find((x) => x.id === id);
@@ -1745,7 +1999,26 @@ function setPort(port) {
// —— 自动配置 Codex —— 写 config.toml 的 local-router provider + auth.json 的 routerKey
function codexSnippet() {
load();
return `model_provider = "${PROVIDER_ID}"\n\n[model_providers.${PROVIDER_ID}]\nname = "本地路由"\nbase_url = "http://127.0.0.1:${config.port}"\nwire_api = "responses"\nrequires_openai_auth = true`;
const base = `http://127.0.0.1:${config.port}`;
return `model_provider = "${PROVIDER_ID}"\n\n${managedProviderBlocks("本地路由", base).trimEnd()}`;
}
function managedProviderBlocks(name, baseUrl) {
const providerName = JSON.stringify(String(name || "TD 工具箱"));
const compatibilityName = JSON.stringify(`${String(name || "TD 工具箱")}(历史会话兼容)`);
const base = JSON.stringify(String(baseUrl || "").replace(/\/+$/, ""));
const blocks = [];
for (const id of [PROVIDER_ID, DIRECT_PROVIDER_ID, ...managedCompatibilityProviderIds()]) {
blocks.push(
`[model_providers.${id}]`,
`name = ${id === PROVIDER_ID || id === DIRECT_PROVIDER_ID ? providerName : compatibilityName}`,
`base_url = ${base}`,
'wire_api = "responses"',
"requires_openai_auth = true",
""
);
}
return blocks.join("\n");
}
function applyToCodex() {
@@ -1775,9 +2048,10 @@ function applyToCodex() {
if (def && def.modelProvider && def.modelProvider !== PROVIDER_ID) {
text = removeTomlSection(text, `model_providers.${def.modelProvider}`);
}
// 替换/插入 [model_providers.local-router] 段:先删旧的(含被 bug 弄重复的),再追加干净的
const block = `[model_providers.${PROVIDER_ID}]\nname = "本地路由"\nbase_url = "http://127.0.0.1:${config.port}"\nwire_api = "responses"\nrequires_openai_auth = true\n`;
text = removeTomlSection(text, `model_providers.${PROVIDER_ID}`);
// 托管 provider 与历史默认 provider 都保留完整兼容段。历史线程可能记录任意一个 ID,
// 恢复时会按线程自己的 provider 查配置,缺一个就会一直停在 loading。
const block = managedProviderBlocks("本地路由", `http://127.0.0.1:${config.port}`);
text = removeManagedProviderSections(text);
text = text.trimEnd() + "\n\n" + block;
// auth.json 写 routerKey(保留其它字段),Codex 会把它当 Bearer 发到本地路由
@@ -1793,8 +2067,7 @@ function applyToCodex() {
}
text = ensureCatalogLinked(text);
writeCodexConfigPair(text, JSON.stringify(auth, null, 2));
// 先回填该中转站的全部文本模型进目录(异步、不阻塞、失败不动目录),
// 保证用户之后在 Codex 本体里切到任何模型都不会因"模型不在目录"而丢会话。
// 收敛为当前分组的同族文本模型,避免把中转站全量库存灌进 Codex。
if (cur2 && !cur2.isDefault) syncCatalogFromProvider(cur2).catch(() => {});
save();
return { ok: true };
@@ -1835,8 +2108,7 @@ function applyDirectToCodex(groupId) {
backupConfig(text);
// 清理其他接管痕迹:本地路由段、上次直连段、默认分组原始 provider 段
text = removeTomlSection(text, `model_providers.${PROVIDER_ID}`);
text = removeTomlSection(text, `model_providers.${DIRECT_PROVIDER_ID}`);
text = removeManagedProviderSections(text);
if (def && def.modelProvider && def.modelProvider !== DIRECT_PROVIDER_ID) {
text = removeTomlSection(text, `model_providers.${def.modelProvider}`);
}
@@ -1846,9 +2118,11 @@ function applyDirectToCodex(groupId) {
} else {
text = `model_provider = "${DIRECT_PROVIDER_ID}"\n` + text;
}
// 追加 [model_providers.td-direct] 段:base_url = 分组真实中转地址(非 127.0.0.1)
const base = g.baseUrl.replace(/\/+$/, "");
const block = `[model_providers.${DIRECT_PROVIDER_ID}]\nname = "${(g.name || "中转直连").replace(/"/g, "")}"\nbase_url = "${base}"\nwire_api = "responses"\nrequires_openai_auth = true\n`;
// 追加托管与历史 provider 兼容段:base_url 都是分组真实中转地址(非 127.0.0.1)
const base = responsesProviderBase(g.baseUrl);
const block = managedProviderBlocks(g.name || "中转直连", base);
text = setRootTomlValueInText(text, "model_reasoning_effort", "high");
text = setRootTomlValueInText(text, "disable_response_storage", true);
text = text.trimEnd() + "\n\n" + block;
text = text.replace(/\n{3,}/g, "\n\n");
@@ -1865,9 +2139,7 @@ function applyDirectToCodex(groupId) {
}
text = ensureCatalogLinked(text);
writeCodexConfigPair(text, JSON.stringify(auth, null, 2));
// 先回填:把该中转站能用的全部文本模型登记进 Codex 模型目录(异步、不阻塞保存、失败不动目录)。
// 这样即便用户之后在 Codex 本体的模型选择器里切到别的模型(那条路径不经过本 App),
// 目标模型也已在目录里 → Codex 能正常启动、侧栏历史不再"消失"。
// 异步收敛当前分组模型目录;网络失败时保持现状。
syncCatalogFromProvider(g).catch(() => {});
config.activeId = g.id;
save();
@@ -2015,6 +2287,7 @@ module.exports = {
status,
defaultConfigInfo,
upsertGroup,
importSub2apiProvider,
deleteGroup,
setActive,
setPort,