mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-27 08:14:33 +08:00
a04e72a267
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.
388 lines
12 KiB
TypeScript
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>
|
|
);
|
|
};
|