feat(common-config): add extract from current provider functionality

- Add backend command to extract common config snippet from current provider
- Automatically extract common config on first run after importing default provider
- Auto-enable common config checkbox in new provider mode when snippet exists
- Refactor Gemini common config to operate on .env instead of config.json
- Add "Extract from Current" button to all three common config modals
- Update i18n translations for new extraction feature
This commit is contained in:
Jason
2026-01-03 23:43:39 +08:00
parent c049c5f2bb
commit 188c94f2e3
17 changed files with 892 additions and 295 deletions
+15
View File
@@ -219,3 +219,18 @@ pub async fn set_common_config_snippet(
.map_err(|e| e.to_string())?;
Ok(())
}
/// 从当前供应商提取通用配置片段
///
/// 读取当前激活供应商的配置,自动排除差异化字段(API Key、模型配置、端点等),
/// 返回可复用的通用配置片段。
#[tauri::command]
pub async fn extract_common_config_snippet(
app_type: String,
state: tauri::State<'_, crate::store::AppState>,
) -> Result<String, String> {
let app = AppType::from_str(&app_type).map_err(|e| e.to_string())?;
crate::services::provider::ProviderService::extract_common_config_snippet(&state, app)
.map_err(|e| e.to_string())
}
+22
View File
@@ -377,6 +377,27 @@ pub fn run() {
) {
Ok(true) => {
log::info!("✓ Imported default provider for {}", app.as_str());
// 首次运行:自动提取通用配置片段(仅当通用配置为空时)
if app_state
.db
.get_config_snippet(app.as_str())
.ok()
.flatten()
.is_none()
{
match crate::services::provider::ProviderService::extract_common_config_snippet(&app_state, app.clone()) {
Ok(snippet) if !snippet.is_empty() && snippet != "{}" => {
if let Err(e) = app_state.db.set_config_snippet(app.as_str(), Some(snippet)) {
log::warn!("✗ Failed to save common config snippet for {}: {e}", app.as_str());
} else {
log::info!("✓ Extracted common config snippet for {}", app.as_str());
}
}
Ok(_) => log::debug!("○ No common config to extract for {}", app.as_str()),
Err(e) => log::debug!("○ Failed to extract common config for {}: {e}", app.as_str()),
}
}
}
Ok(false) => {} // 已有供应商,静默跳过
Err(e) => {
@@ -606,6 +627,7 @@ pub fn run() {
commands::set_claude_common_config_snippet,
commands::get_common_config_snippet,
commands::set_common_config_snippet,
commands::extract_common_config_snippet,
commands::read_live_provider_settings,
commands::get_settings,
commands::save_settings,
+145
View File
@@ -308,6 +308,151 @@ impl ProviderService {
sync_current_to_live(state)
}
/// Extract common config snippet from current provider
///
/// Extracts the current provider's configuration and removes provider-specific fields
/// (API keys, model settings, endpoints) to create a reusable common config snippet.
pub fn extract_common_config_snippet(
state: &AppState,
app_type: AppType,
) -> Result<String, AppError> {
// Get current provider
let current_id = Self::current(state, app_type.clone())?;
if current_id.is_empty() {
return Err(AppError::Message("No current provider".to_string()));
}
let providers = state.db.get_all_providers(app_type.as_str())?;
let provider = providers
.get(&current_id)
.ok_or_else(|| AppError::Message(format!("Provider {} not found", current_id)))?;
match app_type {
AppType::Claude => Self::extract_claude_common_config(&provider.settings_config),
AppType::Codex => Self::extract_codex_common_config(&provider.settings_config),
AppType::Gemini => Self::extract_gemini_common_config(&provider.settings_config),
}
}
/// Extract common config for Claude (JSON format)
fn extract_claude_common_config(settings: &Value) -> Result<String, AppError> {
let mut config = settings.clone();
// Fields to exclude from common config
const ENV_EXCLUDES: &[&str] = &[
// Auth
"ANTHROPIC_API_KEY",
"ANTHROPIC_AUTH_TOKEN",
// Models (5 fields)
"ANTHROPIC_MODEL",
"ANTHROPIC_REASONING_MODEL",
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
"ANTHROPIC_DEFAULT_OPUS_MODEL",
"ANTHROPIC_DEFAULT_SONNET_MODEL",
// Endpoint
"ANTHROPIC_BASE_URL",
];
const TOP_LEVEL_EXCLUDES: &[&str] = &[
"apiBaseUrl",
// Legacy model fields
"primaryModel",
"smallFastModel",
];
// Remove env fields
if let Some(env) = config.get_mut("env").and_then(|v| v.as_object_mut()) {
for key in ENV_EXCLUDES {
env.remove(*key);
}
// If env is empty after removal, remove the env object itself
if env.is_empty() {
config.as_object_mut().map(|obj| obj.remove("env"));
}
}
// Remove top-level fields
if let Some(obj) = config.as_object_mut() {
for key in TOP_LEVEL_EXCLUDES {
obj.remove(*key);
}
}
// Check if result is empty
if config.as_object().is_none_or(|obj| obj.is_empty()) {
return Ok("{}".to_string());
}
serde_json::to_string_pretty(&config)
.map_err(|e| AppError::Message(format!("Serialization failed: {e}")))
}
/// Extract common config for Codex (TOML format)
fn extract_codex_common_config(settings: &Value) -> Result<String, AppError> {
// Codex config is stored as { "auth": {...}, "config": "toml string" }
let config_toml = settings
.get("config")
.and_then(|v| v.as_str())
.unwrap_or("");
if config_toml.is_empty() {
return Ok(String::new());
}
// Lines to exclude (regex patterns for TOML)
let exclude_patterns = [
Regex::new(r"(?m)^\s*model\s*=.*$").unwrap(),
Regex::new(r"(?m)^\s*model_provider\s*=.*$").unwrap(),
Regex::new(r"(?m)^\s*base_url\s*=.*$").unwrap(),
];
let mut result = config_toml.to_string();
for pattern in &exclude_patterns {
result = pattern.replace_all(&result, "").to_string();
}
// Clean up multiple empty lines
let result = Regex::new(r"\n{3,}")
.unwrap()
.replace_all(&result, "\n\n")
.trim()
.to_string();
Ok(result)
}
/// Extract common config for Gemini (JSON format)
///
/// Extracts `.env` values while excluding provider-specific credentials:
/// - GOOGLE_GEMINI_BASE_URL
/// - GEMINI_API_KEY
fn extract_gemini_common_config(settings: &Value) -> Result<String, AppError> {
let env = settings.get("env").and_then(|v| v.as_object());
let mut snippet = serde_json::Map::new();
if let Some(env) = env {
for (key, value) in env {
if key == "GOOGLE_GEMINI_BASE_URL" || key == "GEMINI_API_KEY" {
continue;
}
let Value::String(v) = value else {
continue;
};
let trimmed = v.trim();
if !trimmed.is_empty() {
snippet.insert(key.to_string(), Value::String(trimmed.to_string()));
}
}
}
if snippet.is_empty() {
return Ok("{}".to_string());
}
serde_json::to_string_pretty(&Value::Object(snippet))
.map_err(|e| AppError::Message(format!("Serialization failed: {e}")))
}
/// Import default configuration from live files (re-export)
///
/// Returns `Ok(true)` if imported, `Ok(false)` if skipped.
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react";
import { Save } from "lucide-react";
import { Save, Download, Loader2 } from "lucide-react";
import { useTranslation } from "react-i18next";
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
import { Button } from "@/components/ui/button";
@@ -11,6 +11,8 @@ interface CodexCommonConfigModalProps {
value: string;
onChange: (value: string) => void;
error?: string;
onExtract?: () => void;
isExtracting?: boolean;
}
/**
@@ -23,6 +25,8 @@ export const CodexCommonConfigModal: React.FC<CodexCommonConfigModalProps> = ({
value,
onChange,
error,
onExtract,
isExtracting,
}) => {
const { t } = useTranslation();
const [isDarkMode, setIsDarkMode] = useState(false);
@@ -49,6 +53,24 @@ export const CodexCommonConfigModal: React.FC<CodexCommonConfigModalProps> = ({
onClose={onClose}
footer={
<>
{onExtract && (
<Button
type="button"
variant="outline"
onClick={onExtract}
disabled={isExtracting}
className="gap-2"
>
{isExtracting ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Download className="w-4 h-4" />
)}
{t("codexConfig.extractFromCurrent", {
defaultValue: "从当前供应商提取",
})}
</Button>
)}
<Button type="button" variant="outline" onClick={onClose}>
{t("common.cancel")}
</Button>
@@ -26,6 +26,10 @@ interface CodexConfigEditorProps {
authError: string;
configError: string; // config.toml 错误提示
onExtract?: () => void;
isExtracting?: boolean;
}
const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
@@ -41,6 +45,8 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
commonConfigError,
authError,
configError,
onExtract,
isExtracting,
}) => {
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
@@ -79,6 +85,8 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
value={commonConfigSnippet}
onChange={onCommonConfigSnippetChange}
error={commonConfigError}
onExtract={onExtract}
isExtracting={isExtracting}
/>
</div>
);
@@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Save } from "lucide-react";
import { Save, Download, Loader2 } from "lucide-react";
import JsonEditor from "@/components/JsonEditor";
interface CommonConfigEditorProps {
@@ -17,6 +17,8 @@ interface CommonConfigEditorProps {
onEditClick: () => void;
isModalOpen: boolean;
onModalClose: () => void;
onExtract?: () => void;
isExtracting?: boolean;
}
export function CommonConfigEditor({
@@ -30,6 +32,8 @@ export function CommonConfigEditor({
onEditClick,
isModalOpen,
onModalClose,
onExtract,
isExtracting,
}: CommonConfigEditorProps) {
const { t } = useTranslation();
const [isDarkMode, setIsDarkMode] = useState(false);
@@ -111,6 +115,24 @@ export function CommonConfigEditor({
onClose={onModalClose}
footer={
<>
{onExtract && (
<Button
type="button"
variant="outline"
onClick={onExtract}
disabled={isExtracting}
className="gap-2"
>
{isExtracting ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Download className="w-4 h-4" />
)}
{t("claudeConfig.extractFromCurrent", {
defaultValue: "从当前供应商提取",
})}
</Button>
)}
<Button type="button" variant="outline" onClick={onModalClose}>
{t("common.cancel")}
</Button>
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react";
import { Save } from "lucide-react";
import { Save, Download, Loader2 } from "lucide-react";
import { useTranslation } from "react-i18next";
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
import { Button } from "@/components/ui/button";
@@ -11,15 +11,17 @@ interface GeminiCommonConfigModalProps {
value: string;
onChange: (value: string) => void;
error?: string;
onExtract?: () => void;
isExtracting?: boolean;
}
/**
* GeminiCommonConfigModal - Common Gemini configuration editor modal
* Allows editing of common JSON configuration shared across Gemini providers
* Allows editing of common env snippet shared across Gemini providers
*/
export const GeminiCommonConfigModal: React.FC<
GeminiCommonConfigModalProps
> = ({ isOpen, onClose, value, onChange, error }) => {
> = ({ isOpen, onClose, value, onChange, error, onExtract, isExtracting }) => {
const { t } = useTranslation();
const [isDarkMode, setIsDarkMode] = useState(false);
@@ -47,6 +49,24 @@ export const GeminiCommonConfigModal: React.FC<
onClose={onClose}
footer={
<>
{onExtract && (
<Button
type="button"
variant="outline"
onClick={onExtract}
disabled={isExtracting}
className="gap-2"
>
{isExtracting ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Download className="w-4 h-4" />
)}
{t("geminiConfig.extractFromCurrent", {
defaultValue: "从当前供应商提取",
})}
</Button>
)}
<Button type="button" variant="outline" onClick={onClose}>
{t("common.cancel")}
</Button>
@@ -61,7 +81,7 @@ export const GeminiCommonConfigModal: React.FC<
<p className="text-sm text-muted-foreground">
{t("geminiConfig.commonConfigHint", {
defaultValue:
"通用配置片段将合并到所有启用它的 Gemini 供应商配置中",
"该片段会写入 Gemini 的 .env(不允许包含 GOOGLE_GEMINI_BASE_URL、GEMINI_API_KEY",
})}
</p>
@@ -69,9 +89,7 @@ export const GeminiCommonConfigModal: React.FC<
value={value}
onChange={onChange}
placeholder={`{
"timeout": 30000,
"maxRetries": 3,
"customField": "value"
"GEMINI_MODEL": "gemini-3-pro-preview"
}`}
darkMode={isDarkMode}
rows={16}
@@ -15,6 +15,8 @@ interface GeminiConfigEditorProps {
commonConfigError: string;
envError: string;
configError: string;
onExtract?: () => void;
isExtracting?: boolean;
}
const GeminiConfigEditor: React.FC<GeminiConfigEditorProps> = ({
@@ -30,6 +32,8 @@ const GeminiConfigEditor: React.FC<GeminiConfigEditorProps> = ({
commonConfigError,
envError,
configError,
onExtract,
isExtracting,
}) => {
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
@@ -48,16 +52,16 @@ const GeminiConfigEditor: React.FC<GeminiConfigEditorProps> = ({
onChange={onEnvChange}
onBlur={onEnvBlur}
error={envError}
useCommonConfig={useCommonConfig}
onCommonConfigToggle={onCommonConfigToggle}
onEditCommonConfig={() => setIsCommonConfigModalOpen(true)}
commonConfigError={commonConfigError}
/>
{/* Config JSON Section */}
<GeminiConfigSection
value={configValue}
onChange={onConfigChange}
useCommonConfig={useCommonConfig}
onCommonConfigToggle={onCommonConfigToggle}
onEditCommonConfig={() => setIsCommonConfigModalOpen(true)}
commonConfigError={commonConfigError}
configError={configError}
/>
@@ -68,6 +72,8 @@ const GeminiConfigEditor: React.FC<GeminiConfigEditorProps> = ({
value={commonConfigSnippet}
onChange={onCommonConfigSnippetChange}
error={commonConfigError}
onExtract={onExtract}
isExtracting={isExtracting}
/>
</div>
);
@@ -7,6 +7,10 @@ interface GeminiEnvSectionProps {
onChange: (value: string) => void;
onBlur?: () => void;
error?: string;
useCommonConfig: boolean;
onCommonConfigToggle: (checked: boolean) => void;
onEditCommonConfig: () => void;
commonConfigError?: string;
}
/**
@@ -17,6 +21,10 @@ export const GeminiEnvSection: React.FC<GeminiEnvSectionProps> = ({
onChange,
onBlur,
error,
useCommonConfig,
onCommonConfigToggle,
onEditCommonConfig,
commonConfigError,
}) => {
const { t } = useTranslation();
const [isDarkMode, setIsDarkMode] = useState(false);
@@ -43,92 +51,14 @@ export const GeminiEnvSection: React.FC<GeminiEnvSectionProps> = ({
}
};
return (
<div className="space-y-2">
<label
htmlFor="geminiEnv"
className="block text-sm font-medium text-foreground"
>
{t("geminiConfig.envFile", { defaultValue: "环境变量 (.env)" })}
</label>
<JsonEditor
value={value}
onChange={handleChange}
placeholder={`GOOGLE_GEMINI_BASE_URL=https://your-api-endpoint.com/
GEMINI_API_KEY=sk-your-api-key-here
GEMINI_MODEL=gemini-3-pro-preview`}
darkMode={isDarkMode}
rows={6}
showValidation={false}
language="javascript"
/>
{error && (
<p className="text-xs text-red-500 dark:text-red-400">{error}</p>
)}
{!error && (
<p className="text-xs text-muted-foreground">
{t("geminiConfig.envFileHint", {
defaultValue: "使用 .env 格式配置 Gemini 环境变量",
})}
</p>
)}
</div>
);
};
interface GeminiConfigSectionProps {
value: string;
onChange: (value: string) => void;
useCommonConfig: boolean;
onCommonConfigToggle: (checked: boolean) => void;
onEditCommonConfig: () => void;
commonConfigError?: string;
configError?: string;
}
/**
* GeminiConfigSection - Config JSON editor section with common config support
*/
export const GeminiConfigSection: React.FC<GeminiConfigSectionProps> = ({
value,
onChange,
useCommonConfig,
onCommonConfigToggle,
onEditCommonConfig,
commonConfigError,
configError,
}) => {
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();
}, []);
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<label
htmlFor="geminiConfig"
htmlFor="geminiEnv"
className="block text-sm font-medium text-foreground"
>
{t("geminiConfig.configJson", {
defaultValue: "配置文件 (config.json)",
})}
{t("geminiConfig.envFile", { defaultValue: "环境变量 (.env)" })}
</label>
<label className="inline-flex items-center gap-2 text-sm text-muted-foreground cursor-pointer">
@@ -162,6 +92,76 @@ export const GeminiConfigSection: React.FC<GeminiConfigSectionProps> = ({
</p>
)}
<JsonEditor
value={value}
onChange={handleChange}
placeholder={`GOOGLE_GEMINI_BASE_URL=https://your-api-endpoint.com/
GEMINI_API_KEY=sk-your-api-key-here
GEMINI_MODEL=gemini-3-pro-preview`}
darkMode={isDarkMode}
rows={6}
showValidation={false}
language="javascript"
/>
{error && (
<p className="text-xs text-red-500 dark:text-red-400">{error}</p>
)}
{!error && (
<p className="text-xs text-muted-foreground">
{t("geminiConfig.envFileHint", {
defaultValue: "使用 .env 格式配置 Gemini 环境变量",
})}
</p>
)}
</div>
);
};
interface GeminiConfigSectionProps {
value: string;
onChange: (value: string) => void;
configError?: string;
}
/**
* GeminiConfigSection - Config JSON editor section with common config support
*/
export const GeminiConfigSection: React.FC<GeminiConfigSectionProps> = ({
value,
onChange,
configError,
}) => {
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();
}, []);
return (
<div className="space-y-2">
<label
htmlFor="geminiConfig"
className="block text-sm font-medium text-foreground"
>
{t("geminiConfig.configJson", {
defaultValue: "配置文件 (config.json)",
})}
</label>
<JsonEditor
value={value}
onChange={onChange}
@@ -352,6 +352,8 @@ export function ProviderForm({
commonConfigError,
handleCommonConfigToggle,
handleCommonConfigSnippetChange,
isExtracting: isClaudeExtracting,
handleExtract: handleClaudeExtract,
} = useCommonConfigSnippet({
settingsConfig: form.watch("settingsConfig"),
onConfigChange: (config) => form.setValue("settingsConfig", config),
@@ -365,6 +367,8 @@ export function ProviderForm({
commonConfigError: codexCommonConfigError,
handleCommonConfigToggle: handleCodexCommonConfigToggle,
handleCommonConfigSnippetChange: handleCodexCommonConfigSnippetChange,
isExtracting: isCodexExtracting,
handleExtract: handleCodexExtract,
} = useCodexCommonConfig({
codexConfig,
onConfigChange: handleCodexConfigChange,
@@ -387,6 +391,7 @@ export function ProviderForm({
handleGeminiConfigChange,
resetGeminiConfig,
envStringToObj,
envObjToString,
} = useGeminiConfigState({
initialData: appId === "gemini" ? initialData : undefined,
});
@@ -447,9 +452,13 @@ export function ProviderForm({
commonConfigError: geminiCommonConfigError,
handleCommonConfigToggle: handleGeminiCommonConfigToggle,
handleCommonConfigSnippetChange: handleGeminiCommonConfigSnippetChange,
isExtracting: isGeminiExtracting,
handleExtract: handleGeminiExtract,
} = useGeminiCommonConfig({
configValue: geminiConfig,
onConfigChange: handleGeminiConfigChange,
envValue: geminiEnv,
onEnvChange: handleGeminiEnvChange,
envStringToObj,
envObjToString,
initialData: appId === "gemini" ? initialData : undefined,
});
@@ -927,6 +936,8 @@ export function ProviderForm({
commonConfigError={codexCommonConfigError}
authError={codexAuthError}
configError={codexConfigError}
onExtract={handleCodexExtract}
isExtracting={isCodexExtracting}
/>
{/* 配置验证错误显示 */}
<FormField
@@ -955,6 +966,8 @@ export function ProviderForm({
commonConfigError={geminiCommonConfigError}
envError={envError}
configError={geminiConfigError}
onExtract={handleGeminiExtract}
isExtracting={isGeminiExtracting}
/>
{/* 配置验证错误显示 */}
<FormField
@@ -980,6 +993,8 @@ export function ProviderForm({
onEditClick={() => setIsCommonConfigModalOpen(true)}
isModalOpen={isCommonConfigModalOpen}
onModalClose={() => setIsCommonConfigModalOpen(false)}
onExtract={handleClaudeExtract}
isExtracting={isClaudeExtracting}
/>
{/* 配置验证错误显示 */}
<FormField
@@ -1,4 +1,5 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { useTranslation } from "react-i18next";
import {
updateTomlCommonConfigSnippet,
hasTomlCommonConfigSnippet,
@@ -26,15 +27,19 @@ export function useCodexCommonConfig({
onConfigChange,
initialData,
}: 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);
// 初始化:从 config.json 加载,支持从 localStorage 迁移
useEffect(() => {
@@ -100,6 +105,38 @@ export function useCodexCommonConfig({
}
}, [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) => {
@@ -140,7 +177,7 @@ export function useCodexCommonConfig({
// 保存到 config.json(清空)
configApi.setCommonConfigSnippet("codex", "").catch((error) => {
console.error("保存 Codex 通用配置失败:", error);
setCommonConfigError(`保存失败: ${error}`);
setCommonConfigError(t("codexConfig.saveFailed", { error: String(error) }));
});
if (useCommonConfig) {
@@ -160,7 +197,7 @@ export function useCodexCommonConfig({
// 保存到 config.json
configApi.setCommonConfigSnippet("codex", value).catch((error) => {
console.error("保存 Codex 通用配置失败:", error);
setCommonConfigError(`保存失败: ${error}`);
setCommonConfigError(t("codexConfig.saveFailed", { error: String(error) }));
});
// 若当前启用通用配置,需要替换为最新片段
@@ -209,12 +246,40 @@ export function useCodexCommonConfig({
setUseCommonConfig(hasCommon);
}, [codexConfig, commonConfigSnippet, isLoading]);
// 从当前供应商提取通用配置片段
const handleExtract = useCallback(async () => {
setIsExtracting(true);
setCommonConfigError("");
try {
const extracted = await configApi.extractCommonConfigSnippet("codex");
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);
}
}, []);
return {
useCommonConfig,
commonConfigSnippet,
commonConfigError,
isLoading,
isExtracting,
handleCommonConfigToggle,
handleCommonConfigSnippetChange,
handleExtract,
};
}
@@ -1,4 +1,5 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { useTranslation } from "react-i18next";
import {
updateCommonConfigSnippet,
hasCommonConfigSnippet,
@@ -28,15 +29,19 @@ export function useCommonConfigSnippet({
onConfigChange,
initialData,
}: UseCommonConfigSnippetProps) {
const { t } = useTranslation();
const [useCommonConfig, setUseCommonConfig] = useState(false);
const [commonConfigSnippet, setCommonConfigSnippetState] = useState<string>(
DEFAULT_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);
// 初始化:从 config.json 加载,支持从 localStorage 迁移
useEffect(() => {
@@ -102,6 +107,38 @@ export function useCommonConfigSnippet({
}
}, [initialData, commonConfigSnippet, isLoading]);
// 新建模式:如果通用配置片段存在且有效,默认启用
useEffect(() => {
// 仅新建模式、加载完成、尚未初始化过
if (!initialData && !isLoading && !hasInitializedNewMode.current) {
hasInitializedNewMode.current = true;
// 检查片段是否有实质内容
try {
const snippetObj = JSON.parse(commonConfigSnippet);
const hasContent = Object.keys(snippetObj).length > 0;
if (hasContent) {
setUseCommonConfig(true);
// 合并通用配置到当前配置
const { updatedConfig, error } = updateCommonConfigSnippet(
settingsConfig,
commonConfigSnippet,
true,
);
if (!error) {
isUpdatingFromCommonConfig.current = true;
onConfigChange(updatedConfig);
setTimeout(() => {
isUpdatingFromCommonConfig.current = false;
}, 0);
}
}
} catch {
// ignore parse error
}
}
}, [initialData, commonConfigSnippet, isLoading, settingsConfig, onConfigChange]);
// 处理通用配置开关
const handleCommonConfigToggle = useCallback(
(checked: boolean) => {
@@ -141,7 +178,7 @@ export function useCommonConfigSnippet({
// 保存到 config.json(清空)
configApi.setCommonConfigSnippet("claude", "").catch((error) => {
console.error("保存通用配置失败:", error);
setCommonConfigError(`保存失败: ${error}`);
setCommonConfigError(t("claudeConfig.saveFailed", { error: String(error) }));
});
if (useCommonConfig) {
@@ -165,7 +202,7 @@ export function useCommonConfigSnippet({
// 保存到 config.json
configApi.setCommonConfigSnippet("claude", value).catch((error) => {
console.error("保存通用配置失败:", error);
setCommonConfigError(`保存失败: ${error}`);
setCommonConfigError(t("claudeConfig.saveFailed", { error: String(error) }));
});
}
@@ -215,12 +252,47 @@ export function useCommonConfigSnippet({
setUseCommonConfig(hasCommon);
}, [settingsConfig, commonConfigSnippet, isLoading]);
// 从当前供应商提取通用配置片段
const handleExtract = useCallback(async () => {
setIsExtracting(true);
setCommonConfigError("");
try {
const extracted = await configApi.extractCommonConfigSnippet("claude");
if (!extracted || extracted === "{}") {
setCommonConfigError(t("claudeConfig.extractNoCommonConfig"));
return;
}
// 验证 JSON 格式
const validationError = validateJsonConfig(extracted, "提取的配置");
if (validationError) {
setCommonConfigError(validationError);
return;
}
// 更新片段状态
setCommonConfigSnippetState(extracted);
// 保存到后端
await configApi.setCommonConfigSnippet("claude", extracted);
} catch (error) {
console.error("提取通用配置失败:", error);
setCommonConfigError(t("claudeConfig.extractFailed", { error: String(error) }));
} finally {
setIsExtracting(false);
}
}, []);
return {
useCommonConfig,
commonConfigSnippet,
commonConfigError,
isLoading,
isExtracting,
handleCommonConfigToggle,
handleCommonConfigSnippetChange,
handleExtract,
};
}
@@ -1,126 +1,147 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { useTranslation } from "react-i18next";
import { configApi } from "@/lib/api";
const LEGACY_STORAGE_KEY = "cc-switch:gemini-common-config-snippet";
const DEFAULT_GEMINI_COMMON_CONFIG_SNIPPET = `{
"timeout": 30000,
"maxRetries": 3
}`;
const DEFAULT_GEMINI_COMMON_CONFIG_SNIPPET = "{}";
const GEMINI_COMMON_ENV_FORBIDDEN_KEYS = [
"GOOGLE_GEMINI_BASE_URL",
"GEMINI_API_KEY",
] as const;
type GeminiForbiddenEnvKey = (typeof GEMINI_COMMON_ENV_FORBIDDEN_KEYS)[number];
interface UseGeminiCommonConfigProps {
configValue: string;
onConfigChange: (config: string) => void;
envValue: string;
onEnvChange: (env: string) => void;
envStringToObj: (envString: string) => Record<string, string>;
envObjToString: (envObj: Record<string, unknown>) => string;
initialData?: {
settingsConfig?: Record<string, unknown>;
};
}
/**
* 深度合并两个对象(用于合并通用配置)
*/
function deepMerge(target: any, source: any): any {
if (typeof target !== "object" || target === null) {
return source;
}
if (typeof source !== "object" || source === null) {
return target;
}
if (Array.isArray(source)) {
return source;
}
const result = { ...target };
for (const key of Object.keys(source)) {
if (typeof source[key] === "object" && !Array.isArray(source[key])) {
result[key] = deepMerge(result[key], source[key]);
} else {
result[key] = source[key];
}
}
return result;
}
/**
* 从配置中移除通用配置片段(递归比较)
*/
function removeCommonConfig(config: any, commonConfig: any): any {
if (typeof config !== "object" || config === null) {
return config;
}
if (typeof commonConfig !== "object" || commonConfig === null) {
return config;
}
const result = { ...config };
for (const key of Object.keys(commonConfig)) {
if (result[key] === undefined) continue;
// 如果值完全相等,删除该键
if (JSON.stringify(result[key]) === JSON.stringify(commonConfig[key])) {
delete result[key];
} else if (
typeof result[key] === "object" &&
!Array.isArray(result[key]) &&
typeof commonConfig[key] === "object" &&
!Array.isArray(commonConfig[key])
) {
// 递归移除嵌套对象
result[key] = removeCommonConfig(result[key], commonConfig[key]);
// 如果移除后对象为空,删除该键
if (Object.keys(result[key]).length === 0) {
delete result[key];
}
}
}
return result;
}
/**
* 检查配置中是否包含通用配置片段
*/
function hasCommonConfigSnippet(config: any, commonConfig: any): boolean {
if (typeof config !== "object" || config === null) return false;
if (typeof commonConfig !== "object" || commonConfig === null) return false;
for (const key of Object.keys(commonConfig)) {
if (config[key] === undefined) return false;
if (JSON.stringify(config[key]) !== JSON.stringify(commonConfig[key])) {
// 检查嵌套对象
if (
typeof config[key] === "object" &&
!Array.isArray(config[key]) &&
typeof commonConfig[key] === "object" &&
!Array.isArray(commonConfig[key])
) {
if (!hasCommonConfigSnippet(config[key], commonConfig[key])) {
return false;
}
} else {
return false;
}
}
}
return true;
function isPlainObject(value: unknown): value is Record<string, unknown> {
return (
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
Object.prototype.toString.call(value) === "[object Object]"
);
}
/**
* 管理 Gemini 通用配置片段 (JSON 格式)
* 从 config.json 读取和保存,支持从 localStorage 平滑迁移
* 写入 Gemini 的 .env,但会排除以下敏感字段:
* - GOOGLE_GEMINI_BASE_URL
* - GEMINI_API_KEY
*/
export function useGeminiCommonConfig({
configValue,
onConfigChange,
envValue,
onEnvChange,
envStringToObj,
envObjToString,
initialData,
}: UseGeminiCommonConfigProps) {
const { t } = useTranslation();
const [useCommonConfig, setUseCommonConfig] = useState(false);
const [commonConfigSnippet, setCommonConfigSnippetState] = useState<string>(
DEFAULT_GEMINI_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);
const parseSnippetEnv = useCallback(
(
snippetString: string,
): { env: Record<string, string>; error?: string } => {
const trimmed = snippetString.trim();
if (!trimmed) {
return { env: {} };
}
let parsed: unknown;
try {
parsed = JSON.parse(trimmed);
} catch {
return { env: {}, error: t("geminiConfig.invalidJsonFormat") };
}
if (!isPlainObject(parsed)) {
return { env: {}, error: t("geminiConfig.invalidJsonFormat") };
}
const keys = Object.keys(parsed);
const forbiddenKeys = keys.filter((key) =>
GEMINI_COMMON_ENV_FORBIDDEN_KEYS.includes(key as GeminiForbiddenEnvKey),
);
if (forbiddenKeys.length > 0) {
return {
env: {},
error: t("geminiConfig.commonConfigInvalidKeys", {
keys: forbiddenKeys.join(", "),
}),
};
}
const env: Record<string, string> = {};
for (const [key, value] of Object.entries(parsed)) {
if (typeof value !== "string") {
return {
env: {},
error: t("geminiConfig.commonConfigInvalidValues"),
};
}
const normalized = value.trim();
if (!normalized) continue;
env[key] = normalized;
}
return { env };
},
[t],
);
const hasEnvCommonConfigSnippet = useCallback(
(envObj: Record<string, string>, snippetEnv: Record<string, string>) => {
const entries = Object.entries(snippetEnv);
if (entries.length === 0) return false;
return entries.every(([key, value]) => envObj[key] === value);
},
[],
);
const applySnippetToEnv = useCallback(
(envObj: Record<string, string>, snippetEnv: Record<string, string>) => {
const updated = { ...envObj };
for (const [key, value] of Object.entries(snippetEnv)) {
if (typeof value === "string") {
updated[key] = value;
}
}
return updated;
},
[],
);
const removeSnippetFromEnv = useCallback(
(envObj: Record<string, string>, snippetEnv: Record<string, string>) => {
const updated = { ...envObj };
for (const [key, value] of Object.entries(snippetEnv)) {
if (typeof value === "string" && updated[key] === value) {
delete updated[key];
}
}
return updated;
},
[],
);
// 初始化:从 config.json 加载,支持从 localStorage 迁移
useEffect(() => {
@@ -142,6 +163,13 @@ export function useGeminiCommonConfig({
const legacySnippet =
window.localStorage.getItem(LEGACY_STORAGE_KEY);
if (legacySnippet && legacySnippet.trim()) {
const parsed = parseSnippetEnv(legacySnippet);
if (parsed.error) {
console.warn(
"[迁移] legacy Gemini 通用配置片段格式不符合当前规则,跳过迁移",
);
return;
}
// 迁移到 config.json
await configApi.setCommonConfigSnippet("gemini", legacySnippet);
if (mounted) {
@@ -172,60 +200,110 @@ export function useGeminiCommonConfig({
return () => {
mounted = false;
};
}, []);
}, [parseSnippetEnv]);
// 初始化时检查通用配置片段(编辑模式)
useEffect(() => {
if (initialData?.settingsConfig && !isLoading) {
try {
const config =
typeof initialData.settingsConfig.config === "object"
? initialData.settingsConfig.config
const env =
isPlainObject(initialData.settingsConfig.env) &&
Object.keys(initialData.settingsConfig.env).length > 0
? (initialData.settingsConfig.env as Record<string, string>)
: {};
const commonConfigObj = JSON.parse(commonConfigSnippet);
const hasCommon = hasCommonConfigSnippet(config, commonConfigObj);
const parsed = parseSnippetEnv(commonConfigSnippet);
if (parsed.error) return;
const hasCommon = hasEnvCommonConfigSnippet(
env,
parsed.env as Record<string, string>,
);
setUseCommonConfig(hasCommon);
} catch {
// ignore parse error
}
}
}, [initialData, commonConfigSnippet, isLoading]);
}, [
commonConfigSnippet,
hasEnvCommonConfigSnippet,
initialData,
isLoading,
parseSnippetEnv,
]);
// 新建模式:如果通用配置片段存在且有效,默认启用
useEffect(() => {
// 仅新建模式、加载完成、尚未初始化过
if (!initialData && !isLoading && !hasInitializedNewMode.current) {
hasInitializedNewMode.current = true;
const parsed = parseSnippetEnv(commonConfigSnippet);
if (parsed.error) return;
const hasContent = Object.keys(parsed.env).length > 0;
if (!hasContent) return;
setUseCommonConfig(true);
const currentEnv = envStringToObj(envValue);
const merged = applySnippetToEnv(currentEnv, parsed.env);
const nextEnvString = envObjToString(merged);
isUpdatingFromCommonConfig.current = true;
onEnvChange(nextEnvString);
setTimeout(() => {
isUpdatingFromCommonConfig.current = false;
}, 0);
}
}, [
initialData,
isLoading,
commonConfigSnippet,
envValue,
envStringToObj,
envObjToString,
applySnippetToEnv,
onEnvChange,
parseSnippetEnv,
]);
// 处理通用配置开关
const handleCommonConfigToggle = useCallback(
(checked: boolean) => {
try {
const configObj = configValue.trim() ? JSON.parse(configValue) : {};
const commonConfigObj = JSON.parse(commonConfigSnippet);
let updatedConfig: any;
if (checked) {
// 合并通用配置
updatedConfig = deepMerge(configObj, commonConfigObj);
} else {
// 移除通用配置
updatedConfig = removeCommonConfig(configObj, commonConfigObj);
}
setCommonConfigError("");
setUseCommonConfig(checked);
// 标记正在通过通用配置更新
isUpdatingFromCommonConfig.current = true;
onConfigChange(JSON.stringify(updatedConfig, null, 2));
// 在下一个事件循环中重置标记
setTimeout(() => {
isUpdatingFromCommonConfig.current = false;
}, 0);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
setCommonConfigError(`配置合并失败: ${errorMessage}`);
const parsed = parseSnippetEnv(commonConfigSnippet);
if (parsed.error) {
setCommonConfigError(parsed.error);
setUseCommonConfig(false);
return;
}
if (Object.keys(parsed.env).length === 0) {
setCommonConfigError(t("geminiConfig.noCommonConfigToApply"));
setUseCommonConfig(false);
return;
}
const currentEnv = envStringToObj(envValue);
const updatedEnvObj = checked
? applySnippetToEnv(currentEnv, parsed.env)
: removeSnippetFromEnv(currentEnv, parsed.env);
setCommonConfigError("");
setUseCommonConfig(checked);
isUpdatingFromCommonConfig.current = true;
onEnvChange(envObjToString(updatedEnvObj));
setTimeout(() => {
isUpdatingFromCommonConfig.current = false;
}, 0);
},
[configValue, commonConfigSnippet, onConfigChange],
[
applySnippetToEnv,
commonConfigSnippet,
envObjToString,
envStringToObj,
envValue,
onEnvChange,
parseSnippetEnv,
removeSnippetFromEnv,
t,
],
);
// 处理通用配置片段变化
@@ -239,95 +317,132 @@ export function useGeminiCommonConfig({
// 保存到 config.json(清空)
configApi.setCommonConfigSnippet("gemini", "").catch((error) => {
console.error("保存 Gemini 通用配置失败:", error);
setCommonConfigError(`保存失败: ${error}`);
setCommonConfigError(t("geminiConfig.saveFailed", { error: String(error) }));
});
if (useCommonConfig) {
// 移除旧的通用配置
try {
const configObj = configValue.trim() ? JSON.parse(configValue) : {};
const previousCommonConfigObj = JSON.parse(previousSnippet);
const updatedConfig = removeCommonConfig(
configObj,
previousCommonConfigObj,
);
onConfigChange(JSON.stringify(updatedConfig, null, 2));
setUseCommonConfig(false);
} catch {
// ignore
const parsed = parseSnippetEnv(previousSnippet);
if (!parsed.error && Object.keys(parsed.env).length > 0) {
const currentEnv = envStringToObj(envValue);
const updatedEnv = removeSnippetFromEnv(currentEnv, parsed.env);
onEnvChange(envObjToString(updatedEnv));
}
setUseCommonConfig(false);
}
return;
}
// 校验 JSON 格式
try {
JSON.parse(value);
setCommonConfigError("");
// 保存到 config.json
configApi.setCommonConfigSnippet("gemini", value).catch((error) => {
console.error("保存 Gemini 通用配置失败:", error);
setCommonConfigError(`保存失败: ${error}`);
});
} catch {
setCommonConfigError("通用配置片段格式错误(必须是有效的 JSON)");
const parsed = parseSnippetEnv(value);
if (parsed.error) {
setCommonConfigError(parsed.error);
return;
}
setCommonConfigError("");
configApi.setCommonConfigSnippet("gemini", value).catch((error) => {
console.error("保存 Gemini 通用配置失败:", error);
setCommonConfigError(t("geminiConfig.saveFailed", { error: String(error) }));
});
// 若当前启用通用配置,需要替换为最新片段
if (useCommonConfig) {
try {
const configObj = configValue.trim() ? JSON.parse(configValue) : {};
const previousCommonConfigObj = JSON.parse(previousSnippet);
const newCommonConfigObj = JSON.parse(value);
const prevParsed = parseSnippetEnv(previousSnippet);
const prevEnv = prevParsed.error ? {} : prevParsed.env;
const nextEnv = parsed.env;
const currentEnv = envStringToObj(envValue);
// 先移除旧的通用配置
const withoutOld = removeCommonConfig(
configObj,
previousCommonConfigObj,
);
// 再合并新的通用配置
const withNew = deepMerge(withoutOld, newCommonConfigObj);
const withoutOld =
Object.keys(prevEnv).length > 0
? removeSnippetFromEnv(currentEnv, prevEnv)
: currentEnv;
const withNew =
Object.keys(nextEnv).length > 0
? applySnippetToEnv(withoutOld, nextEnv)
: withoutOld;
// 标记正在通过通用配置更新,避免触发状态检查
isUpdatingFromCommonConfig.current = true;
onConfigChange(JSON.stringify(withNew, null, 2));
// 在下一个事件循环中重置标记
setTimeout(() => {
isUpdatingFromCommonConfig.current = false;
}, 0);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
setCommonConfigError(`配置替换失败: ${errorMessage}`);
}
isUpdatingFromCommonConfig.current = true;
onEnvChange(envObjToString(withNew));
setTimeout(() => {
isUpdatingFromCommonConfig.current = false;
}, 0);
}
},
[commonConfigSnippet, configValue, useCommonConfig, onConfigChange],
[
applySnippetToEnv,
commonConfigSnippet,
envObjToString,
envStringToObj,
envValue,
onEnvChange,
parseSnippetEnv,
removeSnippetFromEnv,
t,
useCommonConfig,
],
);
// 当配置变化时检查是否包含通用配置(但避免在通过通用配置更新时检查)
// 当 env 变化时检查是否包含通用配置(但避免在通过通用配置更新时检查)
useEffect(() => {
if (isUpdatingFromCommonConfig.current || isLoading) {
return;
}
const parsed = parseSnippetEnv(commonConfigSnippet);
if (parsed.error) return;
const envObj = envStringToObj(envValue);
setUseCommonConfig(
hasEnvCommonConfigSnippet(envObj, parsed.env as Record<string, string>),
);
}, [
envValue,
commonConfigSnippet,
envStringToObj,
hasEnvCommonConfigSnippet,
isLoading,
parseSnippetEnv,
]);
// 从当前供应商提取通用配置片段
const handleExtract = useCallback(async () => {
setIsExtracting(true);
setCommonConfigError("");
try {
const configObj = configValue.trim() ? JSON.parse(configValue) : {};
const commonConfigObj = JSON.parse(commonConfigSnippet);
const hasCommon = hasCommonConfigSnippet(configObj, commonConfigObj);
setUseCommonConfig(hasCommon);
} catch {
// ignore parse error
const extracted = await configApi.extractCommonConfigSnippet("gemini");
if (!extracted || extracted === "{}") {
setCommonConfigError(t("geminiConfig.extractNoCommonConfig"));
return;
}
// 验证 JSON 格式
const parsed = parseSnippetEnv(extracted);
if (parsed.error) {
setCommonConfigError(t("geminiConfig.extractedConfigInvalid"));
return;
}
// 更新片段状态
setCommonConfigSnippetState(extracted);
// 保存到后端
await configApi.setCommonConfigSnippet("gemini", extracted);
} catch (error) {
console.error("提取 Gemini 通用配置失败:", error);
setCommonConfigError(t("geminiConfig.extractFailed", { error: String(error) }));
} finally {
setIsExtracting(false);
}
}, [configValue, commonConfigSnippet, isLoading]);
}, [parseSnippetEnv, t]);
return {
useCommonConfig,
commonConfigSnippet,
commonConfigError,
isLoading,
isExtracting,
handleCommonConfigToggle,
handleCommonConfigSnippetChange,
handleExtract,
};
}
+22 -3
View File
@@ -53,7 +53,11 @@
"editCommonConfig": "Edit Common Config",
"editCommonConfigTitle": "Edit Common Config Snippet",
"commonConfigHint": "This snippet will be merged into settings.json when 'Write Common Config' is checked",
"fullSettingsHint": "Full Claude Code settings.json content"
"fullSettingsHint": "Full Claude Code settings.json content",
"extractFromCurrent": "Extract from Current Provider",
"extractNoCommonConfig": "No common config available to extract from current provider",
"extractFailed": "Extract failed: {{error}}",
"saveFailed": "Save failed: {{error}}"
},
"header": {
"viewOnGithub": "View on GitHub",
@@ -393,7 +397,11 @@
"editCommonConfig": "Edit Common Config",
"editCommonConfigTitle": "Edit Codex Common Config Snippet",
"commonConfigHint": "This snippet will be appended to the end of config.toml when 'Write Common Config' is checked",
"apiUrlLabel": "API Request URL"
"apiUrlLabel": "API Request URL",
"extractFromCurrent": "Extract from Current Provider",
"extractNoCommonConfig": "No common config available to extract from current provider",
"extractFailed": "Extract failed: {{error}}",
"saveFailed": "Save failed: {{error}}"
},
"geminiConfig": {
"envFile": "Environment Variables (.env)",
@@ -403,7 +411,18 @@
"writeCommonConfig": "Write Common Config",
"editCommonConfig": "Edit Common Config",
"editCommonConfigTitle": "Edit Gemini Common Config Snippet",
"commonConfigHint": "Common config snippet will be merged into all Gemini providers with it enabled"
"commonConfigHint": "This snippet writes to Gemini .env (GOOGLE_GEMINI_BASE_URL and GEMINI_API_KEY are not allowed)",
"extractFromCurrent": "Extract from Current Provider",
"extractNoCommonConfig": "No common config available to extract from current provider",
"extractFailed": "Extract failed: {{error}}",
"saveFailed": "Save failed: {{error}}",
"extractedConfigInvalid": "Extracted config format is invalid",
"invalidJsonFormat": "Common config snippet format error (must be valid JSON)",
"commonConfigInvalidKeys": "Common config snippet must not include GOOGLE_GEMINI_BASE_URL or GEMINI_API_KEY (found: {{keys}})",
"commonConfigInvalidValues": "Common config snippet values must be strings",
"noCommonConfigToApply": "Common config snippet is empty or has no applicable entries",
"configMergeFailed": "Config merge failed: {{error}}",
"configReplaceFailed": "Config replace failed: {{error}}"
},
"providerPreset": {
"label": "Provider Preset",
+22 -3
View File
@@ -53,7 +53,11 @@
"editCommonConfig": "共通設定を編集",
"editCommonConfigTitle": "共通設定スニペットを編集",
"commonConfigHint": "「共通設定を書き込む」がオンのとき settings.json にマージされます",
"fullSettingsHint": "Claude Code の settings.json 全文"
"fullSettingsHint": "Claude Code の settings.json 全文",
"extractFromCurrent": "現在のプロバイダーから抽出",
"extractNoCommonConfig": "現在のプロバイダーから抽出できる共通設定がありません",
"extractFailed": "抽出に失敗しました: {{error}}",
"saveFailed": "保存に失敗しました: {{error}}"
},
"header": {
"viewOnGithub": "GitHub で見る",
@@ -393,7 +397,11 @@
"editCommonConfig": "共通設定を編集",
"editCommonConfigTitle": "Codex 共通設定スニペットを編集",
"commonConfigHint": "「共通設定を書き込む」がオンの場合、config.toml の末尾に追記されます",
"apiUrlLabel": "API リクエスト URL"
"apiUrlLabel": "API リクエスト URL",
"extractFromCurrent": "現在のプロバイダーから抽出",
"extractNoCommonConfig": "現在のプロバイダーから抽出できる共通設定がありません",
"extractFailed": "抽出に失敗しました: {{error}}",
"saveFailed": "保存に失敗しました: {{error}}"
},
"geminiConfig": {
"envFile": "環境変数 (.env)",
@@ -403,7 +411,18 @@
"writeCommonConfig": "共通設定を書き込む",
"editCommonConfig": "共通設定を編集",
"editCommonConfigTitle": "Gemini 共通設定スニペットを編集",
"commonConfigHint": "共通設定スニペットは、この機能をオンにしたすべての Gemini プロバイダーへマージされます"
"commonConfigHint": "このスニペットは Gemini の .env に書き込みます(GOOGLE_GEMINI_BASE_URL、GEMINI_API_KEY は使用できません)",
"extractFromCurrent": "現在のプロバイダーから抽出",
"extractNoCommonConfig": "現在のプロバイダーから抽出できる共通設定がありません",
"extractFailed": "抽出に失敗しました: {{error}}",
"saveFailed": "保存に失敗しました: {{error}}",
"extractedConfigInvalid": "抽出した設定のフォーマットが不正です",
"invalidJsonFormat": "共通設定スニペットの形式が不正です(有効な JSON でなければなりません)",
"commonConfigInvalidKeys": "共通設定スニペットに GOOGLE_GEMINI_BASE_URL または GEMINI_API_KEY を含めることはできません(検出: {{keys}}",
"commonConfigInvalidValues": "共通設定スニペットの値は文字列である必要があります",
"noCommonConfigToApply": "共通設定スニペットが空、または適用できる項目がありません",
"configMergeFailed": "設定のマージに失敗しました: {{error}}",
"configReplaceFailed": "設定の置換に失敗しました: {{error}}"
},
"providerPreset": {
"label": "プロバイダータイプ",
+22 -3
View File
@@ -53,7 +53,11 @@
"editCommonConfig": "编辑通用配置",
"editCommonConfigTitle": "编辑通用配置片段",
"commonConfigHint": "该片段会在勾选\"写入通用配置\"时合并到 settings.json 中",
"fullSettingsHint": "完整的 Claude Code settings.json 配置内容"
"fullSettingsHint": "完整的 Claude Code settings.json 配置内容",
"extractFromCurrent": "从当前供应商提取",
"extractNoCommonConfig": "当前供应商没有可提取的通用配置",
"extractFailed": "提取失败: {{error}}",
"saveFailed": "保存失败: {{error}}"
},
"header": {
"viewOnGithub": "在 GitHub 上查看",
@@ -393,7 +397,11 @@
"editCommonConfig": "编辑通用配置",
"editCommonConfigTitle": "编辑 Codex 通用配置片段",
"commonConfigHint": "该片段会在勾选'写入通用配置'时追加到 config.toml 末尾",
"apiUrlLabel": "API 请求地址"
"apiUrlLabel": "API 请求地址",
"extractFromCurrent": "从当前供应商提取",
"extractNoCommonConfig": "当前供应商没有可提取的通用配置",
"extractFailed": "提取失败: {{error}}",
"saveFailed": "保存失败: {{error}}"
},
"geminiConfig": {
"envFile": "环境变量 (.env)",
@@ -403,7 +411,18 @@
"writeCommonConfig": "写入通用配置",
"editCommonConfig": "编辑通用配置",
"editCommonConfigTitle": "编辑 Gemini 通用配置片段",
"commonConfigHint": "通用配置片段将合并到所有启用它的 Gemini 供应商配置中"
"commonConfigHint": "该片段会写入 Gemini 的 .env(不允许包含 GOOGLE_GEMINI_BASE_URL、GEMINI_API_KEY",
"extractFromCurrent": "从当前供应商提取",
"extractNoCommonConfig": "当前供应商没有可提取的通用配置",
"extractFailed": "提取失败: {{error}}",
"saveFailed": "保存失败: {{error}}",
"extractedConfigInvalid": "提取的配置格式错误",
"invalidJsonFormat": "通用配置片段格式错误(必须是有效的 JSON)",
"commonConfigInvalidKeys": "通用配置片段不能包含 GOOGLE_GEMINI_BASE_URL 或 GEMINI_API_KEY(发现:{{keys}}",
"commonConfigInvalidValues": "通用配置片段的值必须是字符串",
"noCommonConfigToApply": "通用配置片段为空或没有可写入的内容",
"configMergeFailed": "配置合并失败: {{error}}",
"configReplaceFailed": "配置替换失败: {{error}}"
},
"providerPreset": {
"label": "预设供应商",
+15
View File
@@ -47,3 +47,18 @@ export async function setCommonConfigSnippet(
): Promise<void> {
return invoke("set_common_config_snippet", { appType, snippet });
}
/**
* 从当前供应商提取通用配置片段
*
* 读取当前激活供应商的配置,自动排除差异化字段(API Key、模型配置、端点等),
* 返回可复用的通用配置片段。
*
* @param appType - 应用类型(claude/codex/gemini
* @returns 提取的通用配置片段(JSON/TOML 字符串)
*/
export async function extractCommonConfigSnippet(
appType: AppType,
): Promise<string> {
return invoke<string>("extract_common_config_snippet", { appType });
}