diff --git a/src/components/providers/forms/CommonConfigEditor.tsx b/src/components/providers/forms/CommonConfigEditor.tsx index f6392b846..99f9b2d7f 100644 --- a/src/components/providers/forms/CommonConfigEditor.tsx +++ b/src/components/providers/forms/CommonConfigEditor.tsx @@ -3,6 +3,7 @@ import { useEffect, useState, useCallback, useMemo } from "react"; import { Label } from "@/components/ui/label"; import JsonEditor from "@/components/JsonEditor"; import type { ProviderEditorInactiveField } from "@/lib/api/providers"; +import { CLAUDE_AUTO_MODE_SERVER_ENV } from "@/utils/claudeEditorOverlay"; import { InactiveFieldsPanel } from "./InactiveFieldsPanel"; interface CommonConfigEditorProps { @@ -94,6 +95,9 @@ export function CommonConfigEditor({ disableArtifact: config?.env?.CLAUDE_CODE_DISABLE_ARTIFACT === "1" || config?.env?.CLAUDE_CODE_DISABLE_ARTIFACT === 1, + disableAutoModeServer: + config?.env?.[CLAUDE_AUTO_MODE_SERVER_ENV] === "0" || + config?.env?.[CLAUDE_AUTO_MODE_SERVER_ENV] === 0, }; } catch { return { @@ -103,6 +107,7 @@ export function CommonConfigEditor({ effortMax: false, disableAutoUpgrade: false, disableArtifact: false, + disableAutoModeServer: false, }; } }, [localValue]); @@ -168,6 +173,15 @@ export function CommonConfigEditor({ if (Object.keys(config.env).length === 0) delete config.env; } break; + case "disableAutoModeServer": + if (!config.env) config.env = {}; + if (checked) { + config.env[CLAUDE_AUTO_MODE_SERVER_ENV] = "0"; + } else { + delete config.env[CLAUDE_AUTO_MODE_SERVER_ENV]; + if (Object.keys(config.env).length === 0) delete config.env; + } + break; } handleLocalChange(JSON.stringify(config, null, 2)); @@ -270,6 +284,20 @@ export function CommonConfigEditor({ /> {t("claudeConfig.disableArtifact")} + , + claudeLiveBase ?? {}, + withClaudeGatewayDefaults( + JSON.parse(CLAUDE_DEFAULT_CONFIG) as Record, + "custom", + ), ), null, 2, @@ -2211,10 +2217,13 @@ function ProviderFormFull({ ); // 预设只带关键字段和独有字段,套在当前 live 上显示(和切换的结果一致)。 const config = - appId === "claude" && claudeLiveBase + appId === "claude" ? overlayClaudeProviderFields( - claudeLiveBase, - templated as Record, + claudeLiveBase ?? {}, + withClaudeGatewayDefaults( + templated as Record, + preset.category, + ), ) : templated; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 110143543..376259d9d 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -76,6 +76,8 @@ "effortMax": "Max Effort Thinking", "disableAutoUpgrade": "Disable Auto-Upgrade", "disableArtifact": "Disable Artifact Tool", + "disableAutoModeServer": "Disable Auto Mode Server Checks", + "disableAutoModeServerHint": "Third-party gateways can't run auto mode's server-side checks. When on, Claude Code uses its own checks and stops showing the \"this session isn't eligible\" notice in every session (CLAUDE_CODE_AUTO_MODE_SERVER=0). Leave off for the official endpoint.", "keyFieldsHint": "Endpoint, key, models, context window and compatibility switches follow the provider; everything else is a global Claude Code setting and applies to every provider once saved.", "inactiveFieldsHint": "This provider still stores {{count}} field(s) that do not take effect when switching. Click one to add it to the global settings above; it is written to the config file on save, and the value stored in the provider is kept.", "addToGlobalSettings": "Add to global settings" diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 9187999f0..5198285bf 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -76,6 +76,8 @@ "effortMax": "最大強度思考", "disableAutoUpgrade": "自動アップグレードを無効化", "disableArtifact": "Artifact ツールを無効化", + "disableAutoModeServer": "auto mode のサーバー側チェックを無効化", + "disableAutoModeServerHint": "サードパーティのゲートウェイは auto mode のサーバー側チェックに対応していません。オンにすると Claude Code は自前のチェックを使い、セッションごとの「このセッションは対象外です」という通知が出なくなります(CLAUDE_CODE_AUTO_MODE_SERVER=0)。公式エンドポイントではオフのままにしてください。", "keyFieldsHint": "エンドポイント、キー、モデル、コンテキストウィンドウ、互換スイッチはプロバイダーの切り替えに従います。それ以外のフィールドは Claude Code のグローバル設定で、保存するとすべてのプロバイダーに適用されます。", "inactiveFieldsHint": "このプロバイダーには、切り替えても反映されないフィールドが {{count}} 件保存されています。クリックすると上のグローバル設定に追加され、保存時に設定ファイルへ書き込まれます。プロバイダーに保存された元の値は削除されません。", "addToGlobalSettings": "グローバル設定に追加" diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index 16ae3c7fa..8b5187e4e 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -76,6 +76,8 @@ "effortMax": "最大強度思考", "disableAutoUpgrade": "停用自動升級", "disableArtifact": "停用 Artifact 工具", + "disableAutoModeServer": "停用 auto mode 伺服器端檢查", + "disableAutoModeServerHint": "第三方閘道不支援 auto mode 的伺服器端檢查。勾選後 Claude Code 改用自己的檢查,不再每個工作階段跳出「此工作階段不符合資格」的提示(CLAUDE_CODE_AUTO_MODE_SERVER=0)。官方端點請勿勾選。", "keyFieldsHint": "位址、Key、模型、上下文視窗和相容開關隨供應商切換;其餘欄位是 Claude Code 全域設定,儲存後對所有供應商生效。", "inactiveFieldsHint": "這個供應商還保存著 {{count}} 個不隨切換生效的欄位。點擊可加入上方的全域設定,儲存後寫入設定檔;供應商裡保存的原值不會刪除。", "addToGlobalSettings": "加入全域設定" diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index fe4b6a094..11cc4a9b7 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -76,6 +76,8 @@ "effortMax": "最大强度思考", "disableAutoUpgrade": "禁用自动升级", "disableArtifact": "禁用 Artifact 工具", + "disableAutoModeServer": "关闭 auto mode 服务端检查", + "disableAutoModeServerHint": "第三方网关不支持 auto mode 的服务端检查。勾选后 Claude Code 改用自己的检查,不再每个会话弹出“本会话不符合条件”的提示(CLAUDE_CODE_AUTO_MODE_SERVER=0)。官方端点不要勾选。", "keyFieldsHint": "地址、Key、模型、上下文窗口和兼容开关随供应商切换;其余字段是 Claude Code 全局设置,保存后对所有供应商生效。", "inactiveFieldsHint": "这个供应商还保存着 {{count}} 个不随切换生效的字段。点击可加入上方的全局设置,保存后写入配置文件;供应商里保存的原值不会删除。", "addToGlobalSettings": "加入全局设置" diff --git a/src/utils/claudeEditorOverlay.ts b/src/utils/claudeEditorOverlay.ts index 1e966967d..a1ca18236 100644 --- a/src/utils/claudeEditorOverlay.ts +++ b/src/utils/claudeEditorOverlay.ts @@ -1,6 +1,30 @@ const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null && !Array.isArray(value); +/** + * auto mode 的服务端检查只有官方端点支持,经网关时 Claude Code 每个会话都会弹一次需要 + * 确认的提示;设成 "0" 改用 Claude Code 自己的检查,提示不再出现。 + */ +export const CLAUDE_AUTO_MODE_SERVER_ENV = "CLAUDE_CODE_AUTO_MODE_SERVER"; + +/** + * 新增 Claude 供应商时补上网关默认值:除官方和云服务商(Bedrock、Vertex 直连,服务端 + * 检查随平台上线)外,默认关掉 auto mode 的服务端检查。预设自己写了的以预设为准。 + */ +export function withClaudeGatewayDefaults( + fields: Record, + category: string | undefined, +): Record { + if (category === "official" || category === "cloud_provider") { + return fields; + } + const env = isRecord(fields.env) ? fields.env : {}; + if (CLAUDE_AUTO_MODE_SERVER_ENV in env) { + return fields; + } + return { ...fields, env: { ...env, [CLAUDE_AUTO_MODE_SERVER_ENV]: "0" } }; +} + /** * 新增 Claude 供应商时,把预设的字段(只有关键字段和独有字段)套在当前 live 上显示, * 和切过去之后的结果一致:预设的顶层键、`env` 键覆盖 live 的同名键,其余保持 live 的 diff --git a/tests/components/CommonConfigEditor.test.tsx b/tests/components/CommonConfigEditor.test.tsx index b4b8dd9d0..92c96745a 100644 --- a/tests/components/CommonConfigEditor.test.tsx +++ b/tests/components/CommonConfigEditor.test.tsx @@ -152,3 +152,43 @@ describe("CommonConfigEditor inactive row fields", () => { ).not.toBeInTheDocument(); }); }); + +describe("CommonConfigEditor auto mode server toggle", () => { + const autoModeServerCheckbox = () => + screen.getByRole("checkbox", { + name: "claudeConfig.disableAutoModeServer", + }); + + it("reads CLAUDE_CODE_AUTO_MODE_SERVER=0 as checked", () => { + renderEditor( + JSON.stringify({ env: { CLAUDE_CODE_AUTO_MODE_SERVER: "0" } }), + ); + + expect(autoModeServerCheckbox()).toBeChecked(); + }); + + it("writes and removes the env key", () => { + const onChange = renderEditor( + JSON.stringify({ env: { ANTHROPIC_BASE_URL: "https://gw.example" } }), + ); + + fireEvent.click(autoModeServerCheckbox()); + + expect(JSON.parse(onChange.mock.calls[0][0])).toEqual({ + env: { + ANTHROPIC_BASE_URL: "https://gw.example", + CLAUDE_CODE_AUTO_MODE_SERVER: "0", + }, + }); + }); + + it("drops an emptied env when unchecked", () => { + const onChange = renderEditor( + JSON.stringify({ env: { CLAUDE_CODE_AUTO_MODE_SERVER: "0" } }), + ); + + fireEvent.click(autoModeServerCheckbox()); + + expect(JSON.parse(onChange.mock.calls[0][0])).toEqual({}); + }); +}); diff --git a/tests/components/__snapshots__/ProviderForm.presetRows.golden.test.tsx.snap b/tests/components/__snapshots__/ProviderForm.presetRows.golden.test.tsx.snap index fab279bb8..5b39e24c7 100644 --- a/tests/components/__snapshots__/ProviderForm.presetRows.golden.test.tsx.snap +++ b/tests/components/__snapshots__/ProviderForm.presetRows.golden.test.tsx.snap @@ -74,6 +74,7 @@ exports[`ProviderForm 预设新增行金标 > claude > E-FlowCode 1`] = ` "env": { "ANTHROPIC_AUTH_TOKEN": "sk-golden-test", "ANTHROPIC_BASE_URL": "https://e-flowcode.cc", + "CLAUDE_CODE_AUTO_MODE_SERVER": "0", "ENABLE_TOOL_SEARCH": "true", }, }, @@ -107,6 +108,7 @@ exports[`ProviderForm 预设新增行金标 > claude > Kimi For Coding 1`] = ` "ANTHROPIC_DEFAULT_SONNET_MODEL": "kimi-for-coding", "ANTHROPIC_MODEL": "kimi-for-coding", "CLAUDE_CODE_AUTO_COMPACT_WINDOW": "262144", + "CLAUDE_CODE_AUTO_MODE_SERVER": "0", "CLAUDE_CODE_MAX_CONTEXT_TOKENS": "262144", }, }, @@ -139,6 +141,7 @@ exports[`ProviderForm 预设新增行金标 > claude > Nvidia 1`] = ` "ANTHROPIC_DEFAULT_OPUS_MODEL": "moonshotai/kimi-k3", "ANTHROPIC_DEFAULT_SONNET_MODEL": "moonshotai/kimi-k3", "ANTHROPIC_MODEL": "moonshotai/kimi-k3", + "CLAUDE_CODE_AUTO_MODE_SERVER": "0", }, }, "websiteUrl": "https://build.nvidia.com", @@ -166,6 +169,7 @@ exports[`ProviderForm 预设新增行金标 > claude > RelaxyCode 1`] = ` "env": { "ANTHROPIC_AUTH_TOKEN": "sk-golden-test", "ANTHROPIC_BASE_URL": "https://www.relaxycode.com", + "CLAUDE_CODE_AUTO_MODE_SERVER": "0", }, }, "websiteUrl": "https://www.relaxycode.com", diff --git a/tests/utils/claudeEditorOverlay.test.ts b/tests/utils/claudeEditorOverlay.test.ts new file mode 100644 index 000000000..21b246736 --- /dev/null +++ b/tests/utils/claudeEditorOverlay.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vitest"; +import { withClaudeGatewayDefaults } from "@/utils/claudeEditorOverlay"; + +describe("withClaudeGatewayDefaults", () => { + const gateway = { env: { ANTHROPIC_BASE_URL: "https://gw.example" } }; + + it.each(["third_party", "aggregator", "cn_official", "custom", undefined])( + "turns auto mode server checks off for %s providers", + (category) => { + expect(withClaudeGatewayDefaults(gateway, category)).toEqual({ + env: { + ANTHROPIC_BASE_URL: "https://gw.example", + CLAUDE_CODE_AUTO_MODE_SERVER: "0", + }, + }); + }, + ); + + it.each(["official", "cloud_provider"])( + "leaves %s providers on the default", + (category) => { + expect(withClaudeGatewayDefaults(gateway, category)).toBe(gateway); + }, + ); + + it("keeps a value the preset sets itself", () => { + const preset = { env: { CLAUDE_CODE_AUTO_MODE_SERVER: "1" } }; + expect(withClaudeGatewayDefaults(preset, "third_party")).toBe(preset); + }); + + it("adds env when the preset has none", () => { + expect(withClaudeGatewayDefaults({}, "custom")).toEqual({ + env: { CLAUDE_CODE_AUTO_MODE_SERVER: "0" }, + }); + }); +});