mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-03 11:01:22 +08:00
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:
@@ -217,14 +217,30 @@ pub async fn set_common_config_snippet(
|
|||||||
// 验证格式(根据应用类型)
|
// 验证格式(根据应用类型)
|
||||||
if !snippet.trim().is_empty() {
|
if !snippet.trim().is_empty() {
|
||||||
match app_type.as_str() {
|
match app_type.as_str() {
|
||||||
"claude" | "gemini" => {
|
"claude" => {
|
||||||
// 验证 JSON 格式
|
// 验证 JSON 格式
|
||||||
serde_json::from_str::<serde_json::Value>(&snippet)
|
serde_json::from_str::<serde_json::Value>(&snippet)
|
||||||
.map_err(invalid_json_format_error)?;
|
.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" => {
|
"codex" => {
|
||||||
// TOML 格式暂不验证(或可使用 toml crate)
|
// TOML 格式,暂不验证(前端验证)
|
||||||
// 注意:TOML 验证较为复杂,暂时跳过
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -565,13 +565,24 @@ fn merge_codex_config(custom_config: &JsonValue, common_snippet: &str) -> MergeR
|
|||||||
///
|
///
|
||||||
/// This function supports all formats for backward compatibility.
|
/// This function supports all formats for backward compatibility.
|
||||||
fn merge_gemini_config(custom_config: &JsonValue, common_snippet: &str) -> MergeResult {
|
fn merge_gemini_config(custom_config: &JsonValue, common_snippet: &str) -> MergeResult {
|
||||||
// Parse common config (try JSON first, then ENV format)
|
// Parse and validate common config (filters forbidden keys)
|
||||||
let common_env = parse_gemini_common_snippet(common_snippet);
|
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() {
|
if common_env.is_empty() {
|
||||||
return MergeResult {
|
return MergeResult {
|
||||||
config: custom_config.clone(),
|
config: custom_config.clone(),
|
||||||
warning: None,
|
warning,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -597,7 +608,7 @@ fn merge_gemini_config(custom_config: &JsonValue, common_snippet: &str) -> Merge
|
|||||||
|
|
||||||
MergeResult {
|
MergeResult {
|
||||||
config: merged_config,
|
config: merged_config,
|
||||||
warning: None,
|
warning,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,6 +657,47 @@ pub(crate) fn parse_gemini_common_snippet(snippet: &str) -> serde_json::Map<Stri
|
|||||||
result
|
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.
|
/// Strip surrounding quotes from ENV value.
|
||||||
/// Supports both single and double quotes: "value" -> value, 'value' -> value
|
/// Supports both single and double quotes: "value" -> value, 'value' -> value
|
||||||
fn strip_env_quotes(s: &str) -> &str {
|
fn strip_env_quotes(s: &str) -> &str {
|
||||||
|
|||||||
@@ -654,15 +654,19 @@ impl ProviderService {
|
|||||||
Ok(cleaned.trim().to_string())
|
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:
|
/// Extracts `.env` values while excluding provider-specific credentials:
|
||||||
/// - GOOGLE_GEMINI_BASE_URL
|
/// - GOOGLE_GEMINI_BASE_URL
|
||||||
/// - GEMINI_API_KEY
|
/// - 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> {
|
fn extract_gemini_common_config(settings: &Value) -> Result<String, AppError> {
|
||||||
let env = settings.get("env").and_then(|v| v.as_object());
|
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 {
|
if let Some(env) = env {
|
||||||
for (key, value) in env {
|
for (key, value) in env {
|
||||||
if key == "GOOGLE_GEMINI_BASE_URL" || key == "GEMINI_API_KEY" {
|
if key == "GOOGLE_GEMINI_BASE_URL" || key == "GEMINI_API_KEY" {
|
||||||
@@ -673,17 +677,22 @@ impl ProviderService {
|
|||||||
};
|
};
|
||||||
let trimmed = v.trim();
|
let trimmed = v.trim();
|
||||||
if !trimmed.is_empty() {
|
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() {
|
if lines.is_empty() {
|
||||||
return Ok("{}".to_string());
|
return Ok(String::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
serde_json::to_string_pretty(&Value::Object(snippet))
|
// Sort for consistent output
|
||||||
.map_err(|e| AppError::Message(format!("Serialization failed: {e}")))
|
lines.sort();
|
||||||
|
Ok(lines.join("\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract common config for OpenCode (JSON format)
|
/// Extract common config for OpenCode (JSON format)
|
||||||
|
|||||||
@@ -53,15 +53,12 @@ export function DeepLinkImportDialog() {
|
|||||||
const unlistenImport = listen<DeepLinkImportRequest>(
|
const unlistenImport = listen<DeepLinkImportRequest>(
|
||||||
"deeplink-import",
|
"deeplink-import",
|
||||||
async (event) => {
|
async (event) => {
|
||||||
console.log("Deep link import event received:", event.payload);
|
|
||||||
|
|
||||||
// If config is present, merge it to get the complete configuration
|
// If config is present, merge it to get the complete configuration
|
||||||
if (event.payload.config || event.payload.configUrl) {
|
if (event.payload.config || event.payload.configUrl) {
|
||||||
try {
|
try {
|
||||||
const mergedRequest = await deeplinkApi.mergeDeeplinkConfig(
|
const mergedRequest = await deeplinkApi.mergeDeeplinkConfig(
|
||||||
event.payload,
|
event.payload,
|
||||||
);
|
);
|
||||||
console.log("Config merged successfully:", mergedRequest);
|
|
||||||
setRequest(mergedRequest);
|
setRequest(mergedRequest);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to merge config:", error);
|
console.error("Failed to merge config:", error);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ interface GeminiCommonConfigModalProps {
|
|||||||
/**
|
/**
|
||||||
* GeminiCommonConfigModal - Common Gemini configuration editor modal
|
* GeminiCommonConfigModal - Common Gemini configuration editor modal
|
||||||
* Allows editing of common env snippet shared across Gemini providers
|
* Allows editing of common env snippet shared across Gemini providers
|
||||||
|
* Uses ENV format (KEY=VALUE) instead of JSON
|
||||||
*/
|
*/
|
||||||
export const GeminiCommonConfigModal: React.FC<
|
export const GeminiCommonConfigModal: React.FC<
|
||||||
GeminiCommonConfigModalProps
|
GeminiCommonConfigModalProps
|
||||||
@@ -88,13 +89,14 @@ export const GeminiCommonConfigModal: React.FC<
|
|||||||
<JsonEditor
|
<JsonEditor
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
placeholder={`{
|
placeholder={`# Gemini 通用配置
|
||||||
"GEMINI_MODEL": "gemini-3-pro-preview"
|
# 格式: KEY=VALUE
|
||||||
}`}
|
|
||||||
|
GEMINI_MODEL=gemini-2.5-pro`}
|
||||||
darkMode={isDarkMode}
|
darkMode={isDarkMode}
|
||||||
rows={16}
|
rows={16}
|
||||||
showValidation={true}
|
showValidation={false}
|
||||||
language="json"
|
language="javascript"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|||||||
@@ -66,13 +66,16 @@ import {
|
|||||||
useCodexConfigState,
|
useCodexConfigState,
|
||||||
useApiKeyLink,
|
useApiKeyLink,
|
||||||
useTemplateValues,
|
useTemplateValues,
|
||||||
useCommonConfigSnippet,
|
|
||||||
useCodexCommonConfig,
|
useCodexCommonConfig,
|
||||||
useSpeedTestEndpoints,
|
useSpeedTestEndpoints,
|
||||||
useCodexTomlValidation,
|
useCodexTomlValidation,
|
||||||
useGeminiConfigState,
|
useGeminiConfigState,
|
||||||
useGeminiCommonConfig,
|
|
||||||
} from "./hooks";
|
} from "./hooks";
|
||||||
|
import { useCommonConfigBase } from "@/hooks/useCommonConfigBase";
|
||||||
|
import {
|
||||||
|
claudeAdapter,
|
||||||
|
createGeminiAdapter,
|
||||||
|
} from "@/hooks/commonConfigAdapters";
|
||||||
import { useProvidersQuery } from "@/lib/query/queries";
|
import { useProvidersQuery } from "@/lib/query/queries";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -469,6 +472,17 @@ export function ProviderForm({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 使用通用配置片段 hook (仅 Claude 模式)
|
// 使用通用配置片段 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 {
|
const {
|
||||||
useCommonConfig,
|
useCommonConfig,
|
||||||
commonConfigSnippet,
|
commonConfigSnippet,
|
||||||
@@ -478,17 +492,10 @@ export function ProviderForm({
|
|||||||
isExtracting: isClaudeExtracting,
|
isExtracting: isClaudeExtracting,
|
||||||
handleExtract: handleClaudeExtract,
|
handleExtract: handleClaudeExtract,
|
||||||
isLoading: isClaudeCommonConfigLoading,
|
isLoading: isClaudeCommonConfigLoading,
|
||||||
finalConfig: claudeFinalConfig,
|
finalValue: claudeFinalConfig,
|
||||||
getPendingCommonConfigSnippet: getPendingClaudeCommonConfigSnippet,
|
getPendingCommonConfigSnippet: getPendingClaudeCommonConfigSnippet,
|
||||||
markCommonConfigSaved: markClaudeCommonConfigSaved,
|
markCommonConfigSaved: markClaudeCommonConfigSaved,
|
||||||
} = useCommonConfigSnippet({
|
} = claudeCommonConfig;
|
||||||
settingsConfig: form.getValues("settingsConfig"),
|
|
||||||
onConfigChange: (config) => form.setValue("settingsConfig", config),
|
|
||||||
initialData: appId === "claude" ? initialData : undefined,
|
|
||||||
selectedPresetId: selectedPresetId ?? undefined,
|
|
||||||
enabled: appId === "claude",
|
|
||||||
currentProviderId: providerId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 使用 Codex 通用配置片段 hook (仅 Codex 模式)
|
// 使用 Codex 通用配置片段 hook (仅 Codex 模式)
|
||||||
const {
|
const {
|
||||||
@@ -537,14 +544,16 @@ export function ProviderForm({
|
|||||||
(key: string) => {
|
(key: string) => {
|
||||||
originalHandleGeminiApiKeyChange(key);
|
originalHandleGeminiApiKeyChange(key);
|
||||||
// 同步更新 settingsConfig
|
// 同步更新 settingsConfig
|
||||||
|
let config: { env?: Record<string, unknown> };
|
||||||
try {
|
try {
|
||||||
const config = JSON.parse(form.getValues("settingsConfig") || "{}");
|
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));
|
|
||||||
} catch {
|
} 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],
|
[originalHandleGeminiApiKeyChange, form],
|
||||||
);
|
);
|
||||||
@@ -553,14 +562,16 @@ export function ProviderForm({
|
|||||||
(url: string) => {
|
(url: string) => {
|
||||||
originalHandleGeminiBaseUrlChange(url);
|
originalHandleGeminiBaseUrlChange(url);
|
||||||
// 同步更新 settingsConfig
|
// 同步更新 settingsConfig
|
||||||
|
let config: { env?: Record<string, unknown> };
|
||||||
try {
|
try {
|
||||||
const config = JSON.parse(form.getValues("settingsConfig") || "{}");
|
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));
|
|
||||||
} catch {
|
} 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],
|
[originalHandleGeminiBaseUrlChange, form],
|
||||||
);
|
);
|
||||||
@@ -569,19 +580,36 @@ export function ProviderForm({
|
|||||||
(model: string) => {
|
(model: string) => {
|
||||||
originalHandleGeminiModelChange(model);
|
originalHandleGeminiModelChange(model);
|
||||||
// 同步更新 settingsConfig
|
// 同步更新 settingsConfig
|
||||||
|
let config: { env?: Record<string, unknown> };
|
||||||
try {
|
try {
|
||||||
const config = JSON.parse(form.getValues("settingsConfig") || "{}");
|
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));
|
|
||||||
} catch {
|
} 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],
|
[originalHandleGeminiModelChange, form],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 使用 Gemini 通用配置 hook (仅 Gemini 模式)
|
// 使用 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 {
|
const {
|
||||||
useCommonConfig: useGeminiCommonConfigFlag,
|
useCommonConfig: useGeminiCommonConfigFlag,
|
||||||
commonConfigSnippet: geminiCommonConfigSnippet,
|
commonConfigSnippet: geminiCommonConfigSnippet,
|
||||||
@@ -591,18 +619,10 @@ export function ProviderForm({
|
|||||||
isExtracting: isGeminiExtracting,
|
isExtracting: isGeminiExtracting,
|
||||||
handleExtract: handleGeminiExtract,
|
handleExtract: handleGeminiExtract,
|
||||||
isLoading: isGeminiCommonConfigLoading,
|
isLoading: isGeminiCommonConfigLoading,
|
||||||
finalEnv: geminiFinalEnv,
|
finalValue: geminiFinalEnv,
|
||||||
getPendingCommonConfigSnippet: getPendingGeminiCommonConfigSnippet,
|
getPendingCommonConfigSnippet: getPendingGeminiCommonConfigSnippet,
|
||||||
markCommonConfigSaved: markGeminiCommonConfigSaved,
|
markCommonConfigSaved: markGeminiCommonConfigSaved,
|
||||||
} = useGeminiCommonConfig({
|
} = geminiCommonConfig;
|
||||||
envValue: geminiEnv,
|
|
||||||
onEnvChange: handleGeminiEnvChange,
|
|
||||||
envStringToObj,
|
|
||||||
envObjToString,
|
|
||||||
initialData: appId === "gemini" ? initialData : undefined,
|
|
||||||
selectedPresetId: selectedPresetId ?? undefined,
|
|
||||||
currentProviderId: providerId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const supportsCommonConfig =
|
const supportsCommonConfig =
|
||||||
appId === "claude" || appId === "codex" || appId === "gemini";
|
appId === "claude" || appId === "codex" || appId === "gemini";
|
||||||
@@ -1041,8 +1061,8 @@ export function ProviderForm({
|
|||||||
const configObj = geminiConfig.trim() ? JSON.parse(geminiConfig) : {};
|
const configObj = geminiConfig.trim() ? JSON.parse(geminiConfig) : {};
|
||||||
// 如果启用了通用配置,只保存与通用配置不同的部分(自定义配置)
|
// 如果启用了通用配置,只保存与通用配置不同的部分(自定义配置)
|
||||||
if (useGeminiCommonConfigFlag && geminiCommonConfigSnippet.trim()) {
|
if (useGeminiCommonConfigFlag && geminiCommonConfigSnippet.trim()) {
|
||||||
// Parse common config as JSON (flat {"KEY": "VALUE"} format)
|
// Parse common config snippet (supports ENV/Flat JSON/Wrapped JSON formats)
|
||||||
// Note: geminiCommonConfigSnippet is stored as JSON by useGeminiCommonConfig hook
|
// Uses parseGeminiCommonConfigSnippet for unified parsing
|
||||||
const { env: commonEnvObj, warning } = parseGeminiCommonConfig(
|
const { env: commonEnvObj, warning } = parseGeminiCommonConfig(
|
||||||
geminiCommonConfigSnippet.trim(),
|
geminiCommonConfigSnippet.trim(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,4 @@ export { useCodexTomlValidation } from "./useCodexTomlValidation";
|
|||||||
export { useGeminiConfigState } from "./useGeminiConfigState";
|
export { useGeminiConfigState } from "./useGeminiConfigState";
|
||||||
|
|
||||||
// Common Config Hooks
|
// Common Config Hooks
|
||||||
export { useCommonConfigSnippet } from "./useCommonConfigSnippet";
|
|
||||||
export { useCodexCommonConfig } from "./useCodexCommonConfig";
|
export { useCodexCommonConfig } from "./useCodexCommonConfig";
|
||||||
export { useGeminiCommonConfig } from "./useGeminiCommonConfig";
|
|
||||||
|
|||||||
@@ -10,12 +10,6 @@ import { codexAdapter } from "@/hooks/commonConfigAdapters";
|
|||||||
import { extractTomlDifference } from "@/utils/tomlConfigMerge";
|
import { extractTomlDifference } from "@/utils/tomlConfigMerge";
|
||||||
import type { ProviderMeta } from "@/types";
|
import type { ProviderMeta } from "@/types";
|
||||||
|
|
||||||
/** TOML 校验错误码 */
|
|
||||||
export type TomlValidationErrorCode =
|
|
||||||
| "TOML_SYNTAX_ERROR"
|
|
||||||
| "TOML_PARSE_FAILED"
|
|
||||||
| "";
|
|
||||||
|
|
||||||
interface UseCodexCommonConfigProps {
|
interface UseCodexCommonConfigProps {
|
||||||
/**
|
/**
|
||||||
* 当前 Codex 配置(可能是纯 TOML 或 JSON wrapper 格式)
|
* 当前 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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -249,7 +249,7 @@ export const codexAdapter: CommonConfigAdapter<string, string> = {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const GEMINI_LEGACY_STORAGE_KEY = "cc-switch:gemini-common-config-snippet";
|
const GEMINI_LEGACY_STORAGE_KEY = "cc-switch:gemini-common-config-snippet";
|
||||||
const GEMINI_DEFAULT_SNIPPET = "{}";
|
const GEMINI_DEFAULT_SNIPPET = "";
|
||||||
|
|
||||||
export interface GeminiAdapterOptions {
|
export interface GeminiAdapterOptions {
|
||||||
/** 字符串转对象 */
|
/** 字符串转对象 */
|
||||||
@@ -306,7 +306,9 @@ export function createGeminiAdapter(
|
|||||||
) {
|
) {
|
||||||
return t("geminiConfig.commonConfigInvalidValues");
|
return t("geminiConfig.commonConfigInvalidValues");
|
||||||
}
|
}
|
||||||
return t("geminiConfig.invalidJsonFormat");
|
return t("geminiConfig.invalidEnvFormat", {
|
||||||
|
defaultValue: "配置格式错误",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(result.env).length === 0) {
|
if (Object.keys(result.env).length === 0) {
|
||||||
|
|||||||
@@ -234,7 +234,9 @@ export function useCommonConfigBase<TConfig, TFinal>({
|
|||||||
);
|
);
|
||||||
if (legacySnippet && legacySnippet.trim()) {
|
if (legacySnippet && legacySnippet.trim()) {
|
||||||
const parsed = adapter.parseSnippet(legacySnippet);
|
const parsed = adapter.parseSnippet(legacySnippet);
|
||||||
if (!parsed.error) {
|
// 只有在解析成功且有有效内容时才迁移
|
||||||
|
// 这避免了将 "{}" 这样的空 JSON 迁移为"有效配置"
|
||||||
|
if (!parsed.error && adapter.hasValidContent(legacySnippet)) {
|
||||||
await configApi.setCommonConfigSnippet(
|
await configApi.setCommonConfigSnippet(
|
||||||
adapter.appKey,
|
adapter.appKey,
|
||||||
legacySnippet,
|
legacySnippet,
|
||||||
@@ -246,6 +248,9 @@ export function useCommonConfigBase<TConfig, TFinal>({
|
|||||||
console.log(
|
console.log(
|
||||||
`[迁移] ${adapter.appKey} 通用配置已从 localStorage 迁移到数据库`,
|
`[迁移] ${adapter.appKey} 通用配置已从 localStorage 迁移到数据库`,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
// 解析失败或无有效内容,清理 localStorage 不迁移
|
||||||
|
window.localStorage.removeItem(adapter.legacyStorageKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -584,6 +584,7 @@
|
|||||||
"saveFailed": "Save failed: {{error}}",
|
"saveFailed": "Save failed: {{error}}",
|
||||||
"extractedConfigInvalid": "Extracted config format is invalid",
|
"extractedConfigInvalid": "Extracted config format is invalid",
|
||||||
"invalidJsonFormat": "Common config snippet format error (must be valid JSON)",
|
"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}})",
|
"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",
|
"commonConfigInvalidValues": "Common config snippet values must be strings",
|
||||||
"noCommonConfigToApply": "Common config snippet is empty or has no applicable entries",
|
"noCommonConfigToApply": "Common config snippet is empty or has no applicable entries",
|
||||||
|
|||||||
@@ -584,6 +584,7 @@
|
|||||||
"saveFailed": "保存に失敗しました: {{error}}",
|
"saveFailed": "保存に失敗しました: {{error}}",
|
||||||
"extractedConfigInvalid": "抽出した設定のフォーマットが不正です",
|
"extractedConfigInvalid": "抽出した設定のフォーマットが不正です",
|
||||||
"invalidJsonFormat": "共通設定スニペットの形式が不正です(有効な JSON でなければなりません)",
|
"invalidJsonFormat": "共通設定スニペットの形式が不正です(有効な JSON でなければなりません)",
|
||||||
|
"invalidEnvFormat": "設定フォーマットエラー(KEY=VALUE 形式を使用してください、各行に1つ)",
|
||||||
"commonConfigInvalidKeys": "共通設定スニペットに GOOGLE_GEMINI_BASE_URL または GEMINI_API_KEY を含めることはできません(検出: {{keys}})",
|
"commonConfigInvalidKeys": "共通設定スニペットに GOOGLE_GEMINI_BASE_URL または GEMINI_API_KEY を含めることはできません(検出: {{keys}})",
|
||||||
"commonConfigInvalidValues": "共通設定スニペットの値は文字列である必要があります",
|
"commonConfigInvalidValues": "共通設定スニペットの値は文字列である必要があります",
|
||||||
"noCommonConfigToApply": "共通設定スニペットが空、または適用できる項目がありません",
|
"noCommonConfigToApply": "共通設定スニペットが空、または適用できる項目がありません",
|
||||||
|
|||||||
@@ -584,6 +584,7 @@
|
|||||||
"saveFailed": "保存失败: {{error}}",
|
"saveFailed": "保存失败: {{error}}",
|
||||||
"extractedConfigInvalid": "提取的配置格式错误",
|
"extractedConfigInvalid": "提取的配置格式错误",
|
||||||
"invalidJsonFormat": "通用配置片段格式错误(必须是有效的 JSON)",
|
"invalidJsonFormat": "通用配置片段格式错误(必须是有效的 JSON)",
|
||||||
|
"invalidEnvFormat": "配置格式错误(请使用 KEY=VALUE 格式,每行一个)",
|
||||||
"commonConfigInvalidKeys": "通用配置片段不能包含 GOOGLE_GEMINI_BASE_URL 或 GEMINI_API_KEY(发现:{{keys}})",
|
"commonConfigInvalidKeys": "通用配置片段不能包含 GOOGLE_GEMINI_BASE_URL 或 GEMINI_API_KEY(发现:{{keys}})",
|
||||||
"commonConfigInvalidValues": "通用配置片段的值必须是字符串",
|
"commonConfigInvalidValues": "通用配置片段的值必须是字符串",
|
||||||
"noCommonConfigToApply": "通用配置片段为空或没有可写入的内容",
|
"noCommonConfigToApply": "通用配置片段为空或没有可写入的内容",
|
||||||
|
|||||||
+5
-10
@@ -81,7 +81,10 @@ export async function getCommonConfigSnippet(
|
|||||||
* 设置通用配置片段(统一接口)
|
* 设置通用配置片段(统一接口)
|
||||||
* @param appType - 应用类型(claude/codex/gemini)
|
* @param appType - 应用类型(claude/codex/gemini)
|
||||||
* @param snippet - 通用配置片段(原始字符串)
|
* @param snippet - 通用配置片段(原始字符串)
|
||||||
* @throws 如果格式无效(Claude/Gemini 验证 JSON,Codex 暂不验证)
|
* @throws 如果格式无效
|
||||||
|
* - Claude: 验证 JSON 格式
|
||||||
|
* - Gemini: 验证 ENV/JSON 格式,拒绝包含禁用键(GEMINI_API_KEY/GOOGLE_GEMINI_BASE_URL)
|
||||||
|
* - Codex: TOML 格式,暂不验证
|
||||||
*/
|
*/
|
||||||
export async function setCommonConfigSnippet(
|
export async function setCommonConfigSnippet(
|
||||||
appType: AppType,
|
appType: AppType,
|
||||||
@@ -98,7 +101,7 @@ export async function setCommonConfigSnippet(
|
|||||||
*
|
*
|
||||||
* @param appType - 应用类型(claude/codex/gemini)
|
* @param appType - 应用类型(claude/codex/gemini)
|
||||||
* @param options - 可选:提取来源
|
* @param options - 可选:提取来源
|
||||||
* @returns 提取的通用配置片段(JSON/TOML 字符串)
|
* @returns 提取的通用配置片段(Claude: JSON, Codex: TOML, Gemini: ENV)
|
||||||
*/
|
*/
|
||||||
export type ExtractCommonConfigSnippetOptions = {
|
export type ExtractCommonConfigSnippetOptions = {
|
||||||
settingsConfig?: string;
|
settingsConfig?: string;
|
||||||
@@ -201,10 +204,6 @@ async function executeSyncWithLock(appType: AppType): Promise<void> {
|
|||||||
// 输出结果
|
// 输出结果
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
console.warn(`[syncCommonConfig] ${appType} 同步失败: ${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 {
|
try {
|
||||||
await providersApi.update(providerToSave, appType);
|
await providersApi.update(providerToSave, appType);
|
||||||
updatedCount++;
|
updatedCount++;
|
||||||
console.log(`[syncCommonConfig] 已更新供应商 ${id}`);
|
|
||||||
} catch (updateError) {
|
} catch (updateError) {
|
||||||
errors.push(`供应商 ${id}: 保存失败 - ${String(updateError)}`);
|
errors.push(`供应商 ${id}: 保存失败 - ${String(updateError)}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = (
|
export const hasGeminiCommonConfigSnippet = (
|
||||||
jsonString: string,
|
jsonString: string,
|
||||||
snippetString: string,
|
snippetString: string,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
try {
|
try {
|
||||||
if (!snippetString.trim()) return false;
|
if (!snippetString.trim()) return false;
|
||||||
|
|
||||||
|
// 解析 provider 的 settingsConfig
|
||||||
const config = jsonString ? JSON.parse(jsonString) : {};
|
const config = jsonString ? JSON.parse(jsonString) : {};
|
||||||
if (!isPlainObject(config)) return false;
|
if (!isPlainObject(config)) return false;
|
||||||
const envValue = (config as Record<string, unknown>).env;
|
const envValue = (config as Record<string, unknown>).env;
|
||||||
@@ -79,27 +92,18 @@ export const hasGeminiCommonConfigSnippet = (
|
|||||||
unknown
|
unknown
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const parsed = JSON.parse(snippetString);
|
// 使用 parseGeminiCommonConfigSnippet 解析 snippet (支持 ENV/JSON)
|
||||||
if (!isPlainObject(parsed)) return false;
|
const parseResult = parseGeminiCommonConfigSnippet(snippetString, {
|
||||||
|
strictForbiddenKeys: false, // 过滤禁用键而非报错
|
||||||
|
});
|
||||||
|
|
||||||
const entries = Object.entries(parsed).filter(
|
// 解析失败或为空
|
||||||
(entry): entry is [string, string] => {
|
if (parseResult.error || Object.keys(parseResult.env).length === 0) {
|
||||||
const [key, value] = entry;
|
return false;
|
||||||
if (
|
}
|
||||||
GEMINI_COMMON_ENV_FORBIDDEN_KEYS.includes(
|
|
||||||
key as GeminiForbiddenEnvKey,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (typeof value !== "string") return false;
|
|
||||||
return value.trim().length > 0;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (entries.length === 0) return false;
|
// 检查所有有效键值对是否都存在于 provider.env 中
|
||||||
|
return Object.entries(parseResult.env).every(([key, value]) => {
|
||||||
return entries.every(([key, value]) => {
|
|
||||||
const current = env[key];
|
const current = env[key];
|
||||||
return typeof current === "string" && current === value.trim();
|
return typeof current === "string" && current === value.trim();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user