refactor(common-config): consolidate hooks and migrate Gemini to ENV format

- Delete redundant wrapper hooks (useCommonConfigSnippet, useGeminiCommonConfig)
- Change Gemini common config from JSON to ENV format (.env style)
- Add backend validation with forbidden keys filtering (GEMINI_API_KEY, GOOGLE_GEMINI_BASE_URL)
- Fix localStorage migration to skip empty parsed snippets
- Add error handling for silent JSON parse failures
- Clean up debug logs and unused types
This commit is contained in:
YoVinchen
2026-01-30 10:16:57 +08:00
Unverified
parent b8a53f9e36
commit aa1231903f
17 changed files with 198 additions and 323 deletions
+19 -3
View File
@@ -217,14 +217,30 @@ pub async fn set_common_config_snippet(
// 验证格式(根据应用类型)
if !snippet.trim().is_empty() {
match app_type.as_str() {
"claude" | "gemini" => {
"claude" => {
// 验证 JSON 格式
serde_json::from_str::<serde_json::Value>(&snippet)
.map_err(invalid_json_format_error)?;
}
"gemini" => {
// 验证 ENV/JSON 格式并拒绝禁用键
let validation = crate::config_merge::validate_gemini_common_snippet(&snippet);
// 如果有禁用键,返回错误
if !validation.forbidden_keys_found.is_empty() {
return Err(format!(
"GEMINI_FORBIDDEN_KEYS:{}",
validation.forbidden_keys_found.join(",")
));
}
// 如果非空但解析后无有效内容,返回错误
if !validation.is_valid {
return Err("GEMINI_INVALID_SNIPPET".to_string());
}
}
"codex" => {
// TOML 格式暂不验证(或可使用 toml crate
// 注意:TOML 验证较为复杂,暂时跳过
// TOML 格式暂不验证(前端验证
}
_ => {}
}
+56 -4
View File
@@ -565,13 +565,24 @@ fn merge_codex_config(custom_config: &JsonValue, common_snippet: &str) -> MergeR
///
/// This function supports all formats for backward compatibility.
fn merge_gemini_config(custom_config: &JsonValue, common_snippet: &str) -> MergeResult {
// Parse common config (try JSON first, then ENV format)
let common_env = parse_gemini_common_snippet(common_snippet);
// Parse and validate common config (filters forbidden keys)
let validation = validate_gemini_common_snippet(common_snippet);
let common_env = validation.env;
// Generate warning if forbidden keys were found
let warning = if !validation.forbidden_keys_found.is_empty() {
Some(format!(
"GEMINI_FORBIDDEN_KEYS:{}",
validation.forbidden_keys_found.join(",")
))
} else {
None
};
if common_env.is_empty() {
return MergeResult {
config: custom_config.clone(),
warning: None,
warning,
};
}
@@ -597,7 +608,7 @@ fn merge_gemini_config(custom_config: &JsonValue, common_snippet: &str) -> Merge
MergeResult {
config: merged_config,
warning: None,
warning,
}
}
@@ -646,6 +657,47 @@ pub(crate) fn parse_gemini_common_snippet(snippet: &str) -> serde_json::Map<Stri
result
}
/// Gemini common config forbidden keys - these should never be in common config
/// as they are provider-specific credentials/endpoints.
const GEMINI_FORBIDDEN_KEYS: &[&str] = &["GOOGLE_GEMINI_BASE_URL", "GEMINI_API_KEY"];
/// Result of validating Gemini common config snippet
#[derive(Debug, Clone)]
pub struct GeminiSnippetValidation {
/// Parsed environment variables (with forbidden keys filtered out)
pub env: serde_json::Map<String, JsonValue>,
/// List of forbidden keys that were found and filtered
pub forbidden_keys_found: Vec<String>,
/// Whether the snippet is valid (parseable and non-empty after filtering)
pub is_valid: bool,
}
/// Parse and validate Gemini common config snippet.
///
/// This function:
/// 1. Parses the snippet (supports ENV/JSON formats)
/// 2. Filters out forbidden keys (GOOGLE_GEMINI_BASE_URL, GEMINI_API_KEY)
/// 3. Returns validation result with filtered env and forbidden keys found
pub fn validate_gemini_common_snippet(snippet: &str) -> GeminiSnippetValidation {
let raw_env = parse_gemini_common_snippet(snippet);
let mut filtered_env = serde_json::Map::new();
let mut forbidden_keys_found = Vec::new();
for (key, value) in raw_env {
if GEMINI_FORBIDDEN_KEYS.contains(&key.as_str()) {
forbidden_keys_found.push(key);
} else {
filtered_env.insert(key, value);
}
}
GeminiSnippetValidation {
is_valid: !filtered_env.is_empty() || snippet.trim().is_empty(),
env: filtered_env,
forbidden_keys_found,
}
}
/// Strip surrounding quotes from ENV value.
/// Supports both single and double quotes: "value" -> value, 'value' -> value
fn strip_env_quotes(s: &str) -> &str {
+16 -7
View File
@@ -654,15 +654,19 @@ impl ProviderService {
Ok(cleaned.trim().to_string())
}
/// Extract common config for Gemini (JSON format)
/// Extract common config for Gemini (ENV format)
///
/// Extracts `.env` values while excluding provider-specific credentials:
/// - GOOGLE_GEMINI_BASE_URL
/// - GEMINI_API_KEY
///
/// Returns ENV format (KEY=VALUE per line) instead of JSON.
/// Values containing newlines/carriage returns are skipped to prevent
/// ENV format injection/truncation.
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();
let mut lines: Vec<String> = Vec::new();
if let Some(env) = env {
for (key, value) in env {
if key == "GOOGLE_GEMINI_BASE_URL" || key == "GEMINI_API_KEY" {
@@ -673,17 +677,22 @@ impl ProviderService {
};
let trimmed = v.trim();
if !trimmed.is_empty() {
snippet.insert(key.to_string(), Value::String(trimmed.to_string()));
// Skip values containing newlines to prevent ENV format injection
if trimmed.contains('\n') || trimmed.contains('\r') {
continue;
}
lines.push(format!("{key}={trimmed}"));
}
}
}
if snippet.is_empty() {
return Ok("{}".to_string());
if lines.is_empty() {
return Ok(String::new());
}
serde_json::to_string_pretty(&Value::Object(snippet))
.map_err(|e| AppError::Message(format!("Serialization failed: {e}")))
// Sort for consistent output
lines.sort();
Ok(lines.join("\n"))
}
/// Extract common config for OpenCode (JSON format)
-3
View File
@@ -53,15 +53,12 @@ export function DeepLinkImportDialog() {
const unlistenImport = listen<DeepLinkImportRequest>(
"deeplink-import",
async (event) => {
console.log("Deep link import event received:", event.payload);
// If config is present, merge it to get the complete configuration
if (event.payload.config || event.payload.configUrl) {
try {
const mergedRequest = await deeplinkApi.mergeDeeplinkConfig(
event.payload,
);
console.log("Config merged successfully:", mergedRequest);
setRequest(mergedRequest);
} catch (error) {
console.error("Failed to merge config:", error);
@@ -18,6 +18,7 @@ interface GeminiCommonConfigModalProps {
/**
* GeminiCommonConfigModal - Common Gemini configuration editor modal
* Allows editing of common env snippet shared across Gemini providers
* Uses ENV format (KEY=VALUE) instead of JSON
*/
export const GeminiCommonConfigModal: React.FC<
GeminiCommonConfigModalProps
@@ -88,13 +89,14 @@ export const GeminiCommonConfigModal: React.FC<
<JsonEditor
value={value}
onChange={onChange}
placeholder={`{
"GEMINI_MODEL": "gemini-3-pro-preview"
}`}
placeholder={`# Gemini 通用配置
# 格式: KEY=VALUE
GEMINI_MODEL=gemini-2.5-pro`}
darkMode={isDarkMode}
rows={16}
showValidation={true}
language="json"
showValidation={false}
language="javascript"
/>
{error && (
+58 -38
View File
@@ -66,13 +66,16 @@ import {
useCodexConfigState,
useApiKeyLink,
useTemplateValues,
useCommonConfigSnippet,
useCodexCommonConfig,
useSpeedTestEndpoints,
useCodexTomlValidation,
useGeminiConfigState,
useGeminiCommonConfig,
} from "./hooks";
import { useCommonConfigBase } from "@/hooks/useCommonConfigBase";
import {
claudeAdapter,
createGeminiAdapter,
} from "@/hooks/commonConfigAdapters";
import { useProvidersQuery } from "@/lib/query/queries";
/**
@@ -469,6 +472,17 @@ export function ProviderForm({
});
// 使用通用配置片段 hook (仅 Claude 模式)
// 直接使用 useCommonConfigBase + claudeAdapter
const claudeCommonConfig = useCommonConfigBase({
adapter: claudeAdapter,
inputValue: form.getValues("settingsConfig"),
onInputChange: (config) => form.setValue("settingsConfig", config),
initialData: appId === "claude" ? initialData : undefined,
selectedPresetId: selectedPresetId ?? undefined,
enabled: appId === "claude",
});
// 解构 Claude 通用配置
const {
useCommonConfig,
commonConfigSnippet,
@@ -478,17 +492,10 @@ export function ProviderForm({
isExtracting: isClaudeExtracting,
handleExtract: handleClaudeExtract,
isLoading: isClaudeCommonConfigLoading,
finalConfig: claudeFinalConfig,
finalValue: claudeFinalConfig,
getPendingCommonConfigSnippet: getPendingClaudeCommonConfigSnippet,
markCommonConfigSaved: markClaudeCommonConfigSaved,
} = useCommonConfigSnippet({
settingsConfig: form.getValues("settingsConfig"),
onConfigChange: (config) => form.setValue("settingsConfig", config),
initialData: appId === "claude" ? initialData : undefined,
selectedPresetId: selectedPresetId ?? undefined,
enabled: appId === "claude",
currentProviderId: providerId,
});
} = claudeCommonConfig;
// 使用 Codex 通用配置片段 hook (仅 Codex 模式)
const {
@@ -537,14 +544,16 @@ export function ProviderForm({
(key: string) => {
originalHandleGeminiApiKeyChange(key);
// 同步更新 settingsConfig
let config: { env?: Record<string, unknown> };
try {
const config = JSON.parse(form.getValues("settingsConfig") || "{}");
if (!config.env) config.env = {};
config.env.GEMINI_API_KEY = key.trim();
form.setValue("settingsConfig", JSON.stringify(config, null, 2));
config = JSON.parse(form.getValues("settingsConfig") || "{}");
} catch {
// ignore
// 解析失败时初始化为有效结构
config = { env: {} };
}
if (!config.env) config.env = {};
config.env.GEMINI_API_KEY = key.trim();
form.setValue("settingsConfig", JSON.stringify(config, null, 2));
},
[originalHandleGeminiApiKeyChange, form],
);
@@ -553,14 +562,16 @@ export function ProviderForm({
(url: string) => {
originalHandleGeminiBaseUrlChange(url);
// 同步更新 settingsConfig
let config: { env?: Record<string, unknown> };
try {
const config = JSON.parse(form.getValues("settingsConfig") || "{}");
if (!config.env) config.env = {};
config.env.GOOGLE_GEMINI_BASE_URL = url.trim().replace(/\/+$/, "");
form.setValue("settingsConfig", JSON.stringify(config, null, 2));
config = JSON.parse(form.getValues("settingsConfig") || "{}");
} catch {
// ignore
// 解析失败时初始化为有效结构
config = { env: {} };
}
if (!config.env) config.env = {};
config.env.GOOGLE_GEMINI_BASE_URL = url.trim().replace(/\/+$/, "");
form.setValue("settingsConfig", JSON.stringify(config, null, 2));
},
[originalHandleGeminiBaseUrlChange, form],
);
@@ -569,19 +580,36 @@ export function ProviderForm({
(model: string) => {
originalHandleGeminiModelChange(model);
// 同步更新 settingsConfig
let config: { env?: Record<string, unknown> };
try {
const config = JSON.parse(form.getValues("settingsConfig") || "{}");
if (!config.env) config.env = {};
config.env.GEMINI_MODEL = model.trim();
form.setValue("settingsConfig", JSON.stringify(config, null, 2));
config = JSON.parse(form.getValues("settingsConfig") || "{}");
} catch {
// ignore
// 解析失败时初始化为有效结构
config = { env: {} };
}
if (!config.env) config.env = {};
config.env.GEMINI_MODEL = model.trim();
form.setValue("settingsConfig", JSON.stringify(config, null, 2));
},
[originalHandleGeminiModelChange, form],
);
// 使用 Gemini 通用配置 hook (仅 Gemini 模式)
// 直接使用 useCommonConfigBase + createGeminiAdapter
const geminiAdapter = useMemo(
() => createGeminiAdapter({ envStringToObj, envObjToString }),
[envStringToObj, envObjToString],
);
const geminiCommonConfig = useCommonConfigBase({
adapter: geminiAdapter,
inputValue: geminiEnv,
onInputChange: handleGeminiEnvChange,
initialData: appId === "gemini" ? initialData : undefined,
selectedPresetId: selectedPresetId ?? undefined,
enabled: appId === "gemini",
});
// 解构 Gemini 通用配置
const {
useCommonConfig: useGeminiCommonConfigFlag,
commonConfigSnippet: geminiCommonConfigSnippet,
@@ -591,18 +619,10 @@ export function ProviderForm({
isExtracting: isGeminiExtracting,
handleExtract: handleGeminiExtract,
isLoading: isGeminiCommonConfigLoading,
finalEnv: geminiFinalEnv,
finalValue: geminiFinalEnv,
getPendingCommonConfigSnippet: getPendingGeminiCommonConfigSnippet,
markCommonConfigSaved: markGeminiCommonConfigSaved,
} = useGeminiCommonConfig({
envValue: geminiEnv,
onEnvChange: handleGeminiEnvChange,
envStringToObj,
envObjToString,
initialData: appId === "gemini" ? initialData : undefined,
selectedPresetId: selectedPresetId ?? undefined,
currentProviderId: providerId,
});
} = geminiCommonConfig;
const supportsCommonConfig =
appId === "claude" || appId === "codex" || appId === "gemini";
@@ -1041,8 +1061,8 @@ export function ProviderForm({
const configObj = geminiConfig.trim() ? JSON.parse(geminiConfig) : {};
// 如果启用了通用配置,只保存与通用配置不同的部分(自定义配置)
if (useGeminiCommonConfigFlag && geminiCommonConfigSnippet.trim()) {
// Parse common config as JSON (flat {"KEY": "VALUE"} format)
// Note: geminiCommonConfigSnippet is stored as JSON by useGeminiCommonConfig hook
// Parse common config snippet (supports ENV/Flat JSON/Wrapped JSON formats)
// Uses parseGeminiCommonConfigSnippet for unified parsing
const { env: commonEnvObj, warning } = parseGeminiCommonConfig(
geminiCommonConfigSnippet.trim(),
);
@@ -11,6 +11,4 @@ export { useCodexTomlValidation } from "./useCodexTomlValidation";
export { useGeminiConfigState } from "./useGeminiConfigState";
// Common Config Hooks
export { useCommonConfigSnippet } from "./useCommonConfigSnippet";
export { useCodexCommonConfig } from "./useCodexCommonConfig";
export { useGeminiCommonConfig } from "./useGeminiCommonConfig";
@@ -10,12 +10,6 @@ import { codexAdapter } from "@/hooks/commonConfigAdapters";
import { extractTomlDifference } from "@/utils/tomlConfigMerge";
import type { ProviderMeta } from "@/types";
/** TOML 校验错误码 */
export type TomlValidationErrorCode =
| "TOML_SYNTAX_ERROR"
| "TOML_PARSE_FAILED"
| "";
interface UseCodexCommonConfigProps {
/**
* 当前 Codex 配置(可能是纯 TOML 或 JSON wrapper 格式)
@@ -1,104 +0,0 @@
import { useMemo } from "react";
import {
useCommonConfigBase,
type UseCommonConfigBaseReturn,
} from "@/hooks/useCommonConfigBase";
import { claudeAdapter } from "@/hooks/commonConfigAdapters";
import type { ProviderMeta } from "@/types";
interface UseCommonConfigSnippetProps {
/**
* 当前配置(用于显示和运行时合并)
* 新架构:传入自定义配置,返回最终配置
*/
settingsConfig: string;
/**
* 配置变化回调
*/
onConfigChange: (config: string) => void;
/**
* 初始数据(编辑模式)
*/
initialData?: {
settingsConfig?: Record<string, unknown>;
meta?: ProviderMeta;
};
/**
* 当前选中的预设 ID
*/
selectedPresetId?: string;
/**
* 当 false 时跳过所有逻辑,返回禁用状态。默认:true
*/
enabled?: boolean;
/**
* 当前正在编辑的供应商 ID
*/
currentProviderId?: string;
}
export interface UseCommonConfigSnippetReturn {
/** 是否启用通用配置 */
useCommonConfig: boolean;
/** 通用配置片段 (JSON 格式) */
commonConfigSnippet: string;
/** 通用配置错误信息 */
commonConfigError: string;
/** 是否正在加载 */
isLoading: boolean;
/** 是否正在提取 */
isExtracting: boolean;
/** 通用配置开关处理函数 */
handleCommonConfigToggle: (checked: boolean) => void;
/** 通用配置片段变化处理函数 */
handleCommonConfigSnippetChange: (snippet: string) => void;
/** 从当前配置提取通用配置 */
handleExtract: () => Promise<void>;
/** 最终配置(运行时合并结果,只读) */
finalConfig: string;
/** 是否有待保存的通用配置变更 */
hasUnsavedCommonConfig: boolean;
/** 获取待保存的通用配置片段(用于 handleSubmit */
getPendingCommonConfigSnippet: () => string | null;
/** 标记通用配置已保存 */
markCommonConfigSaved: () => void;
}
/**
* 管理 Claude 通用配置片段
*
* 基于 useCommonConfigBase 泛型 Hook + Claude JSON 适配器实现。
*/
export function useCommonConfigSnippet({
settingsConfig,
onConfigChange,
initialData,
selectedPresetId,
enabled = true,
}: UseCommonConfigSnippetProps): UseCommonConfigSnippetReturn {
const adapter = useMemo(() => claudeAdapter, []);
const base: UseCommonConfigBaseReturn<string> = useCommonConfigBase({
adapter,
inputValue: settingsConfig,
onInputChange: onConfigChange,
initialData,
selectedPresetId,
enabled,
});
return {
useCommonConfig: base.useCommonConfig,
commonConfigSnippet: base.commonConfigSnippet,
commonConfigError: base.commonConfigError,
isLoading: base.isLoading,
isExtracting: base.isExtracting,
handleCommonConfigToggle: base.handleCommonConfigToggle,
handleCommonConfigSnippetChange: base.handleCommonConfigSnippetChange,
handleExtract: base.handleExtract,
finalConfig: base.finalValue,
hasUnsavedCommonConfig: base.hasUnsavedCommonConfig,
getPendingCommonConfigSnippet: base.getPendingCommonConfigSnippet,
markCommonConfigSaved: base.markCommonConfigSaved,
};
}
@@ -1,118 +0,0 @@
import { useMemo } from "react";
import {
useCommonConfigBase,
type UseCommonConfigBaseReturn,
} from "@/hooks/useCommonConfigBase";
import { createGeminiAdapter } from "@/hooks/commonConfigAdapters";
import type { ProviderMeta } from "@/types";
interface UseGeminiCommonConfigProps {
/**
* 当前 env 值(字符串格式,如 "KEY=VALUE\nKEY2=VALUE2"
*/
envValue: string;
/**
* env 变化回调
*/
onEnvChange: (env: string) => void;
/**
* 字符串转对象
*/
envStringToObj: (envString: string) => Record<string, string>;
/**
* 对象转字符串
*/
envObjToString: (envObj: Record<string, unknown>) => string;
/**
* 初始数据(编辑模式)
*/
initialData?: {
settingsConfig?: Record<string, unknown>;
meta?: ProviderMeta;
};
/**
* 当前选中的预设 ID
*/
selectedPresetId?: string;
/**
* 当前正在编辑的供应商 ID
*/
currentProviderId?: string;
}
export interface UseGeminiCommonConfigReturn {
/** 是否启用通用配置 */
useCommonConfig: boolean;
/** 通用配置片段 (JSON 格式) */
commonConfigSnippet: string;
/** 通用配置错误信息 */
commonConfigError: string;
/** 是否正在加载 */
isLoading: boolean;
/** 是否正在提取 */
isExtracting: boolean;
/** 通用配置开关处理函数 */
handleCommonConfigToggle: (checked: boolean) => void;
/** 通用配置片段变化处理函数 */
handleCommonConfigSnippetChange: (snippet: string) => void;
/** 从当前配置提取通用配置 */
handleExtract: () => Promise<void>;
/** 最终 env 对象(运行时合并结果,只读) */
finalEnv: Record<string, string>;
/** 是否有待保存的通用配置变更 */
hasUnsavedCommonConfig: boolean;
/** 获取待保存的通用配置片段(用于 handleSubmit */
getPendingCommonConfigSnippet: () => string | null;
/** 标记通用配置已保存 */
markCommonConfigSaved: () => void;
}
/**
* 管理 Gemini 通用配置片段
*
* 基于 useCommonConfigBase 泛型 Hook + Gemini ENV/JSON 适配器实现。
*
* 架构:
* - envValue:传入当前 env 字符串
* - commonConfigSnippet:存储在数据库中的通用配置片段
* - finalEnv:运行时计算 = merge(commonConfig, customEnv)
* - 开启/关闭通用配置只改变 enabled 状态,不修改 envValue
*/
export function useGeminiCommonConfig({
envValue,
onEnvChange,
envStringToObj,
envObjToString,
initialData,
selectedPresetId,
}: UseGeminiCommonConfigProps): UseGeminiCommonConfigReturn {
// 创建适配器(需要转换函数)
const adapter = useMemo(
() => createGeminiAdapter({ envStringToObj, envObjToString }),
[envStringToObj, envObjToString],
);
const base: UseCommonConfigBaseReturn<Record<string, string>> =
useCommonConfigBase({
adapter,
inputValue: envValue,
onInputChange: onEnvChange,
initialData,
selectedPresetId,
});
return {
useCommonConfig: base.useCommonConfig,
commonConfigSnippet: base.commonConfigSnippet,
commonConfigError: base.commonConfigError,
isLoading: base.isLoading,
isExtracting: base.isExtracting,
handleCommonConfigToggle: base.handleCommonConfigToggle,
handleCommonConfigSnippetChange: base.handleCommonConfigSnippetChange,
handleExtract: base.handleExtract,
finalEnv: base.finalValue,
hasUnsavedCommonConfig: base.hasUnsavedCommonConfig,
getPendingCommonConfigSnippet: base.getPendingCommonConfigSnippet,
markCommonConfigSaved: base.markCommonConfigSaved,
};
}
+4 -2
View File
@@ -249,7 +249,7 @@ export const codexAdapter: CommonConfigAdapter<string, string> = {
// ============================================================================
const GEMINI_LEGACY_STORAGE_KEY = "cc-switch:gemini-common-config-snippet";
const GEMINI_DEFAULT_SNIPPET = "{}";
const GEMINI_DEFAULT_SNIPPET = "";
export interface GeminiAdapterOptions {
/** 字符串转对象 */
@@ -306,7 +306,9 @@ export function createGeminiAdapter(
) {
return t("geminiConfig.commonConfigInvalidValues");
}
return t("geminiConfig.invalidJsonFormat");
return t("geminiConfig.invalidEnvFormat", {
defaultValue: "配置格式错误",
});
}
if (Object.keys(result.env).length === 0) {
+6 -1
View File
@@ -234,7 +234,9 @@ export function useCommonConfigBase<TConfig, TFinal>({
);
if (legacySnippet && legacySnippet.trim()) {
const parsed = adapter.parseSnippet(legacySnippet);
if (!parsed.error) {
// 只有在解析成功且有有效内容时才迁移
// 这避免了将 "{}" 这样的空 JSON 迁移为"有效配置"
if (!parsed.error && adapter.hasValidContent(legacySnippet)) {
await configApi.setCommonConfigSnippet(
adapter.appKey,
legacySnippet,
@@ -246,6 +248,9 @@ export function useCommonConfigBase<TConfig, TFinal>({
console.log(
`[迁移] ${adapter.appKey} 通用配置已从 localStorage 迁移到数据库`,
);
} else {
// 解析失败或无有效内容,清理 localStorage 不迁移
window.localStorage.removeItem(adapter.legacyStorageKey);
}
}
} catch (e) {
+1
View File
@@ -584,6 +584,7 @@
"saveFailed": "Save failed: {{error}}",
"extractedConfigInvalid": "Extracted config format is invalid",
"invalidJsonFormat": "Common config snippet format error (must be valid JSON)",
"invalidEnvFormat": "Config format error (use KEY=VALUE format, one per line)",
"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",
+1
View File
@@ -584,6 +584,7 @@
"saveFailed": "保存に失敗しました: {{error}}",
"extractedConfigInvalid": "抽出した設定のフォーマットが不正です",
"invalidJsonFormat": "共通設定スニペットの形式が不正です(有効な JSON でなければなりません)",
"invalidEnvFormat": "設定フォーマットエラー(KEY=VALUE 形式を使用してください、各行に1つ)",
"commonConfigInvalidKeys": "共通設定スニペットに GOOGLE_GEMINI_BASE_URL または GEMINI_API_KEY を含めることはできません(検出: {{keys}}",
"commonConfigInvalidValues": "共通設定スニペットの値は文字列である必要があります",
"noCommonConfigToApply": "共通設定スニペットが空、または適用できる項目がありません",
+1
View File
@@ -584,6 +584,7 @@
"saveFailed": "保存失败: {{error}}",
"extractedConfigInvalid": "提取的配置格式错误",
"invalidJsonFormat": "通用配置片段格式错误(必须是有效的 JSON)",
"invalidEnvFormat": "配置格式错误(请使用 KEY=VALUE 格式,每行一个)",
"commonConfigInvalidKeys": "通用配置片段不能包含 GOOGLE_GEMINI_BASE_URL 或 GEMINI_API_KEY(发现:{{keys}}",
"commonConfigInvalidValues": "通用配置片段的值必须是字符串",
"noCommonConfigToApply": "通用配置片段为空或没有可写入的内容",
+5 -10
View File
@@ -81,7 +81,10 @@ export async function getCommonConfigSnippet(
*
* @param appType - claude/codex/gemini
* @param snippet -
* @throws Claude/Gemini JSONCodex
* @throws
* - Claude: 验证 JSON
* - Gemini: 验证 ENV/JSON GEMINI_API_KEY/GOOGLE_GEMINI_BASE_URL
* - Codex: TOML
*/
export async function setCommonConfigSnippet(
appType: AppType,
@@ -98,7 +101,7 @@ export async function setCommonConfigSnippet(
*
* @param appType - claude/codex/gemini
* @param options -
* @returns JSON/TOML
* @returns Claude: JSON, Codex: TOML, Gemini: ENV
*/
export type ExtractCommonConfigSnippetOptions = {
settingsConfig?: string;
@@ -201,10 +204,6 @@ async function executeSyncWithLock(appType: AppType): Promise<void> {
// 输出结果
if (result.error) {
console.warn(`[syncCommonConfig] ${appType} 同步失败: ${result.error}`);
} else if (result.count > 0) {
console.log(
`[syncCommonConfig] 共更新 ${result.count}${appType} 供应商`,
);
}
// 通知回调
@@ -321,15 +320,11 @@ async function doSyncCommonConfigToProviders(
},
},
};
console.log(
`[syncCommonConfig] 供应商 ${id} 检测到通用配置,已补写 meta`,
);
}
try {
await providersApi.update(providerToSave, appType);
updatedCount++;
console.log(`[syncCommonConfig] 已更新供应商 ${id}`);
} catch (updateError) {
errors.push(`供应商 ${id}: 保存失败 - ${String(updateError)}`);
}
+24 -20
View File
@@ -63,13 +63,26 @@ export const hasCommonConfigSnippet = (
}
};
// 检查 Gemini 配置是否已包含通用配置片段(env JSON)
/**
* Gemini
*
* snippet :
* - ENV 格式: KEY=VALUE ()
* - Flat JSON: {"KEY": "VALUE", ...}
* - Wrapped JSON: {"env": {"KEY": "VALUE", ...}}
*
* @param jsonString - Provider settingsConfig (JSON )
* @param snippetString - (ENV/JSON )
* @returns
*/
export const hasGeminiCommonConfigSnippet = (
jsonString: string,
snippetString: string,
): boolean => {
try {
if (!snippetString.trim()) return false;
// 解析 provider 的 settingsConfig
const config = jsonString ? JSON.parse(jsonString) : {};
if (!isPlainObject(config)) return false;
const envValue = (config as Record<string, unknown>).env;
@@ -79,27 +92,18 @@ export const hasGeminiCommonConfigSnippet = (
unknown
>;
const parsed = JSON.parse(snippetString);
if (!isPlainObject(parsed)) return false;
// 使用 parseGeminiCommonConfigSnippet 解析 snippet (支持 ENV/JSON)
const parseResult = parseGeminiCommonConfigSnippet(snippetString, {
strictForbiddenKeys: false, // 过滤禁用键而非报错
});
const entries = Object.entries(parsed).filter(
(entry): entry is [string, string] => {
const [key, value] = entry;
if (
GEMINI_COMMON_ENV_FORBIDDEN_KEYS.includes(
key as GeminiForbiddenEnvKey,
)
) {
return false;
}
if (typeof value !== "string") return false;
return value.trim().length > 0;
},
);
// 解析失败或为空
if (parseResult.error || Object.keys(parseResult.env).length === 0) {
return false;
}
if (entries.length === 0) return false;
return entries.every(([key, value]) => {
// 检查所有有效键值对是否都存在于 provider.env 中
return Object.entries(parseResult.env).every(([key, value]) => {
const current = env[key];
return typeof current === "string" && current === value.trim();
});