mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-05-19 19:50:26 +08:00
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:
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(¤t_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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "プロバイダータイプ",
|
||||
|
||||
@@ -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": "预设供应商",
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user