Files
CC-Switch/src/components/providers/forms/CodexConfigSections.tsx
T
Jason a04e72a267 Fix Codex edit dialog masking live OAuth during proxy takeover
The reported "OAuth access token disappears when enabling Codex proxy
takeover" was a display artifact, not data loss: auth.json on disk kept
the OAuth login the whole time. During takeover the edit dialog falls
back to the stored provider config (so it does not surface the proxy
placeholder), which for a third-party provider shows that provider's own
key instead of the live auth.json, making the OAuth token look gone.

Thread an isProxyTakeover flag from App through ProviderForm into the
Codex editor and show an explicit notice plus storage-aware auth/config
hints clarifying that the form displays the stored provider config while
the live config is temporarily managed by the proxy. Drop the
proxy-running condition so the notice shows whenever takeover is active,
even with the proxy stopped.

Add a regression test asserting the dialog does not read live settings
during takeover and renders the database config. i18n synced across
en/zh/ja/zh-TW.
2026-06-01 11:41:05 +08:00

388 lines
12 KiB
TypeScript

// NOTE: Codex 1M 上下文 UI 已暂时隐藏(详见下方 CodexConfigSection 内 JSX 注释)。
// 如需恢复,请同时:
// - 取消下面 `@/utils/providerConfigUtils` import 的注释
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import JsonEditor from "@/components/JsonEditor";
import {
isCodexGoalModeEnabled,
isCodexRemoteCompactionEnabled,
setCodexGoalMode,
setCodexRemoteCompaction,
} from "@/utils/providerConfigUtils";
/*
import {
extractCodexTopLevelInt,
setCodexTopLevelInt,
removeCodexTopLevelField,
} from "@/utils/providerConfigUtils";
*/
interface CodexAuthSectionProps {
value: string;
onChange: (value: string) => void;
onBlur?: () => void;
error?: string;
isProxyTakeover?: boolean;
}
/**
* CodexAuthSection - Auth JSON editor section
*/
export const CodexAuthSection: React.FC<CodexAuthSectionProps> = ({
value,
onChange,
onBlur,
error,
isProxyTakeover = false,
}) => {
const { t } = useTranslation();
const [isDarkMode, setIsDarkMode] = useState(false);
useEffect(() => {
setIsDarkMode(document.documentElement.classList.contains("dark"));
const observer = new MutationObserver(() => {
setIsDarkMode(document.documentElement.classList.contains("dark"));
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
return () => observer.disconnect();
}, []);
const handleChange = (newValue: string) => {
onChange(newValue);
if (onBlur) {
onBlur();
}
};
return (
<div className="space-y-2">
<label
htmlFor="codexAuth"
className="block text-sm font-medium text-foreground"
>
{t("codexConfig.authJson")}
</label>
<JsonEditor
value={value}
onChange={handleChange}
placeholder={t("codexConfig.authJsonPlaceholder")}
darkMode={isDarkMode}
rows={6}
showValidation={true}
language="json"
/>
{error && (
<p className="text-xs text-red-500 dark:text-red-400">{error}</p>
)}
{!error && (
<p className="text-xs text-muted-foreground">
{t(
isProxyTakeover
? "codexConfig.authJsonStorageHint"
: "codexConfig.authJsonHint",
)}
</p>
)}
</div>
);
};
interface CodexConfigSectionProps {
value: string;
onChange: (value: string) => void;
providerName?: string;
showRemoteCompaction?: boolean;
useCommonConfig: boolean;
onCommonConfigToggle: (checked: boolean) => void;
onEditCommonConfig: () => void;
commonConfigError?: string;
configError?: string;
isProxyTakeover?: boolean;
}
/**
* CodexConfigSection - Config TOML editor section
*/
export const CodexConfigSection: React.FC<CodexConfigSectionProps> = ({
value,
onChange,
providerName,
showRemoteCompaction = true,
useCommonConfig,
onCommonConfigToggle,
onEditCommonConfig,
commonConfigError,
configError,
isProxyTakeover = false,
}) => {
const { t } = useTranslation();
const [isDarkMode, setIsDarkMode] = useState(false);
useEffect(() => {
setIsDarkMode(document.documentElement.classList.contains("dark"));
const observer = new MutationObserver(() => {
setIsDarkMode(document.documentElement.classList.contains("dark"));
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
return () => observer.disconnect();
}, []);
// Mirror value prop to local state (same pattern as CommonConfigEditor)
const [localValue, setLocalValue] = useState(value);
const localValueRef = useRef(value);
useEffect(() => {
setLocalValue(value);
localValueRef.current = value;
}, [value]);
const handleLocalChange = useCallback(
(newValue: string) => {
if (newValue === localValueRef.current) return;
localValueRef.current = newValue;
setLocalValue(newValue);
onChange(newValue);
},
[onChange],
);
const goalModeEnabled = useMemo(
() => isCodexGoalModeEnabled(localValue),
[localValue],
);
const remoteCompactionEnabled = useMemo(
() => isCodexRemoteCompactionEnabled(localValue),
[localValue],
);
const handleGoalModeToggle = useCallback(
(checked: boolean) => {
handleLocalChange(setCodexGoalMode(localValueRef.current || "", checked));
},
[handleLocalChange],
);
const handleRemoteCompactionToggle = useCallback(
(checked: boolean) => {
handleLocalChange(
setCodexRemoteCompaction(
localValueRef.current || "",
checked,
providerName,
),
);
},
[handleLocalChange, providerName],
);
// Codex 1M 上下文相关状态/回调暂时禁用——见同文件下方 JSX 注释处的恢复说明。
/*
// Parse toggle states from TOML text
const toggleStates = useMemo(() => {
const contextWindow = extractCodexTopLevelInt(
localValue,
"model_context_window",
);
const compactLimit = extractCodexTopLevelInt(
localValue,
"model_auto_compact_token_limit",
);
return {
contextWindow1M: contextWindow === 1000000,
compactLimit: compactLimit ?? 900000,
};
}, [localValue]);
// Debounce timer for compact limit input
const compactTimerRef = useRef<ReturnType<typeof setTimeout>>();
const handleContextWindowToggle = useCallback(
(checked: boolean) => {
let toml = localValueRef.current || "";
if (checked) {
toml = setCodexTopLevelInt(toml, "model_context_window", 1000000);
// Auto-set compact limit if not already present
if (
extractCodexTopLevelInt(toml, "model_auto_compact_token_limit") ===
undefined
) {
toml = setCodexTopLevelInt(
toml,
"model_auto_compact_token_limit",
900000,
);
}
} else {
toml = removeCodexTopLevelField(toml, "model_context_window");
toml = removeCodexTopLevelField(toml, "model_auto_compact_token_limit");
}
handleLocalChange(toml);
},
[handleLocalChange],
);
const handleCompactLimitChange = useCallback(
(inputValue: string) => {
clearTimeout(compactTimerRef.current);
compactTimerRef.current = setTimeout(() => {
const num = parseInt(inputValue, 10);
if (!Number.isNaN(num) && num > 0) {
handleLocalChange(
setCodexTopLevelInt(
localValueRef.current || "",
"model_auto_compact_token_limit",
num,
),
);
}
}, 500);
},
[handleLocalChange],
);
// Cleanup debounce timer
useEffect(() => {
return () => clearTimeout(compactTimerRef.current);
}, []);
*/
return (
<div className="space-y-2">
<div className="flex flex-wrap items-center justify-between gap-2">
<label
htmlFor="codexConfig"
className="block text-sm font-medium text-foreground"
>
{t("codexConfig.configToml")}
</label>
<div className="flex flex-wrap items-center justify-end gap-x-4 gap-y-1">
<label className="inline-flex cursor-pointer items-center gap-2 text-sm text-muted-foreground">
<input
type="checkbox"
checked={goalModeEnabled}
onChange={(e) => handleGoalModeToggle(e.target.checked)}
className="w-4 h-4 text-blue-500 bg-white dark:bg-gray-800 border-border-default rounded focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-2"
/>
{t("codexConfig.enableGoalMode")}
</label>
{showRemoteCompaction && (
<label
className="inline-flex cursor-pointer items-center gap-2 text-sm text-muted-foreground"
title={t("codexConfig.remoteCompactionHint")}
>
<input
type="checkbox"
checked={remoteCompactionEnabled}
onChange={(e) => handleRemoteCompactionToggle(e.target.checked)}
className="w-4 h-4 text-blue-500 bg-white dark:bg-gray-800 border-border-default rounded focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-2"
/>
{t("codexConfig.enableRemoteCompaction")}
</label>
)}
<label className="inline-flex cursor-pointer items-center gap-2 text-sm text-muted-foreground">
<input
type="checkbox"
checked={useCommonConfig}
onChange={(e) => onCommonConfigToggle(e.target.checked)}
className="w-4 h-4 text-blue-500 bg-white dark:bg-gray-800 border-border-default rounded focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-2"
/>
{t("codexConfig.writeCommonConfig")}
</label>
</div>
</div>
<div className="flex items-center justify-end">
<button
type="button"
onClick={onEditCommonConfig}
className="text-xs text-blue-500 dark:text-blue-400 hover:underline"
>
{t("codexConfig.editCommonConfig")}
</button>
</div>
{commonConfigError && (
<p className="text-xs text-red-500 dark:text-red-400 text-right">
{commonConfigError}
</p>
)}
{/* Codex 1M 上下文 UI 已隐藏:模型不再支持该字段。
恢复方法:(1) 取消本段 JSX 注释;(2) 取消文件顶部 import 中 useMemo / extractCodexTopLevelInt / setCodexTopLevelInt / removeCodexTopLevelField 的注释;(3) 取消下方 toggleStates / compactTimerRef / handleContextWindowToggle / handleCompactLimitChange / cleanup useEffect 的注释。
<div className="flex flex-wrap items-center gap-x-4 gap-y-1">
<label className="inline-flex items-center gap-2 text-sm text-muted-foreground cursor-pointer">
<input
type="checkbox"
checked={toggleStates.contextWindow1M}
onChange={(e) => handleContextWindowToggle(e.target.checked)}
className="w-4 h-4 text-blue-500 bg-white dark:bg-gray-800 border-border-default rounded focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-2"
/>
<span>{t("codexConfig.contextWindow1M")}</span>
</label>
<label className="inline-flex items-center gap-2 text-sm text-muted-foreground">
<span>{t("codexConfig.autoCompactLimit")}:</span>
<input
type="text"
inputMode="numeric"
pattern="[0-9]*"
key={toggleStates.compactLimit}
defaultValue={toggleStates.compactLimit}
disabled={!toggleStates.contextWindow1M}
onChange={(e) => handleCompactLimitChange(e.target.value)}
className="w-28 h-7 px-2 text-sm rounded border border-border bg-background text-foreground disabled:opacity-50 disabled:cursor-not-allowed"
/>
</label>
</div>
*/}
<JsonEditor
value={localValue}
onChange={handleLocalChange}
placeholder=""
darkMode={isDarkMode}
rows={8}
showValidation={false}
language="javascript"
/>
{configError && (
<p className="text-xs text-red-500 dark:text-red-400">{configError}</p>
)}
{!configError && (
<p className="text-xs text-muted-foreground">
{t(
isProxyTakeover
? "codexConfig.configTomlStorageHint"
: "codexConfig.configTomlHint",
)}
</p>
)}
</div>
);
};