mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 21:30:17 +08:00
948d762792
Codex buckets resume history by the model_provider id recorded in each session: official runs (no key, built-in "openai") and cc-switch third-party runs (shared "custom") are mutually invisible in the resume picker. Add an opt-in setting that runs official providers under the shared "custom" id so future official sessions land in the same history bucket as third-party ones. Forward-only by design: existing sessions are not migrated. When enabled, official live config.toml gets model_provider = "custom" plus a [model_providers.custom] entry that mirrors the built-in openai provider (requires_openai_auth routes auth to the ChatGPT login in auth.json, name "OpenAI" keeps is_openai() feature gates, explicit supports_websockets/wire_api restore built-in defaults). auth.json is untouched. Key invariants: - Injection lives only in the live config: switch-away backfill strips the exact injected shape, so stored provider configs stay clean and turning the toggle off fully reverts on the next write. - Toggle changes apply immediately via a takeover-aware reapply: when the proxy owns the live config (backup/placeholder present), only the live backup is updated, mirroring the provider-switch path. - The takeover backup path runs the same injection so a takeover release restores a config that still carries the unified routing. - Injection refuses to activate a foreign [model_providers.custom] table (e.g. stale entry with a third-party base_url) to avoid routing ChatGPT OAuth traffic to an unknown backend. The toggle lives under Settings → Codex App Enhancements; the description warns that resuming old sessions across providers may fail because encrypted_content reasoning only decrypts on the backend that created it (upstream treats cross-provider resume as unsupported).
212 lines
6.3 KiB
TypeScript
212 lines
6.3 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useSettingsQuery } from "@/lib/query";
|
|
import type { Settings } from "@/types";
|
|
|
|
type Language = "zh" | "zh-TW" | "en" | "ja";
|
|
|
|
export type SettingsFormState = Omit<Settings, "language"> & {
|
|
language: Language;
|
|
};
|
|
|
|
const normalizeLanguage = (lang?: string | null): Language => {
|
|
if (!lang) return "zh";
|
|
const normalized = lang.toLowerCase().replace(/_/g, "-");
|
|
|
|
if (normalized === "zh") {
|
|
return "zh";
|
|
}
|
|
|
|
if (
|
|
normalized === "zh-tw" ||
|
|
normalized.startsWith("zh-hant") ||
|
|
normalized.startsWith("zh-hk") ||
|
|
normalized.startsWith("zh-mo")
|
|
) {
|
|
return "zh-TW";
|
|
}
|
|
|
|
if (normalized === "en" || normalized === "ja") {
|
|
return normalized;
|
|
}
|
|
|
|
if (normalized.startsWith("zh")) {
|
|
return "zh";
|
|
}
|
|
|
|
return "zh";
|
|
};
|
|
|
|
const isSupportedLanguage = (lang?: string | null): boolean => {
|
|
if (!lang) return false;
|
|
const normalized = lang.toLowerCase().replace(/_/g, "-");
|
|
return (
|
|
normalized === "en" || normalized === "ja" || normalized.startsWith("zh")
|
|
);
|
|
};
|
|
|
|
const sanitizeDir = (value?: string | null): string | undefined => {
|
|
if (!value) return undefined;
|
|
const trimmed = value.trim();
|
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
};
|
|
|
|
export interface UseSettingsFormResult {
|
|
settings: SettingsFormState | null;
|
|
isLoading: boolean;
|
|
initialLanguage: Language;
|
|
updateSettings: (updates: Partial<SettingsFormState>) => void;
|
|
resetSettings: (serverData: Settings | null) => void;
|
|
readPersistedLanguage: () => Language;
|
|
syncLanguage: (lang: Language) => void;
|
|
}
|
|
|
|
/**
|
|
* useSettingsForm - 表单状态管理
|
|
* 负责:
|
|
* - 表单数据状态
|
|
* - 表单字段更新
|
|
* - 语言同步
|
|
* - 表单重置
|
|
*/
|
|
export function useSettingsForm(): UseSettingsFormResult {
|
|
const { i18n } = useTranslation();
|
|
const { data, isLoading } = useSettingsQuery();
|
|
|
|
const [settingsState, setSettingsState] = useState<SettingsFormState | null>(
|
|
null,
|
|
);
|
|
|
|
const initialLanguageRef = useRef<Language>("zh");
|
|
|
|
const readPersistedLanguage = useCallback((): Language => {
|
|
if (typeof window !== "undefined") {
|
|
const stored = window.localStorage.getItem("language");
|
|
if (isSupportedLanguage(stored)) {
|
|
return normalizeLanguage(stored);
|
|
}
|
|
}
|
|
return normalizeLanguage(i18n.language);
|
|
}, [i18n]);
|
|
|
|
const syncLanguage = useCallback(
|
|
(lang: Language) => {
|
|
const current = normalizeLanguage(i18n.language);
|
|
if (current !== lang) {
|
|
void i18n.changeLanguage(lang);
|
|
}
|
|
},
|
|
[i18n],
|
|
);
|
|
|
|
// 初始化设置数据
|
|
useEffect(() => {
|
|
if (!data) return;
|
|
|
|
const normalizedLanguage = normalizeLanguage(
|
|
data.language ?? readPersistedLanguage(),
|
|
);
|
|
|
|
const normalized: SettingsFormState = {
|
|
...data,
|
|
showInTray: data.showInTray ?? true,
|
|
minimizeToTrayOnClose: data.minimizeToTrayOnClose ?? true,
|
|
useAppWindowControls: data.useAppWindowControls ?? false,
|
|
enableClaudePluginIntegration:
|
|
data.enableClaudePluginIntegration ?? false,
|
|
silentStartup: data.silentStartup ?? false,
|
|
skipClaudeOnboarding: data.skipClaudeOnboarding ?? false,
|
|
preserveCodexOfficialAuthOnSwitch:
|
|
data.preserveCodexOfficialAuthOnSwitch ?? false,
|
|
unifyCodexSessionHistory: data.unifyCodexSessionHistory ?? false,
|
|
claudeConfigDir: sanitizeDir(data.claudeConfigDir),
|
|
codexConfigDir: sanitizeDir(data.codexConfigDir),
|
|
geminiConfigDir: sanitizeDir(data.geminiConfigDir),
|
|
opencodeConfigDir: sanitizeDir(data.opencodeConfigDir),
|
|
openclawConfigDir: sanitizeDir(data.openclawConfigDir),
|
|
language: normalizedLanguage,
|
|
};
|
|
|
|
setSettingsState(normalized);
|
|
initialLanguageRef.current = normalizedLanguage;
|
|
syncLanguage(normalizedLanguage);
|
|
}, [data, readPersistedLanguage, syncLanguage]);
|
|
|
|
const updateSettings = useCallback(
|
|
(updates: Partial<SettingsFormState>) => {
|
|
setSettingsState((prev) => {
|
|
const base =
|
|
prev ??
|
|
({
|
|
showInTray: true,
|
|
minimizeToTrayOnClose: true,
|
|
useAppWindowControls: false,
|
|
enableClaudePluginIntegration: false,
|
|
skipClaudeOnboarding: false,
|
|
preserveCodexOfficialAuthOnSwitch: false,
|
|
unifyCodexSessionHistory: false,
|
|
language: readPersistedLanguage(),
|
|
} as SettingsFormState);
|
|
|
|
const next: SettingsFormState = {
|
|
...base,
|
|
...updates,
|
|
};
|
|
|
|
if (updates.language) {
|
|
const normalized = normalizeLanguage(updates.language);
|
|
next.language = normalized;
|
|
syncLanguage(normalized);
|
|
}
|
|
|
|
return next;
|
|
});
|
|
},
|
|
[readPersistedLanguage, syncLanguage],
|
|
);
|
|
|
|
const resetSettings = useCallback(
|
|
(serverData: Settings | null) => {
|
|
if (!serverData) return;
|
|
|
|
const normalizedLanguage = normalizeLanguage(
|
|
serverData.language ?? readPersistedLanguage(),
|
|
);
|
|
|
|
const normalized: SettingsFormState = {
|
|
...serverData,
|
|
showInTray: serverData.showInTray ?? true,
|
|
minimizeToTrayOnClose: serverData.minimizeToTrayOnClose ?? true,
|
|
useAppWindowControls: serverData.useAppWindowControls ?? false,
|
|
enableClaudePluginIntegration:
|
|
serverData.enableClaudePluginIntegration ?? false,
|
|
silentStartup: serverData.silentStartup ?? false,
|
|
skipClaudeOnboarding: serverData.skipClaudeOnboarding ?? false,
|
|
preserveCodexOfficialAuthOnSwitch:
|
|
serverData.preserveCodexOfficialAuthOnSwitch ?? false,
|
|
unifyCodexSessionHistory: serverData.unifyCodexSessionHistory ?? false,
|
|
claudeConfigDir: sanitizeDir(serverData.claudeConfigDir),
|
|
codexConfigDir: sanitizeDir(serverData.codexConfigDir),
|
|
geminiConfigDir: sanitizeDir(serverData.geminiConfigDir),
|
|
opencodeConfigDir: sanitizeDir(serverData.opencodeConfigDir),
|
|
openclawConfigDir: sanitizeDir(serverData.openclawConfigDir),
|
|
language: normalizedLanguage,
|
|
};
|
|
|
|
setSettingsState(normalized);
|
|
syncLanguage(initialLanguageRef.current);
|
|
},
|
|
[readPersistedLanguage, syncLanguage],
|
|
);
|
|
|
|
return {
|
|
settings: settingsState,
|
|
isLoading,
|
|
initialLanguage: initialLanguageRef.current,
|
|
updateSettings,
|
|
resetSettings,
|
|
readPersistedLanguage,
|
|
syncLanguage,
|
|
};
|
|
}
|