mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-29 18:05:37 +08:00
refactor(provider): switch from full config overwrite to partial key-field merging (#1098)
* refactor(provider): switch from full config overwrite to partial key-field merging Replace the provider switching mechanism for Claude/Codex/Gemini from full settings_config overwrite to partial key-field replacement, preserving user's non-provider settings (plugins, MCP, permissions, etc.) across switches. - Add write_live_partial() with per-app implementations for Claude (JSON env merge), Codex (auth replace + TOML partial merge), and Gemini (env merge) - Add backfill_key_fields() to extract only provider-specific fields when saving live config back to provider entries - Update switch_normal, sync_current_to_live, add, update to use partial merge - Remove common config snippet feature for Claude/Codex/Gemini (no longer needed with partial merging); preserve OMO common config - Delete 6 frontend files (3 components + 3 hooks), clean up 11 modified files - Remove backend extract_common_config_* methods, 3 Tauri commands, CommonConfigSnippets struct, and related migration code - Update integration tests to validate key-field-only backfill behavior * refactor(cleanup): remove dead code and redundant MCP sync after partial-merge refactor - Remove ConfigService legacy full-overwrite sync methods (~150 lines) - Remove redundant McpService::sync_all_enabled from switch_normal - Switch proxy fallback recovery from write_live_snapshot to write_live_partial - Remove dead ProviderService::write_gemini_live wrapper - Update tests to reflect partial-merge behavior (MCP preserved, not re-synced) * feat(claude): add Quick Toggles for common Claude Code preferences Add checkbox toggles for hideAttribution, alwaysThinking, and enableTeammates that write directly to the live settings file via RFC 7396 JSON Merge Patch. Mirror changes to the form editor using form.watch for reactive updates. * fix(provider): add missing key fields to partial-merge constants Add provider-specific fields verified against official docs to prevent key residue or loss during provider switching: - Claude: CLAUDE_CODE_SUBAGENT_MODEL (env), model (top-level) - Codex: review_model, plan_mode_reasoning_effort - Gemini: GOOGLE_API_KEY (official alternative to GEMINI_API_KEY) * fix(provider): expand partial-merge key fields for Bedrock, Vertex, Foundry and behavior settings Add missing env/top-level fields to CLAUDE_KEY_ENV_FIELDS and CLAUDE_KEY_TOP_LEVEL so that provider switching correctly replaces (and clears) credentials and flags for AWS Bedrock, Google Vertex AI, Microsoft Foundry, and provider behavior overrides like max output tokens and prompt caching. * feat(provider): add auth field selector for Claude providers (AUTH_TOKEN / API_KEY) Allow users to choose between ANTHROPIC_AUTH_TOKEN and ANTHROPIC_API_KEY when creating or editing custom Claude providers, persisted in meta.apiKeyField. * refactor(preset): remove AiHubMix hardcoded API_KEY in favor of generic auth selector AiHubMix was the only preset that hardcoded ANTHROPIC_API_KEY before the generic auth field selector was introduced. Now that users can freely choose between AUTH_TOKEN and API_KEY via the UI, remove the special-case and default AiHubMix to the standard ANTHROPIC_AUTH_TOKEN.
This commit is contained in:
@@ -1,308 +0,0 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
updateTomlCommonConfigSnippet,
|
||||
hasTomlCommonConfigSnippet,
|
||||
} from "@/utils/providerConfigUtils";
|
||||
import { configApi } from "@/lib/api";
|
||||
|
||||
const LEGACY_STORAGE_KEY = "cc-switch:codex-common-config-snippet";
|
||||
const DEFAULT_CODEX_COMMON_CONFIG_SNIPPET = `# Common Codex config
|
||||
# Add your common TOML configuration here`;
|
||||
|
||||
interface UseCodexCommonConfigProps {
|
||||
codexConfig: string;
|
||||
onConfigChange: (config: string) => void;
|
||||
initialData?: {
|
||||
settingsConfig?: Record<string, unknown>;
|
||||
};
|
||||
selectedPresetId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理 Codex 通用配置片段 (TOML 格式)
|
||||
* 从 config.json 读取和保存,支持从 localStorage 平滑迁移
|
||||
*/
|
||||
export function useCodexCommonConfig({
|
||||
codexConfig,
|
||||
onConfigChange,
|
||||
initialData,
|
||||
selectedPresetId,
|
||||
}: UseCodexCommonConfigProps) {
|
||||
const { t } = useTranslation();
|
||||
const [useCommonConfig, setUseCommonConfig] = useState(false);
|
||||
const [commonConfigSnippet, setCommonConfigSnippetState] = useState<string>(
|
||||
DEFAULT_CODEX_COMMON_CONFIG_SNIPPET,
|
||||
);
|
||||
const [commonConfigError, setCommonConfigError] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isExtracting, setIsExtracting] = useState(false);
|
||||
|
||||
// 用于跟踪是否正在通过通用配置更新
|
||||
const isUpdatingFromCommonConfig = useRef(false);
|
||||
// 用于跟踪新建模式是否已初始化默认勾选
|
||||
const hasInitializedNewMode = useRef(false);
|
||||
|
||||
// 当预设变化时,重置初始化标记,使新预设能够重新触发初始化逻辑
|
||||
useEffect(() => {
|
||||
hasInitializedNewMode.current = false;
|
||||
}, [selectedPresetId]);
|
||||
|
||||
// 初始化:从 config.json 加载,支持从 localStorage 迁移
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
const loadSnippet = async () => {
|
||||
try {
|
||||
// 使用统一 API 加载
|
||||
const snippet = await configApi.getCommonConfigSnippet("codex");
|
||||
|
||||
if (snippet && snippet.trim()) {
|
||||
if (mounted) {
|
||||
setCommonConfigSnippetState(snippet);
|
||||
}
|
||||
} else {
|
||||
// 如果 config.json 中没有,尝试从 localStorage 迁移
|
||||
if (typeof window !== "undefined") {
|
||||
try {
|
||||
const legacySnippet =
|
||||
window.localStorage.getItem(LEGACY_STORAGE_KEY);
|
||||
if (legacySnippet && legacySnippet.trim()) {
|
||||
// 迁移到 config.json
|
||||
await configApi.setCommonConfigSnippet("codex", legacySnippet);
|
||||
if (mounted) {
|
||||
setCommonConfigSnippetState(legacySnippet);
|
||||
}
|
||||
// 清理 localStorage
|
||||
window.localStorage.removeItem(LEGACY_STORAGE_KEY);
|
||||
console.log(
|
||||
"[迁移] Codex 通用配置已从 localStorage 迁移到 config.json",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[迁移] 从 localStorage 迁移失败:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载 Codex 通用配置失败:", error);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadSnippet();
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 初始化时检查通用配置片段(编辑模式)
|
||||
useEffect(() => {
|
||||
if (initialData?.settingsConfig && !isLoading) {
|
||||
const config =
|
||||
typeof initialData.settingsConfig.config === "string"
|
||||
? initialData.settingsConfig.config
|
||||
: "";
|
||||
const hasCommon = hasTomlCommonConfigSnippet(config, commonConfigSnippet);
|
||||
setUseCommonConfig(hasCommon);
|
||||
}
|
||||
}, [initialData, commonConfigSnippet, isLoading]);
|
||||
|
||||
// 新建模式:如果通用配置片段存在且有效,默认启用
|
||||
useEffect(() => {
|
||||
// 仅新建模式、加载完成、尚未初始化过
|
||||
if (!initialData && !isLoading && !hasInitializedNewMode.current) {
|
||||
hasInitializedNewMode.current = true;
|
||||
|
||||
// 检查 TOML 片段是否有实质内容(不只是注释和空行)
|
||||
const lines = commonConfigSnippet.split("\n");
|
||||
const hasContent = lines.some((line) => {
|
||||
const trimmed = line.trim();
|
||||
return trimmed && !trimmed.startsWith("#");
|
||||
});
|
||||
|
||||
if (hasContent) {
|
||||
setUseCommonConfig(true);
|
||||
// 合并通用配置到当前配置
|
||||
const { updatedConfig, error } = updateTomlCommonConfigSnippet(
|
||||
codexConfig,
|
||||
commonConfigSnippet,
|
||||
true,
|
||||
);
|
||||
if (!error) {
|
||||
isUpdatingFromCommonConfig.current = true;
|
||||
onConfigChange(updatedConfig);
|
||||
setTimeout(() => {
|
||||
isUpdatingFromCommonConfig.current = false;
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [
|
||||
initialData,
|
||||
commonConfigSnippet,
|
||||
isLoading,
|
||||
codexConfig,
|
||||
onConfigChange,
|
||||
]);
|
||||
|
||||
// 处理通用配置开关
|
||||
const handleCommonConfigToggle = useCallback(
|
||||
(checked: boolean) => {
|
||||
const { updatedConfig, error: snippetError } =
|
||||
updateTomlCommonConfigSnippet(
|
||||
codexConfig,
|
||||
commonConfigSnippet,
|
||||
checked,
|
||||
);
|
||||
|
||||
if (snippetError) {
|
||||
setCommonConfigError(snippetError);
|
||||
setUseCommonConfig(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setCommonConfigError("");
|
||||
setUseCommonConfig(checked);
|
||||
// 标记正在通过通用配置更新
|
||||
isUpdatingFromCommonConfig.current = true;
|
||||
onConfigChange(updatedConfig);
|
||||
// 在下一个事件循环中重置标记
|
||||
setTimeout(() => {
|
||||
isUpdatingFromCommonConfig.current = false;
|
||||
}, 0);
|
||||
},
|
||||
[codexConfig, commonConfigSnippet, onConfigChange],
|
||||
);
|
||||
|
||||
// 处理通用配置片段变化
|
||||
const handleCommonConfigSnippetChange = useCallback(
|
||||
(value: string) => {
|
||||
const previousSnippet = commonConfigSnippet;
|
||||
setCommonConfigSnippetState(value);
|
||||
|
||||
if (!value.trim()) {
|
||||
setCommonConfigError("");
|
||||
// 保存到 config.json(清空)
|
||||
configApi.setCommonConfigSnippet("codex", "").catch((error) => {
|
||||
console.error("保存 Codex 通用配置失败:", error);
|
||||
setCommonConfigError(
|
||||
t("codexConfig.saveFailed", { error: String(error) }),
|
||||
);
|
||||
});
|
||||
|
||||
if (useCommonConfig) {
|
||||
const { updatedConfig } = updateTomlCommonConfigSnippet(
|
||||
codexConfig,
|
||||
previousSnippet,
|
||||
false,
|
||||
);
|
||||
onConfigChange(updatedConfig);
|
||||
setUseCommonConfig(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// TOML 格式校验较为复杂,暂时不做校验,直接清空错误
|
||||
setCommonConfigError("");
|
||||
// 保存到 config.json
|
||||
configApi.setCommonConfigSnippet("codex", value).catch((error) => {
|
||||
console.error("保存 Codex 通用配置失败:", error);
|
||||
setCommonConfigError(
|
||||
t("codexConfig.saveFailed", { error: String(error) }),
|
||||
);
|
||||
});
|
||||
|
||||
// 若当前启用通用配置,需要替换为最新片段
|
||||
if (useCommonConfig) {
|
||||
const removeResult = updateTomlCommonConfigSnippet(
|
||||
codexConfig,
|
||||
previousSnippet,
|
||||
false,
|
||||
);
|
||||
if (removeResult.error) {
|
||||
setCommonConfigError(removeResult.error);
|
||||
return;
|
||||
}
|
||||
const addResult = updateTomlCommonConfigSnippet(
|
||||
removeResult.updatedConfig,
|
||||
value,
|
||||
true,
|
||||
);
|
||||
|
||||
if (addResult.error) {
|
||||
setCommonConfigError(addResult.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 标记正在通过通用配置更新,避免触发状态检查
|
||||
isUpdatingFromCommonConfig.current = true;
|
||||
onConfigChange(addResult.updatedConfig);
|
||||
// 在下一个事件循环中重置标记
|
||||
setTimeout(() => {
|
||||
isUpdatingFromCommonConfig.current = false;
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
[commonConfigSnippet, codexConfig, useCommonConfig, onConfigChange],
|
||||
);
|
||||
|
||||
// 当配置变化时检查是否包含通用配置(但避免在通过通用配置更新时检查)
|
||||
useEffect(() => {
|
||||
if (isUpdatingFromCommonConfig.current || isLoading) {
|
||||
return;
|
||||
}
|
||||
const hasCommon = hasTomlCommonConfigSnippet(
|
||||
codexConfig,
|
||||
commonConfigSnippet,
|
||||
);
|
||||
setUseCommonConfig(hasCommon);
|
||||
}, [codexConfig, commonConfigSnippet, isLoading]);
|
||||
|
||||
// 从编辑器当前内容提取通用配置片段
|
||||
const handleExtract = useCallback(async () => {
|
||||
setIsExtracting(true);
|
||||
setCommonConfigError("");
|
||||
|
||||
try {
|
||||
const extracted = await configApi.extractCommonConfigSnippet("codex", {
|
||||
settingsConfig: JSON.stringify({
|
||||
config: codexConfig ?? "",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!extracted || !extracted.trim()) {
|
||||
setCommonConfigError(t("codexConfig.extractNoCommonConfig"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新片段状态
|
||||
setCommonConfigSnippetState(extracted);
|
||||
|
||||
// 保存到后端
|
||||
await configApi.setCommonConfigSnippet("codex", extracted);
|
||||
} catch (error) {
|
||||
console.error("提取 Codex 通用配置失败:", error);
|
||||
setCommonConfigError(
|
||||
t("codexConfig.extractFailed", { error: String(error) }),
|
||||
);
|
||||
} finally {
|
||||
setIsExtracting(false);
|
||||
}
|
||||
}, [codexConfig, t]);
|
||||
|
||||
return {
|
||||
useCommonConfig,
|
||||
commonConfigSnippet,
|
||||
commonConfigError,
|
||||
isLoading,
|
||||
isExtracting,
|
||||
handleCommonConfigToggle,
|
||||
handleCommonConfigSnippetChange,
|
||||
handleExtract,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user