mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-02 10:21:16 +08:00
Merge branch 'main' into feat/gemini-proxy-integration
# Conflicts: # src-tauri/src/proxy/providers/claude.rs # src/i18n/locales/en.json # src/i18n/locales/ja.json # src/i18n/locales/zh.json
This commit is contained in:
@@ -41,7 +41,7 @@ pub async fn fetch_models(
|
||||
}
|
||||
|
||||
let models_url = build_models_url(base_url, is_full_url)?;
|
||||
let client = crate::proxy::http_client::get_for_provider(None);
|
||||
let client = crate::proxy::http_client::get();
|
||||
|
||||
let response = client
|
||||
.get(&models_url)
|
||||
|
||||
@@ -1099,7 +1099,7 @@ pub(crate) fn write_gemini_live(provider: &Provider) -> Result<(), AppError> {
|
||||
// One-time auth type detection to avoid repeated detection
|
||||
let auth_type = detect_gemini_auth_type(provider);
|
||||
|
||||
let mut env_map = json_to_env(&provider.settings_config)?;
|
||||
let env_map = json_to_env(&provider.settings_config)?;
|
||||
|
||||
// Prepare config to write to ~/.gemini/settings.json
|
||||
// Behavior:
|
||||
@@ -1143,17 +1143,12 @@ pub(crate) fn write_gemini_live(provider: &Provider) -> Result<(), AppError> {
|
||||
|
||||
match auth_type {
|
||||
GeminiAuthType::GoogleOfficial => {
|
||||
// Google official uses OAuth, clear env
|
||||
env_map.clear();
|
||||
// Google Official uses OAuth, no API key validation needed.
|
||||
// Write user's env vars as-is (e.g. GEMINI_MODEL, custom vars).
|
||||
write_gemini_env_atomic(&env_map)?;
|
||||
}
|
||||
GeminiAuthType::Packycode => {
|
||||
// PackyCode provider, uses API Key (strict validation on switch)
|
||||
validate_gemini_settings_strict(&provider.settings_config)?;
|
||||
write_gemini_env_atomic(&env_map)?;
|
||||
}
|
||||
GeminiAuthType::Generic => {
|
||||
// Generic provider, uses API Key (strict validation on switch)
|
||||
GeminiAuthType::Packycode | GeminiAuthType::Generic => {
|
||||
// API Key mode -- require GEMINI_API_KEY
|
||||
validate_gemini_settings_strict(&provider.settings_config)?;
|
||||
write_gemini_env_atomic(&env_map)?;
|
||||
}
|
||||
|
||||
@@ -1407,6 +1407,16 @@ impl ProviderService {
|
||||
// Hot-switch only when BOTH: this app is taken over AND proxy server is actually running
|
||||
let should_hot_switch = (is_app_taken_over || live_taken_over) && is_proxy_running;
|
||||
|
||||
// Block switching to official providers when proxy takeover is active.
|
||||
// Using a proxy with official APIs (Anthropic/OpenAI/Google) may cause account bans.
|
||||
if should_hot_switch && _provider.category.as_deref() == Some("official") {
|
||||
return Err(AppError::localized(
|
||||
"switch.official_blocked_by_proxy",
|
||||
"代理接管模式下不能切换到官方供应商,使用代理访问官方 API 可能导致账号被封禁。请先关闭代理接管,或选择第三方供应商。",
|
||||
"Cannot switch to official provider while proxy takeover is active. Using proxy with official APIs may cause account bans.",
|
||||
));
|
||||
}
|
||||
|
||||
if should_hot_switch {
|
||||
// Proxy takeover mode: hot-switch only, don't write Live config
|
||||
log::info!(
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::services::provider::{
|
||||
use serde_json::{json, Value};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tauri::Emitter;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// 用于接管 Live 配置时的占位符(避免客户端提示缺少 key,同时不泄露真实 Token)
|
||||
@@ -375,6 +376,26 @@ impl ProxyService {
|
||||
|
||||
// 7) 兼容旧逻辑:写入 any-of 标志(失败不影响功能)
|
||||
let _ = self.db.set_live_takeover_active(true).await;
|
||||
|
||||
// 8) Warn if the current provider is official (risk of account ban via proxy)
|
||||
if let Ok(Some(current_id)) =
|
||||
crate::settings::get_effective_current_provider(&self.db, &app)
|
||||
{
|
||||
if let Ok(Some(provider)) = self.db.get_provider_by_id(¤t_id, app_type_str) {
|
||||
if provider.category.as_deref() == Some("official") {
|
||||
if let Some(handle) = self.app_handle.read().await.as_ref() {
|
||||
let _ = handle.emit(
|
||||
"proxy-official-warning",
|
||||
serde_json::json!({
|
||||
"appType": app_type_str,
|
||||
"providerName": provider.name,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -1548,6 +1569,14 @@ impl ProxyService {
|
||||
.map_err(|e| format!("读取供应商失败: {e}"))?
|
||||
.ok_or_else(|| format!("供应商不存在: {provider_id}"))?;
|
||||
|
||||
// Defense-in-depth: block official providers during proxy takeover
|
||||
if provider.category.as_deref() == Some("official") {
|
||||
return Err(
|
||||
"代理接管模式下不能切换到官方供应商 (Cannot switch to official provider during proxy takeover)"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let logical_target_changed =
|
||||
crate::settings::get_effective_current_provider(&self.db, &app_type_enum)
|
||||
.map_err(|e| format!("读取当前供应商失败: {e}"))?
|
||||
|
||||
@@ -278,7 +278,11 @@ fn sync_single_file(db: &Database, file_path: &Path) -> Result<(u32, u32), AppEr
|
||||
continue;
|
||||
}
|
||||
|
||||
let request_id = format!("session:{}", msg.message_id);
|
||||
let request_id = format!(
|
||||
"{}{}",
|
||||
crate::proxy::usage::parser::SESSION_REQUEST_ID_PREFIX,
|
||||
msg.message_id
|
||||
);
|
||||
|
||||
// 跳过 output_tokens 为 0 的无意义条目
|
||||
if msg.output_tokens == 0 {
|
||||
@@ -379,6 +383,7 @@ fn insert_session_log_entry(
|
||||
cache_read_tokens: msg.cache_read_tokens,
|
||||
cache_creation_tokens: msg.cache_creation_tokens,
|
||||
model: Some(msg.model.clone()),
|
||||
message_id: None,
|
||||
};
|
||||
|
||||
let pricing = find_model_pricing_for_session(&conn, &msg.model);
|
||||
|
||||
@@ -474,6 +474,7 @@ fn insert_codex_session_entry(
|
||||
cache_read_tokens: delta.cached_input,
|
||||
cache_creation_tokens: 0,
|
||||
model: Some(model.to_string()),
|
||||
message_id: None,
|
||||
};
|
||||
|
||||
let pricing = find_codex_pricing(&conn, model);
|
||||
|
||||
@@ -261,6 +261,7 @@ fn insert_gemini_session_entry(
|
||||
cache_read_tokens: tokens.cached,
|
||||
cache_creation_tokens: 0,
|
||||
model: Some(model.to_string()),
|
||||
message_id: None,
|
||||
};
|
||||
|
||||
let pricing = find_gemini_pricing(&conn, model);
|
||||
|
||||
@@ -680,6 +680,13 @@ impl SkillService {
|
||||
found.display()
|
||||
);
|
||||
source = found;
|
||||
} else if temp_dir.join("SKILL.md").exists() {
|
||||
// 根级 Skill:仓库本身就是 skill,SKILL.md 直接在解压根目录
|
||||
log::info!(
|
||||
"Skill directory '{}' not found, but SKILL.md exists at root, using temp_dir",
|
||||
target_name,
|
||||
);
|
||||
source = temp_dir.clone();
|
||||
} else {
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
return Err(anyhow!(format_skill_error(
|
||||
|
||||
@@ -220,9 +220,8 @@ impl StreamCheckService {
|
||||
.or_else(|| adapter.extract_auth(provider))
|
||||
.ok_or_else(|| AppError::Message("API Key not found".to_string()))?;
|
||||
|
||||
// 获取 HTTP 客户端:优先使用供应商单独代理配置,否则使用全局客户端
|
||||
let proxy_config = provider.meta.as_ref().and_then(|m| m.proxy_config.as_ref());
|
||||
let client = crate::proxy::http_client::get_for_provider(proxy_config);
|
||||
// 获取 HTTP 客户端
|
||||
let client = crate::proxy::http_client::get();
|
||||
let request_timeout = std::time::Duration::from_secs(config.timeout_secs);
|
||||
|
||||
let model_to_test = Self::resolve_test_model(app_type, provider, config);
|
||||
@@ -274,34 +273,11 @@ impl StreamCheckService {
|
||||
};
|
||||
|
||||
let response_time = start.elapsed().as_millis() as u64;
|
||||
let tested_at = chrono::Utc::now().timestamp();
|
||||
|
||||
match result {
|
||||
Ok((status_code, model)) => {
|
||||
let health_status =
|
||||
Self::determine_status(response_time, config.degraded_threshold_ms);
|
||||
Ok(StreamCheckResult {
|
||||
status: health_status,
|
||||
success: true,
|
||||
message: "Check succeeded".to_string(),
|
||||
response_time_ms: Some(response_time),
|
||||
http_status: Some(status_code),
|
||||
model_used: model,
|
||||
tested_at,
|
||||
retry_count: 0,
|
||||
})
|
||||
}
|
||||
Err(e) => Ok(StreamCheckResult {
|
||||
status: HealthStatus::Failed,
|
||||
success: false,
|
||||
message: e.to_string(),
|
||||
response_time_ms: Some(response_time),
|
||||
http_status: None,
|
||||
model_used: String::new(),
|
||||
tested_at,
|
||||
retry_count: 0,
|
||||
}),
|
||||
}
|
||||
Ok(Self::build_stream_check_result(
|
||||
result,
|
||||
response_time,
|
||||
config.degraded_threshold_ms,
|
||||
))
|
||||
}
|
||||
|
||||
/// Claude 流式检查
|
||||
@@ -385,7 +361,7 @@ impl StreamCheckService {
|
||||
anthropic_to_gemini(anthropic_body)
|
||||
.map_err(|e| AppError::Message(format!("Failed to build test request: {e}")))?
|
||||
} else if is_openai_chat {
|
||||
anthropic_to_openai(anthropic_body, Some(&provider.id))
|
||||
anthropic_to_openai(anthropic_body)
|
||||
.map_err(|e| AppError::Message(format!("Failed to build test request: {e}")))?
|
||||
} else {
|
||||
anthropic_body
|
||||
@@ -506,7 +482,7 @@ impl StreamCheckService {
|
||||
|
||||
if !response.status().is_success() {
|
||||
let error_text = response.text().await.unwrap_or_default();
|
||||
return Err(AppError::Message(format!("HTTP {status}: {error_text}")));
|
||||
return Err(Self::http_status_error(status, error_text));
|
||||
}
|
||||
|
||||
// 流式读取:只需首个 chunk
|
||||
@@ -586,7 +562,7 @@ impl StreamCheckService {
|
||||
if i == 0 && status == 404 && urls.len() > 1 {
|
||||
continue;
|
||||
}
|
||||
return Err(AppError::Message(format!("HTTP {status}: {error_text}")));
|
||||
return Err(Self::http_status_error(status, error_text));
|
||||
}
|
||||
|
||||
let mut stream = response.bytes_stream();
|
||||
@@ -661,7 +637,7 @@ impl StreamCheckService {
|
||||
|
||||
if !response.status().is_success() {
|
||||
let error_text = response.text().await.unwrap_or_default();
|
||||
return Err(AppError::Message(format!("HTTP {status}: {error_text}")));
|
||||
return Err(Self::http_status_error(status, error_text));
|
||||
}
|
||||
|
||||
let mut stream = response.bytes_stream();
|
||||
@@ -689,9 +665,8 @@ impl StreamCheckService {
|
||||
config: &StreamCheckConfig,
|
||||
start: Instant,
|
||||
) -> Result<StreamCheckResult, AppError> {
|
||||
// 获取 HTTP 客户端:优先使用供应商单独代理配置,否则使用全局客户端
|
||||
let proxy_config = provider.meta.as_ref().and_then(|m| m.proxy_config.as_ref());
|
||||
let client = crate::proxy::http_client::get_for_provider(proxy_config);
|
||||
// 获取 HTTP 客户端
|
||||
let client = crate::proxy::http_client::get();
|
||||
let request_timeout = std::time::Duration::from_secs(config.timeout_secs);
|
||||
|
||||
let model_to_test = Self::resolve_test_model(app_type, provider, config);
|
||||
@@ -749,16 +724,25 @@ impl StreamCheckService {
|
||||
tested_at,
|
||||
retry_count: 0,
|
||||
},
|
||||
Err(e) => StreamCheckResult {
|
||||
status: HealthStatus::Failed,
|
||||
success: false,
|
||||
message: e.to_string(),
|
||||
response_time_ms: Some(response_time),
|
||||
http_status: None,
|
||||
model_used: String::new(),
|
||||
tested_at,
|
||||
retry_count: 0,
|
||||
},
|
||||
Err(e) => {
|
||||
let (http_status, message) = match &e {
|
||||
AppError::HttpStatus { status, .. } => (
|
||||
Some(*status),
|
||||
Self::classify_http_status(*status).to_string(),
|
||||
),
|
||||
_ => (None, e.to_string()),
|
||||
};
|
||||
StreamCheckResult {
|
||||
status: HealthStatus::Failed,
|
||||
success: false,
|
||||
message,
|
||||
response_time_ms: Some(response_time),
|
||||
http_status,
|
||||
model_used: String::new(),
|
||||
tested_at,
|
||||
retry_count: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1156,6 +1140,39 @@ impl StreamCheckService {
|
||||
}
|
||||
}
|
||||
|
||||
/// 构造 HTTP 状态码错误,截断过长的响应体
|
||||
fn http_status_error(status: u16, body: String) -> AppError {
|
||||
let body = if body.len() > 200 {
|
||||
// 安全截断:找到 200 字节内最近的 char 边界
|
||||
let mut end = 200;
|
||||
while end > 0 && !body.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
format!("{}…", &body[..end])
|
||||
} else {
|
||||
body
|
||||
};
|
||||
AppError::HttpStatus { status, body }
|
||||
}
|
||||
|
||||
/// 将 HTTP 状态码映射为简短的分类标签
|
||||
pub(crate) fn classify_http_status(status: u16) -> &'static str {
|
||||
match status {
|
||||
400 => "Bad request (400)",
|
||||
401 => "Auth rejected (401)",
|
||||
402 => "Payment required (402)",
|
||||
403 => "Access denied (403)",
|
||||
404 => "Not found (404)",
|
||||
429 => "Rate limited (429)",
|
||||
500 => "Internal server error (500)",
|
||||
502 => "Bad gateway (502)",
|
||||
503 => "Service unavailable (503)",
|
||||
504 => "Gateway timeout (504)",
|
||||
s if (500..600).contains(&s) => "Server error",
|
||||
_ => "HTTP error",
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_test_model(
|
||||
app_type: &AppType,
|
||||
provider: &Provider,
|
||||
|
||||
Reference in New Issue
Block a user