From bd5da4c265ff84caa4347d5aa970ba7f3ed418e2 Mon Sep 17 00:00:00 2001 From: gaohu Date: Tue, 18 Aug 2026 20:39:57 +0800 Subject: [PATCH 1/6] =?UTF-8?q?fix(codex):=20=E4=BF=9D=E7=95=99=E5=88=97?= =?UTF-8?q?=E5=8F=96=E6=B6=88=E5=8B=BE=E9=80=89=E6=94=B9=E4=B8=BA=E5=81=9C?= =?UTF-8?q?=E7=94=A8=E6=A8=A1=E5=9E=8B=E5=B9=B6=E9=9A=90=E8=97=8F=E5=81=9C?= =?UTF-8?q?=E7=94=A8=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 保留列取消勾选改为 enabled:false;保存保留停用行;默认模型/目录/Desktop 投影、模型排序、子 Agent 候选、向导模型源、MultiRouter 同步均排除停用模型;新增相关回归测试。 --- src-tauri/src/codex_config.rs | 39 +++++++ src-tauri/src/codex_desktop.rs | 7 ++ .../codex/CodexRouterWorkspacePage.tsx | 12 +- .../providers/forms/CodexFormFields.tsx | 36 ++++-- .../providers/forms/ProviderForm.tsx | 37 ++++-- .../forms/hooks/useCodexConfigState.ts | 3 + src/lib/codexMultiRouterSync.ts | 8 +- src/lib/codexMultiRouterWizard.ts | 35 ++++-- src/types.ts | 3 + src/utils/codexSpawnAgentCandidates.ts | 32 ++++-- .../CodexFormFields.keepColumn.test.tsx | 105 ++++++++++++++++++ .../ProviderForm.codexCatalog.test.ts | 12 ++ tests/lib/codexMultiRouterSync.test.ts | 50 +++++++++ tests/utils/codexSpawnAgentCandidates.test.ts | 26 +++++ 14 files changed, 364 insertions(+), 41 deletions(-) create mode 100644 tests/components/CodexFormFields.keepColumn.test.tsx diff --git a/src-tauri/src/codex_config.rs b/src-tauri/src/codex_config.rs index 01310f747..324361775 100644 --- a/src-tauri/src/codex_config.rs +++ b/src-tauri/src/codex_config.rs @@ -1543,6 +1543,14 @@ fn codex_catalog_model_specs(settings: &Value, config_text: &str) -> Vec (Vec, Vec< let mut projected = Vec::new(); for entry in entries { + if entry + .get("enabled") + .and_then(Value::as_bool) + == Some(false) + { + continue; + } let Some(model_name) = codex_model_name(entry) else { continue; }; diff --git a/src/components/codex/CodexRouterWorkspacePage.tsx b/src/components/codex/CodexRouterWorkspacePage.tsx index 7b87e24bf..0296784d3 100644 --- a/src/components/codex/CodexRouterWorkspacePage.tsx +++ b/src/components/codex/CodexRouterWorkspacePage.tsx @@ -99,6 +99,7 @@ import { normalizeCodexSpawnAgentModels, normalizeSpawnAgentCandidateSelection, readCodexModelCatalog, + readRawCodexModelCatalogModels, reorderSpawnAgentCandidates, validateSpawnAgentCandidates, type CodexCatalogModel, @@ -450,6 +451,7 @@ type ProxyListenDraftValidation = type CodexCatalogModelDraft = { model: string; + enabled?: boolean; upstreamModel?: string; upstream_model?: string; displayName?: string; @@ -691,7 +693,11 @@ function providerWithFetchedModelCatalog( provider: Provider, fetchedModels: FetchedModel[], ): Provider { - const currentCatalog = readCodexModelCatalog(provider); + const visibleCatalog = readCodexModelCatalog(provider); + const currentCatalog = { + models: readRawCodexModelCatalogModels(provider), + spawnAgentModels: visibleCatalog.spawnAgentModels, + }; const fetchConfig = getProviderModelFetchConfig(provider); const models = currentCatalog.models.map((model) => { const id = model.model?.trim(); @@ -719,6 +725,7 @@ function providerWithFetchedModelCatalog( : {}), ...(model.vision !== undefined ? { vision: model.vision } : {}), ...(model.sortIndex !== undefined ? { sortIndex: model.sortIndex } : {}), + ...(model.enabled !== undefined ? { enabled: model.enabled } : {}), // 模型目录刷新必须保留已有 reasoning 声明(用户手动声明的档位/能力)。 // 否则 /models 拉取重建会把声明清空,导致档位消失(K3/Qwen 均受影响)。 ...(model.reasoning ? { reasoning: model.reasoning } : {}), @@ -802,7 +809,7 @@ function providerWithFetchedModelCatalog( models, spawnAgentModels: normalizeCodexSpawnAgentModels( currentCatalog.spawnAgentModels, - models, + models.filter((model) => model.enabled !== false), ), }, }, @@ -1292,6 +1299,7 @@ function catalogDraftFromSourceModel( ); return { model: id, + ...(source?.enabled !== undefined ? { enabled: source.enabled } : {}), ...(upstreamModel && upstreamModel !== id ? { upstreamModel } : {}), ...(displayName ? { displayName } : {}), ...(contextWindow ? { contextWindow } : {}), diff --git a/src/components/providers/forms/CodexFormFields.tsx b/src/components/providers/forms/CodexFormFields.tsx index fa9fd7525..f2730a1ec 100644 --- a/src/components/providers/forms/CodexFormFields.tsx +++ b/src/components/providers/forms/CodexFormFields.tsx @@ -419,6 +419,7 @@ function createCatalogRow(seed?: Partial): CodexCatalogRow { return { rowId: crypto.randomUUID(), model: seed?.model ?? "", + ...(seed?.enabled !== undefined ? { enabled: seed.enabled } : {}), upstreamModel: seed?.upstreamModel ?? seed?.upstream_model ?? "", displayName: seed?.displayName ?? "", contextWindow: seed?.contextWindow ?? "", @@ -515,6 +516,7 @@ function catalogRowsMatchModels( Pick< CodexCatalogRow, | "model" + | "enabled" | "upstreamModel" | "upstream_model" | "displayName" @@ -533,6 +535,7 @@ function catalogRowsMatchModels( const incoming = models[i]; return ( row.model === (incoming.model ?? "") && + (row.enabled ?? true) === (incoming.enabled ?? true) && catalogRowUpstreamModel(row) === catalogRowUpstreamModel(incoming) && (row.displayName ?? "") === (incoming.displayName ?? "") && String(row.contextWindow ?? "") === @@ -1213,8 +1216,8 @@ export function CodexFormFields({ const planModelListAction = codexPlanModelListAction(planFetchSource); const isCatalogOnlyPlan = isCodexCatalogOnlyPlanModelFetch(planFetchSource); if (isCatalogOnlyPlan) { - const hasModelCatalog = catalogRowsRef.current.some((row) => - row.model.trim(), + const hasModelCatalog = catalogRowsRef.current.some( + (row) => row.enabled !== false && row.model.trim(), ); const message = codexCatalogOnlyPlanModelFetchMessage( hasModelCatalog, @@ -1337,7 +1340,9 @@ export function CodexFormFields({ const models = Array.from( new Set( [ - ...catalogRowsRef.current.map((row) => catalogRowUpstreamModel(row)), + ...catalogRowsRef.current + .filter((row) => row.enabled !== false) + .map((row) => catalogRowUpstreamModel(row)), ...fetchedModels.map((model) => model.id.trim()), ].filter(Boolean), ), @@ -2782,27 +2787,34 @@ export function CodexFormFields({ return (
0 ? { contextWindow } : {}), @@ -280,6 +281,7 @@ const normalizeCodexSpawnAgentModelsForSave = ( catalogModels: CodexCatalogModel[], ): string[] => { const catalogModelIds = catalogModels + .filter((item) => item.enabled !== false) .map((item) => item.model.trim()) .filter(Boolean); const availableModels = new Set(catalogModelIds); @@ -1635,17 +1637,36 @@ function ProviderFormFull({ normalizedCatalogModels, ) : []; + const enabledCatalogModels = normalizedCatalogModels.filter( + (item) => item.enabled !== false, + ); + const currentDefaultModel = extractCodexModelName( + normalizedCodexConfig, + )?.trim(); // The default-model field writes the top-level `model` into the TOML // as the user types; only when it was left empty fall back to the - // first catalog row so "fill mapping only" keeps its old behavior. - if ( - normalizedCatalogModels.length > 0 && - !extractCodexModelName(normalizedCodexConfig) - ) { - normalizedCodexConfig = setCodexModelNameInConfig( - normalizedCodexConfig, - normalizedCatalogModels[0].model, + // first enabled catalog row so "fill mapping only" keeps its old + // behavior. A model disabled in the catalog is never kept as default. + if (enabledCatalogModels.length > 0) { + const firstEnabledModel = enabledCatalogModels[0].model; + const defaultModelDisabled = normalizedCatalogModels.some( + (item) => + item.enabled === false && item.model === currentDefaultModel, ); + if (!currentDefaultModel) { + normalizedCodexConfig = setCodexModelNameInConfig( + normalizedCodexConfig, + firstEnabledModel, + ); + } else if (defaultModelDisabled) { + normalizedCodexConfig = setCodexModelNameInConfig( + normalizedCodexConfig, + firstEnabledModel, + ); + toast.info( + `默认模型 ${currentDefaultModel} 已停用,已自动改用 ${firstEnabledModel}。`, + ); + } } const configObj = { auth: authJson, diff --git a/src/components/providers/forms/hooks/useCodexConfigState.ts b/src/components/providers/forms/hooks/useCodexConfigState.ts index d23cd4129..eaf8df157 100644 --- a/src/components/providers/forms/hooks/useCodexConfigState.ts +++ b/src/components/providers/forms/hooks/useCodexConfigState.ts @@ -204,9 +204,12 @@ function extractCodexCatalogModels(modelCatalog: any): CodexCatalogModel[] { item?.reasoning && typeof item.reasoning === "object" ? item.reasoning : undefined; + const enabled = + typeof item?.enabled === "boolean" ? item.enabled : undefined; return { model: typeof item?.model === "string" ? item.model : "", + ...(enabled !== undefined ? { enabled } : {}), ...(upstreamModel ? { upstreamModel } : {}), ...(displayName ? { displayName } : {}), ...(contextWindow ? { contextWindow } : {}), diff --git a/src/lib/codexMultiRouterSync.ts b/src/lib/codexMultiRouterSync.ts index 2d0bf1484..3188df9f4 100644 --- a/src/lib/codexMultiRouterSync.ts +++ b/src/lib/codexMultiRouterSync.ts @@ -11,7 +11,7 @@ import { readWizardCodexOAuthAccountId, resolveWizardModelNameCollisions, } from "@/lib/codexMultiRouterWizard"; -import { readCodexModelCatalog } from "@/utils/codexSpawnAgentCandidates"; +import { readRawCodexModelCatalogModels } from "@/utils/codexSpawnAgentCandidates"; // MultiRouter 同步返回写回后的 plan,以及需要用户人工补选的子 Agent 候选删减。 export interface CodexMultiRouterPlanSyncResult { @@ -124,8 +124,8 @@ function catalogModelUpstreamId(model: CodexCatalogModel): string { function readStrictProviderCatalogModels( provider: Provider, ): CodexCatalogModel[] { - return readCodexModelCatalog(provider) - .models.map((model) => { + return readRawCodexModelCatalogModels(provider) + .map((model) => { const id = model.model?.trim(); if (!id) return null; return { @@ -158,6 +158,7 @@ function readStrictProviderCatalogModels( ? { supports_image: model.supports_image } : {}), ...(model.vision !== undefined ? { vision: model.vision } : {}), + ...(model.enabled !== undefined ? { enabled: model.enabled } : {}), ...(model.reasoning ? { reasoning: model.reasoning } : {}), } satisfies CodexCatalogModel; }) @@ -225,6 +226,7 @@ function buildSyncedRouteModels( planCatalogByModel, ); return targetModels + .filter((sourceModel) => sourceModel.enabled !== false) .map((sourceModel) => { const upstream = catalogModelUpstreamId(sourceModel); const existingVisible = visibleByUpstream.get(upstream); diff --git a/src/lib/codexMultiRouterWizard.ts b/src/lib/codexMultiRouterWizard.ts index 901f17113..53385bd7f 100644 --- a/src/lib/codexMultiRouterWizard.ts +++ b/src/lib/codexMultiRouterWizard.ts @@ -194,18 +194,31 @@ function isWizardNativeCodexAuthSource(provider: Provider): boolean { // 读取 Codex provider 的真实持久化模型目录;缺失或结构异常时返回空目录,不能伪造 OAuth 模型权限。 export function readWizardModelCatalog( provider: Provider, + options: { enabledOnly?: boolean } = { enabledOnly: true }, ): CodexCatalogModel[] { const models = provider.settingsConfig?.modelCatalog?.models; if (!Array.isArray(models)) { return []; } - return models.filter( - (model): model is CodexCatalogModel => - typeof model === "object" && - model !== null && - typeof (model as CodexCatalogModel).model === "string" && - Boolean((model as CodexCatalogModel).model.trim()), - ); + return models + .filter( + (model): model is CodexCatalogModel => + typeof model === "object" && + model !== null && + typeof (model as CodexCatalogModel).model === "string" && + Boolean((model as CodexCatalogModel).model.trim()), + ) + .filter( + (model) => + (options.enabledOnly ?? true) === false || model.enabled !== false, + ); +} + +// 持久化/合并路径需要保留停用行;这些路径单独读取原始目录,避免刷新后丢失用户保留的停用模型。 +export function readRawWizardModelCatalog( + provider: Provider, +): CodexCatalogModel[] { + return readWizardModelCatalog(provider, { enabledOnly: false }); } // 判断 provider 是否是 MultiRouter 方案;向导只把普通 provider 当作上游模型源。 @@ -396,7 +409,7 @@ export function mergeFetchedModelsIntoWizardProvider( fetchedModels: FetchedModel[], options: MergeFetchedWizardModelsOptions = {}, ): Provider { - const existingModels = readWizardModelCatalog(provider); + const existingModels = readRawWizardModelCatalog(provider); const byModel = new Map(); const byFetchedModel = new Map(); for (const model of existingModels) { @@ -448,7 +461,11 @@ export function mergeFetchedModelsIntoWizardProvider( }); } const models = Array.from(byModel.values()); - const allowedModels = new Set(models.map((model) => model.model)); + const allowedModels = new Set( + models + .filter((model) => model.enabled !== false) + .map((model) => model.model), + ); const rawSpawnAgentModels = provider.settingsConfig?.modelCatalog?.spawnAgentModels; const spawnAgentModels = Array.isArray(rawSpawnAgentModels) diff --git a/src/types.ts b/src/types.ts index c038aa2e7..0a7e6b692 100644 --- a/src/types.ts +++ b/src/types.ts @@ -315,6 +315,9 @@ export interface CodexModelReasoningCapability { export interface CodexCatalogModel { model: string; + // false keeps the catalog row for later reuse but excludes it from Codex + // menu/catalog projection; missing means enabled. + enabled?: boolean; upstreamModel?: string; upstream_model?: string; displayName?: string; diff --git a/src/utils/codexSpawnAgentCandidates.ts b/src/utils/codexSpawnAgentCandidates.ts index ee212997b..fbc75e969 100644 --- a/src/utils/codexSpawnAgentCandidates.ts +++ b/src/utils/codexSpawnAgentCandidates.ts @@ -57,30 +57,41 @@ export function normalizeCodexSpawnAgentModels( return normalized; } -// 读取 MultiRouter provider 私有配置中的模型目录和 spawn_agent 候选顺序。 -export function readCodexModelCatalog( +function readCodexCatalogModels( provider: Pick | null, -): CodexModelCatalog { + enabledOnly: boolean, +): CodexCatalogModel[] { const catalog = provider?.settingsConfig?.modelCatalog; if (!catalog || typeof catalog !== "object") { - return { models: [], spawnAgentModels: [] }; + return []; } const catalogObject = catalog as Record; const rawModels: unknown[] = Array.isArray(catalogObject.models) ? catalogObject.models : []; - const models = rawModels + return rawModels .filter( (item: unknown): item is CodexCatalogModel => !!item && typeof item === "object", ) + .filter((item) => !enabledOnly || item.enabled !== false) .filter((item) => typeof item.model === "string" && item.model.trim()); +} + +// 读取 MultiRouter provider 私有配置中的模型目录和 spawn_agent 候选顺序。 +// 默认只返回启用的模型,避免停用行泄漏到模型排序、候选选择等界面。 +export function readCodexModelCatalog( + provider: Pick | null, +): CodexModelCatalog { + const models = readCodexCatalogModels(provider, true); + const catalog = provider?.settingsConfig?.modelCatalog; + const catalogObject = catalog as Record | undefined; const rawSpawnAgentModels: unknown[] = Array.isArray( - catalogObject.spawnAgentModels, + catalogObject?.spawnAgentModels, ) ? catalogObject.spawnAgentModels - : Array.isArray(catalogObject.spawn_agent_models) + : Array.isArray(catalogObject?.spawn_agent_models) ? catalogObject.spawn_agent_models : []; const spawnAgentModels = rawSpawnAgentModels @@ -94,6 +105,13 @@ export function readCodexModelCatalog( }; } +// 持久化和同步路径需要完整保留停用行,供用户后续重新启用;这些路径不应复用上面的展示过滤。 +export function readRawCodexModelCatalogModels( + provider: Pick | null, +): CodexCatalogModel[] { + return readCodexCatalogModels(provider, false); +} + // 展示模型名称时优先使用 catalog 的 displayName,保留 slug 方便用户复制。 export function catalogModelLabel(model: CodexCatalogModel): string { const id = model.model?.trim() ?? ""; diff --git a/tests/components/CodexFormFields.keepColumn.test.tsx b/tests/components/CodexFormFields.keepColumn.test.tsx new file mode 100644 index 000000000..6f1ee4121 --- /dev/null +++ b/tests/components/CodexFormFields.keepColumn.test.tsx @@ -0,0 +1,105 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import type { PropsWithChildren } from "react"; +import { useForm } from "react-hook-form"; +import { describe, expect, it, vi } from "vitest"; +import { CodexFormFields } from "@/components/providers/forms/CodexFormFields"; +import { Form } from "@/components/ui/form"; +import type { CodexCatalogModel } from "@/types"; + +const FormShell = ({ children }: PropsWithChildren) => { + const form = useForm(); + + return
{children}
; +}; + +function renderCodexFormFields(catalogModels: CodexCatalogModel[]) { + const onCatalogModelsChange = vi.fn(); + render( + + + , + ); + return { onCatalogModelsChange }; +} + +describe("CodexFormFields keep column", () => { + it("keeps the catalog row and disables the model when keep is unchecked", async () => { + const { onCatalogModelsChange } = renderCodexFormFields([ + { model: "deepseek-v4-flash" }, + ]); + + fireEvent.click(screen.getByRole("button", { name: "高级选项" })); + + const keepCheckbox = screen.getByRole("checkbox", { + name: "保留 deepseek-v4-flash", + }); + expect(keepCheckbox).toBeChecked(); + + fireEvent.click(keepCheckbox); + + await waitFor(() => { + expect(keepCheckbox).not.toBeChecked(); + expect(screen.getByText("未使用")).toBeInTheDocument(); + }); + expect( + screen.getAllByDisplayValue("deepseek-v4-flash").length, + ).toBeGreaterThan(0); + expect(onCatalogModelsChange).toHaveBeenCalledWith([ + expect.objectContaining({ + model: "deepseek-v4-flash", + enabled: false, + }), + ]); + }); + + it("re-enables the model when the kept row is checked again", async () => { + const { onCatalogModelsChange } = renderCodexFormFields([ + { model: "deepseek-v4-flash", enabled: false }, + ]); + + fireEvent.click(screen.getByRole("button", { name: "高级选项" })); + + const keepCheckbox = screen.getByRole("checkbox", { + name: "保留 deepseek-v4-flash", + }); + expect(keepCheckbox).not.toBeChecked(); + + fireEvent.click(keepCheckbox); + + await waitFor(() => { + expect(keepCheckbox).toBeChecked(); + expect(screen.queryByText("未使用")).not.toBeInTheDocument(); + }); + expect(onCatalogModelsChange).toHaveBeenLastCalledWith([ + expect.objectContaining({ + model: "deepseek-v4-flash", + enabled: true, + }), + ]); + }); +}); diff --git a/tests/components/ProviderForm.codexCatalog.test.ts b/tests/components/ProviderForm.codexCatalog.test.ts index 9f730d9e0..7dc54b4db 100644 --- a/tests/components/ProviderForm.codexCatalog.test.ts +++ b/tests/components/ProviderForm.codexCatalog.test.ts @@ -152,4 +152,16 @@ describe("ProviderForm Codex catalog helpers", () => { { model: "text-explicit", supportsImage: false }, ]); }); + + it("preserves enabled=false while omitting the implicit enabled default", () => { + expect( + normalizeCodexCatalogModelsForSave([ + { model: "deepseek-v4-flash", enabled: false }, + { model: "kimi-k2", enabled: true }, + ]), + ).toEqual([ + { model: "deepseek-v4-flash", enabled: false }, + { model: "kimi-k2" }, + ]); + }); }); diff --git a/tests/lib/codexMultiRouterSync.test.ts b/tests/lib/codexMultiRouterSync.test.ts index 183a50189..df15d7d04 100644 --- a/tests/lib/codexMultiRouterSync.test.ts +++ b/tests/lib/codexMultiRouterSync.test.ts @@ -130,6 +130,56 @@ describe("codexMultiRouterSync", () => { ]); }); + it("同步时排除 provider 目录中停用的模型", () => { + const target = provider({ + id: "qwen", + settingsConfig: { + modelCatalog: { + models: [ + { model: "qwen3.6" }, + { model: "disabled-model", enabled: false }, + ], + }, + }, + }); + const plan = provider({ + id: "router", + settingsConfig: { + modelCatalog: { + models: [{ model: "qwen3.6" }, { model: "disabled-model" }], + }, + codexRouting: { + enabled: true, + routes: [ + { + id: "qwen-route", + targetProviderId: target.id, + match: { models: ["qwen3.6", "disabled-model"] }, + upstream: { apiFormat: "openai_chat" }, + }, + ], + }, + }, + }); + + const synced = syncCodexMultiRouterPlanWithProviders( + plan, + new Map([ + [target.id, target], + [plan.id, plan], + ]), + ); + + expect( + synced?.plan.settingsConfig.codexRouting.routes[0].match.models, + ).toEqual(["qwen3.6"]); + expect( + synced?.plan.settingsConfig.modelCatalog.models.map( + (model: { model: string }) => model.model, + ), + ).toEqual(["qwen3.6"]); + }); + it("把旧版内联默认 OAuth route 迁移到 canonical provider 并同步 5.6", () => { const official = provider({ id: "codex-official", diff --git a/tests/utils/codexSpawnAgentCandidates.test.ts b/tests/utils/codexSpawnAgentCandidates.test.ts index 93dfcdbfc..646135483 100644 --- a/tests/utils/codexSpawnAgentCandidates.test.ts +++ b/tests/utils/codexSpawnAgentCandidates.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { normalizeSpawnAgentCandidateSelection, readCodexModelCatalog, + readRawCodexModelCatalogModels, reorderSpawnAgentCandidates, validateSpawnAgentCandidates, type CodexCatalogModel, @@ -67,6 +68,31 @@ describe("codexSpawnAgentCandidates", () => { ]); }); + it("读取模型目录时过滤停用模型,但保留原始读取用于持久化", () => { + const provider = providerWithModelCatalog({ + models: [ + { model: "qwen3.6" }, + { model: "deepseek-v4-flash", enabled: false }, + { model: "deepseek-v4-pro", enabled: true }, + ], + spawnAgentModels: ["qwen3.6", "deepseek-v4-flash"], + }); + + const visible = readCodexModelCatalog(provider); + expect(visible.models.map((model) => model.model)).toEqual([ + "qwen3.6", + "deepseek-v4-pro", + ]); + expect(visible.spawnAgentModels).toEqual([ + "qwen3.6", + "deepseek-v4-pro", + ]); + + expect( + readRawCodexModelCatalogModels(provider).map((model) => model.model), + ).toEqual(["qwen3.6", "deepseek-v4-flash", "deepseek-v4-pro"]); + }); + it("规整选择时去掉未知模型和重复模型", () => { expect( normalizeSpawnAgentCandidateSelection( From 2fc8d56d8d24ab11ad031fd68922e5b0a5be2911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=8B=E6=96=97=E7=9A=84=E5=B0=8F=E9=AB=98?= Date: Tue, 18 Aug 2026 20:57:09 +0800 Subject: [PATCH 2/6] style(codex): format backend code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cargo fmt 修正 codex_desktop.rs,修复 CI Backend Checks 的格式检查。 --- src-tauri/src/codex_desktop.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src-tauri/src/codex_desktop.rs b/src-tauri/src/codex_desktop.rs index 8110861d0..fb1d0ad71 100644 --- a/src-tauri/src/codex_desktop.rs +++ b/src-tauri/src/codex_desktop.rs @@ -277,11 +277,7 @@ fn codex_model_entries_from_catalog_value(catalog: &Value) -> (Vec, Vec< let mut projected = Vec::new(); for entry in entries { - if entry - .get("enabled") - .and_then(Value::as_bool) - == Some(false) - { + if entry.get("enabled").and_then(Value::as_bool) == Some(false) { continue; } let Some(model_name) = codex_model_name(entry) else { From 146d3e224ce8de9116d1d6ada3332f92131e8139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=8B=E6=96=97=E7=9A=84=E5=B0=8F=E9=AB=98?= Date: Tue, 18 Aug 2026 20:59:35 +0800 Subject: [PATCH 3/6] style(codex): format frontend test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prettier 修正 tests/utils/codexSpawnAgentCandidates.test.ts,修复 CI Frontend Checks 的格式检查。 --- tests/utils/codexSpawnAgentCandidates.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/utils/codexSpawnAgentCandidates.test.ts b/tests/utils/codexSpawnAgentCandidates.test.ts index 646135483..1041fde48 100644 --- a/tests/utils/codexSpawnAgentCandidates.test.ts +++ b/tests/utils/codexSpawnAgentCandidates.test.ts @@ -83,10 +83,7 @@ describe("codexSpawnAgentCandidates", () => { "qwen3.6", "deepseek-v4-pro", ]); - expect(visible.spawnAgentModels).toEqual([ - "qwen3.6", - "deepseek-v4-pro", - ]); + expect(visible.spawnAgentModels).toEqual(["qwen3.6", "deepseek-v4-pro"]); expect( readRawCodexModelCatalogModels(provider).map((model) => model.model), From 1cd6342e6d64c3980495c7523f3946de246b75e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=8B=E6=96=97=E7=9A=84=E5=B0=8F=E9=AB=98?= Date: Tue, 18 Aug 2026 21:06:52 +0800 Subject: [PATCH 4/6] test(codex): update catalog retention test for disabled rows --- tests/components/CodexFormFields.test.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/components/CodexFormFields.test.tsx b/tests/components/CodexFormFields.test.tsx index e7fbeb3f4..47cfc4e0f 100644 --- a/tests/components/CodexFormFields.test.tsx +++ b/tests/components/CodexFormFields.test.tsx @@ -1171,16 +1171,22 @@ describe("CodexFormFields local model routing", () => { await waitFor(() => { expect(latestCatalog().map((model) => model.model)).toEqual([ "model-a", + "model-b", "model-c", ]); + expect(latestCatalog()[1]).toMatchObject({ + model: "model-b", + enabled: false, + }); }); - fireEvent.click(screen.getAllByTitle("上移")[1]); + fireEvent.click(screen.getAllByTitle("上移")[2]); await waitFor(() => { expect(latestCatalog().map((model) => model.model)).toEqual([ - "model-c", "model-a", + "model-c", + "model-b", ]); }); }); From 74a3c8751a87432592441939ec68f5a8bf9be8ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=8B=E6=96=97=E7=9A=84=E5=B0=8F=E9=AB=98?= Date: Tue, 18 Aug 2026 22:44:00 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=90=8D=E7=A7=B0(?= =?UTF-8?q?=E4=BF=9D=E7=95=99->=E5=90=AF=E7=94=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../providers/forms/CodexFormFields.tsx | 16 +++++----- .../CodexFormFields.keepColumn.test.tsx | 32 ++++++++++--------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/components/providers/forms/CodexFormFields.tsx b/src/components/providers/forms/CodexFormFields.tsx index f2730a1ec..fe8e00230 100644 --- a/src/components/providers/forms/CodexFormFields.tsx +++ b/src/components/providers/forms/CodexFormFields.tsx @@ -2729,8 +2729,8 @@ export function CodexFormFields({ {/* 列头:md+ 显示 */}
- {t("codexConfig.keepCatalogModelColumn", { - defaultValue: "保留", + {t("codexConfig.enableCatalogModelColumn", { + defaultValue: "启用", })} @@ -2802,18 +2802,18 @@ export function CodexFormFields({ enabled: event.target.checked, }); }} - aria-label={t("codexConfig.keepCatalogModel", { + aria-label={t("codexConfig.enableCatalogModel", { model: row.model || row.displayName || "", - defaultValue: `保留 ${row.model || row.displayName || "这个模型"}`, + defaultValue: `启用 ${row.model || row.displayName || "这个模型"}`, })} /> {row.enabled === false - ? t("codexConfig.keepCatalogModelDisabled", { - defaultValue: "未使用", + ? t("codexConfig.enableCatalogModelDisabled", { + defaultValue: "未启用", }) - : t("codexConfig.keepCatalogModelColumn", { - defaultValue: "保留", + : t("codexConfig.enableCatalogModelColumn", { + defaultValue: "启用", })} diff --git a/tests/components/CodexFormFields.keepColumn.test.tsx b/tests/components/CodexFormFields.keepColumn.test.tsx index 6f1ee4121..c7c3a47d3 100644 --- a/tests/components/CodexFormFields.keepColumn.test.tsx +++ b/tests/components/CodexFormFields.keepColumn.test.tsx @@ -47,24 +47,25 @@ function renderCodexFormFields(catalogModels: CodexCatalogModel[]) { return { onCatalogModelsChange }; } -describe("CodexFormFields keep column", () => { - it("keeps the catalog row and disables the model when keep is unchecked", async () => { +describe("CodexFormFields enable column", () => { + it("keeps the catalog row and disables the model when enable is unchecked", async () => { const { onCatalogModelsChange } = renderCodexFormFields([ { model: "deepseek-v4-flash" }, ]); fireEvent.click(screen.getByRole("button", { name: "高级选项" })); - const keepCheckbox = screen.getByRole("checkbox", { - name: "保留 deepseek-v4-flash", + expect(screen.getAllByText("启用").length).toBeGreaterThan(0); + const enableCheckbox = screen.getByRole("checkbox", { + name: "启用 deepseek-v4-flash", }); - expect(keepCheckbox).toBeChecked(); + expect(enableCheckbox).toBeChecked(); - fireEvent.click(keepCheckbox); + fireEvent.click(enableCheckbox); await waitFor(() => { - expect(keepCheckbox).not.toBeChecked(); - expect(screen.getByText("未使用")).toBeInTheDocument(); + expect(enableCheckbox).not.toBeChecked(); + expect(screen.getByText("未启用")).toBeInTheDocument(); }); expect( screen.getAllByDisplayValue("deepseek-v4-flash").length, @@ -77,23 +78,24 @@ describe("CodexFormFields keep column", () => { ]); }); - it("re-enables the model when the kept row is checked again", async () => { + it("re-enables the model when the disabled row is checked again", async () => { const { onCatalogModelsChange } = renderCodexFormFields([ { model: "deepseek-v4-flash", enabled: false }, ]); fireEvent.click(screen.getByRole("button", { name: "高级选项" })); - const keepCheckbox = screen.getByRole("checkbox", { - name: "保留 deepseek-v4-flash", + const enableCheckbox = screen.getByRole("checkbox", { + name: "启用 deepseek-v4-flash", }); - expect(keepCheckbox).not.toBeChecked(); + expect(enableCheckbox).not.toBeChecked(); + expect(screen.getByText("未启用")).toBeInTheDocument(); - fireEvent.click(keepCheckbox); + fireEvent.click(enableCheckbox); await waitFor(() => { - expect(keepCheckbox).toBeChecked(); - expect(screen.queryByText("未使用")).not.toBeInTheDocument(); + expect(enableCheckbox).toBeChecked(); + expect(screen.queryByText("未启用")).not.toBeInTheDocument(); }); expect(onCatalogModelsChange).toHaveBeenLastCalledWith([ expect.objectContaining({ From 24ca5b4a49073ecfe2b881afbc16630892bdedd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=8B=E6=96=97=E7=9A=84=E5=B0=8F=E9=AB=98?= Date: Tue, 18 Aug 2026 23:01:54 +0800 Subject: [PATCH 6/6] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=90=8D=E7=A7=B0(?= =?UTF-8?q?=E4=BF=9D=E7=95=99->=E5=90=AF=E7=94=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/components/CodexFormFields.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/CodexFormFields.test.tsx b/tests/components/CodexFormFields.test.tsx index 47cfc4e0f..eb18c01d4 100644 --- a/tests/components/CodexFormFields.test.tsx +++ b/tests/components/CodexFormFields.test.tsx @@ -1159,14 +1159,14 @@ describe("CodexFormFields local model routing", () => { }); }); - it("uses model mapping checkboxes and arrows for catalog retention and order", async () => { + it("uses model mapping checkboxes and arrows for catalog enablement and order", async () => { const { latestCatalog } = renderCatalogHarness([ { model: "model-a", upstreamModel: "model-a" }, { model: "model-b", upstreamModel: "model-b" }, { model: "model-c", upstreamModel: "model-c" }, ]); - fireEvent.click(screen.getByLabelText("保留 model-b")); + fireEvent.click(screen.getByLabelText("启用 model-b")); await waitFor(() => { expect(latestCatalog().map((model) => model.model)).toEqual([