mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-28 00:35:32 +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() {
|
||||
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 格式,暂不验证(前端验证)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user