mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 21:30:17 +08:00
6dd809701b
* refactor(proxy): simplify logging for better readability - Delete 17 verbose debug logs from handlers, streaming, and response_processor - Convert excessive INFO logs to DEBUG level for internal processing details - Add 2 critical INFO logs in forwarder.rs for failover scenarios: - Log when switching to next provider after failure - Log when all providers have been exhausted - Fix clippy uninlined_format_args warning This reduces log noise while maintaining visibility into key user-facing decisions. * fix: replace unsafe unwrap() calls with proper error handling - database/dao/mcp.rs: Use map_err for serde_json serialization - database/dao/providers.rs: Use map_err for settings_config and meta serialization - commands/misc.rs: Use expect() for compile-time regex pattern - services/prompt.rs: Use unwrap_or_default() for SystemTime - deeplink/provider.rs: Replace unwrap() with is_none_or pattern for Option checks Reduces potential panic points from 26 to 1 (static regex init, safe). * refactor(proxy): simplify verbose logging output - Remove response JSON full output logging in response_processor - Remove per-request INFO logs in provider_router (failover status, provider selection) - Change model mapping log from INFO to DEBUG - Change usage logging failure from INFO to WARN - Remove redundant debug logs for circuit breaker operations Reduces log noise significantly while preserving important warnings and errors. * feat(proxy): add structured log codes for i18n support Add error code system to proxy module logs for multi-language support: - CB-001~006: Circuit breaker state transitions and triggers - SRV-001~004: Proxy server lifecycle events - FWD-001~002: Request forwarding and failover - FO-001~005: Failover switch operations - USG-001~002: Usage logging errors Log format: [CODE] Chinese message Frontend/log tools can map codes to any language. New file: src/proxy/log_codes.rs - centralized code definitions * chore: bump version to 3.9.1 * style: format code with prettier and rustfmt * fix(ui): allow number inputs to be fully cleared before saving - Convert numeric state to string type for controlled inputs - Use isNaN() check instead of || fallback to allow 0 values - Apply fix to ProxyPanel, CircuitBreakerConfigPanel, AutoFailoverConfigPanel, and ModelTestConfigPanel * feat(pricing): support @ separator in model name matching - Refactor model name cleaning into chained method calls - Add @ to - replacement (e.g., gpt-5.2-codex@low → gpt-5.2-codex-low) - Add test case for @ separator matching * fix(proxy): improve validation and error handling in proxy config panels - Add StopTimeout/StopFailed error types for proper stop() error reporting - Replace silent clamp with validation-and-block in config panels - Add listenAddress format validation in ProxyPanel - Use log_codes constants instead of hardcoded strings - Use once_cell::Lazy for regex precompilation * fix(proxy): harden error handling and input validation - Handle RwLock poisoning in settings.rs with unwrap_or_else - Add fallback for dirs::home_dir() in config modules - Normalize localhost to 127.0.0.1 in ProxyPanel - Format IPv6 addresses with brackets for valid URLs - Strict port validation with pure digit regex - Treat NaN as validation failure in config panels - Log warning on cost_multiplier parse failure - Align timeoutSeconds range to [0, 300] across all panels
376 lines
14 KiB
Rust
376 lines
14 KiB
Rust
//! 供应商数据访问对象
|
||
//!
|
||
//! 提供供应商(Provider)的 CRUD 操作。
|
||
|
||
use crate::database::{lock_conn, Database};
|
||
use crate::error::AppError;
|
||
use crate::provider::{Provider, ProviderMeta};
|
||
use indexmap::IndexMap;
|
||
use rusqlite::params;
|
||
use std::collections::HashMap;
|
||
|
||
impl Database {
|
||
/// 获取指定应用类型的所有供应商
|
||
pub fn get_all_providers(
|
||
&self,
|
||
app_type: &str,
|
||
) -> Result<IndexMap<String, Provider>, AppError> {
|
||
let conn = lock_conn!(self.conn);
|
||
let mut stmt = conn.prepare(
|
||
"SELECT id, name, settings_config, website_url, category, created_at, sort_index, notes, icon, icon_color, meta, in_failover_queue
|
||
FROM providers WHERE app_type = ?1
|
||
ORDER BY COALESCE(sort_index, 999999), created_at ASC, id ASC"
|
||
).map_err(|e| AppError::Database(e.to_string()))?;
|
||
|
||
let provider_iter = stmt
|
||
.query_map(params![app_type], |row| {
|
||
let id: String = row.get(0)?;
|
||
let name: String = row.get(1)?;
|
||
let settings_config_str: String = row.get(2)?;
|
||
let website_url: Option<String> = row.get(3)?;
|
||
let category: Option<String> = row.get(4)?;
|
||
let created_at: Option<i64> = row.get(5)?;
|
||
let sort_index: Option<usize> = row.get(6)?;
|
||
let notes: Option<String> = row.get(7)?;
|
||
let icon: Option<String> = row.get(8)?;
|
||
let icon_color: Option<String> = row.get(9)?;
|
||
let meta_str: String = row.get(10)?;
|
||
let in_failover_queue: bool = row.get(11)?;
|
||
|
||
let settings_config =
|
||
serde_json::from_str(&settings_config_str).unwrap_or(serde_json::Value::Null);
|
||
let meta: ProviderMeta = serde_json::from_str(&meta_str).unwrap_or_default();
|
||
|
||
Ok((
|
||
id,
|
||
Provider {
|
||
id: "".to_string(), // Placeholder, set below
|
||
name,
|
||
settings_config,
|
||
website_url,
|
||
category,
|
||
created_at,
|
||
sort_index,
|
||
notes,
|
||
meta: Some(meta),
|
||
icon,
|
||
icon_color,
|
||
in_failover_queue,
|
||
},
|
||
))
|
||
})
|
||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||
|
||
let mut providers = IndexMap::new();
|
||
for provider_res in provider_iter {
|
||
let (id, mut provider) = provider_res.map_err(|e| AppError::Database(e.to_string()))?;
|
||
provider.id = id.clone();
|
||
|
||
// 加载 endpoints
|
||
let mut stmt_endpoints = conn.prepare(
|
||
"SELECT url, added_at FROM provider_endpoints WHERE provider_id = ?1 AND app_type = ?2 ORDER BY added_at ASC, url ASC"
|
||
).map_err(|e| AppError::Database(e.to_string()))?;
|
||
|
||
let endpoints_iter = stmt_endpoints
|
||
.query_map(params![id, app_type], |row| {
|
||
let url: String = row.get(0)?;
|
||
let added_at: Option<i64> = row.get(1)?;
|
||
Ok((
|
||
url,
|
||
crate::settings::CustomEndpoint {
|
||
url: "".to_string(),
|
||
added_at: added_at.unwrap_or(0),
|
||
last_used: None,
|
||
},
|
||
))
|
||
})
|
||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||
|
||
let mut custom_endpoints = HashMap::new();
|
||
for ep_res in endpoints_iter {
|
||
let (url, mut ep) = ep_res.map_err(|e| AppError::Database(e.to_string()))?;
|
||
ep.url = url.clone();
|
||
custom_endpoints.insert(url, ep);
|
||
}
|
||
|
||
if let Some(meta) = &mut provider.meta {
|
||
meta.custom_endpoints = custom_endpoints;
|
||
}
|
||
|
||
providers.insert(id, provider);
|
||
}
|
||
|
||
Ok(providers)
|
||
}
|
||
|
||
/// 获取当前激活的供应商 ID
|
||
pub fn get_current_provider(&self, app_type: &str) -> Result<Option<String>, AppError> {
|
||
let conn = lock_conn!(self.conn);
|
||
let mut stmt = conn
|
||
.prepare("SELECT id FROM providers WHERE app_type = ?1 AND is_current = 1 LIMIT 1")
|
||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||
|
||
let mut rows = stmt
|
||
.query(params![app_type])
|
||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||
|
||
if let Some(row) = rows.next().map_err(|e| AppError::Database(e.to_string()))? {
|
||
Ok(Some(
|
||
row.get(0).map_err(|e| AppError::Database(e.to_string()))?,
|
||
))
|
||
} else {
|
||
Ok(None)
|
||
}
|
||
}
|
||
|
||
/// 根据 ID 获取单个供应商
|
||
pub fn get_provider_by_id(
|
||
&self,
|
||
id: &str,
|
||
app_type: &str,
|
||
) -> Result<Option<Provider>, AppError> {
|
||
let conn = lock_conn!(self.conn);
|
||
let result = conn.query_row(
|
||
"SELECT name, settings_config, website_url, category, created_at, sort_index, notes, icon, icon_color, meta, in_failover_queue
|
||
FROM providers WHERE id = ?1 AND app_type = ?2",
|
||
params![id, app_type],
|
||
|row| {
|
||
let name: String = row.get(0)?;
|
||
let settings_config_str: String = row.get(1)?;
|
||
let website_url: Option<String> = row.get(2)?;
|
||
let category: Option<String> = row.get(3)?;
|
||
let created_at: Option<i64> = row.get(4)?;
|
||
let sort_index: Option<usize> = row.get(5)?;
|
||
let notes: Option<String> = row.get(6)?;
|
||
let icon: Option<String> = row.get(7)?;
|
||
let icon_color: Option<String> = row.get(8)?;
|
||
let meta_str: String = row.get(9)?;
|
||
let in_failover_queue: bool = row.get(10)?;
|
||
|
||
let settings_config = serde_json::from_str(&settings_config_str).unwrap_or(serde_json::Value::Null);
|
||
let meta: ProviderMeta = serde_json::from_str(&meta_str).unwrap_or_default();
|
||
|
||
Ok(Provider {
|
||
id: id.to_string(),
|
||
name,
|
||
settings_config,
|
||
website_url,
|
||
category,
|
||
created_at,
|
||
sort_index,
|
||
notes,
|
||
meta: Some(meta),
|
||
icon,
|
||
icon_color,
|
||
in_failover_queue,
|
||
})
|
||
},
|
||
);
|
||
|
||
match result {
|
||
Ok(provider) => Ok(Some(provider)),
|
||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||
Err(e) => Err(AppError::Database(e.to_string())),
|
||
}
|
||
}
|
||
|
||
/// 保存供应商(新增或更新)
|
||
///
|
||
/// 注意:更新模式下不同步 endpoints,因为编辑模式下端点通过单独的 API 管理
|
||
/// (add_custom_endpoint / remove_custom_endpoint),避免覆盖用户的修改。
|
||
pub fn save_provider(&self, app_type: &str, provider: &Provider) -> Result<(), AppError> {
|
||
let mut conn = lock_conn!(self.conn);
|
||
let tx = conn
|
||
.transaction()
|
||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||
|
||
// 处理 meta:取出 endpoints 以便单独处理
|
||
let mut meta_clone = provider.meta.clone().unwrap_or_default();
|
||
let endpoints = std::mem::take(&mut meta_clone.custom_endpoints);
|
||
|
||
// 检查是否存在(用于判断新增/更新,以及保留 is_current 和 in_failover_queue)
|
||
let existing: Option<(bool, bool)> = tx
|
||
.query_row(
|
||
"SELECT is_current, in_failover_queue FROM providers WHERE id = ?1 AND app_type = ?2",
|
||
params![provider.id, app_type],
|
||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||
)
|
||
.ok();
|
||
|
||
let is_update = existing.is_some();
|
||
let (is_current, in_failover_queue) =
|
||
existing.unwrap_or((false, provider.in_failover_queue));
|
||
|
||
if is_update {
|
||
// 更新模式:使用 UPDATE 避免触发 ON DELETE CASCADE
|
||
tx.execute(
|
||
"UPDATE providers SET
|
||
name = ?1,
|
||
settings_config = ?2,
|
||
website_url = ?3,
|
||
category = ?4,
|
||
created_at = ?5,
|
||
sort_index = ?6,
|
||
notes = ?7,
|
||
icon = ?8,
|
||
icon_color = ?9,
|
||
meta = ?10,
|
||
is_current = ?11,
|
||
in_failover_queue = ?12
|
||
WHERE id = ?13 AND app_type = ?14",
|
||
params![
|
||
provider.name,
|
||
serde_json::to_string(&provider.settings_config).map_err(|e| {
|
||
AppError::Database(format!("Failed to serialize settings_config: {e}"))
|
||
})?,
|
||
provider.website_url,
|
||
provider.category,
|
||
provider.created_at,
|
||
provider.sort_index,
|
||
provider.notes,
|
||
provider.icon,
|
||
provider.icon_color,
|
||
serde_json::to_string(&meta_clone).map_err(|e| AppError::Database(format!(
|
||
"Failed to serialize meta: {e}"
|
||
)))?,
|
||
is_current,
|
||
in_failover_queue,
|
||
provider.id,
|
||
app_type,
|
||
],
|
||
)
|
||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||
} else {
|
||
// 新增模式:使用 INSERT
|
||
tx.execute(
|
||
"INSERT INTO providers (
|
||
id, app_type, name, settings_config, website_url, category,
|
||
created_at, sort_index, notes, icon, icon_color, meta, is_current, in_failover_queue
|
||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)",
|
||
params![
|
||
provider.id,
|
||
app_type,
|
||
provider.name,
|
||
serde_json::to_string(&provider.settings_config)
|
||
.map_err(|e| AppError::Database(format!("Failed to serialize settings_config: {e}")))?,
|
||
provider.website_url,
|
||
provider.category,
|
||
provider.created_at,
|
||
provider.sort_index,
|
||
provider.notes,
|
||
provider.icon,
|
||
provider.icon_color,
|
||
serde_json::to_string(&meta_clone)
|
||
.map_err(|e| AppError::Database(format!("Failed to serialize meta: {e}")))?,
|
||
is_current,
|
||
in_failover_queue,
|
||
],
|
||
)
|
||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||
|
||
// 只有新增时才同步 endpoints
|
||
for (url, endpoint) in endpoints {
|
||
tx.execute(
|
||
"INSERT INTO provider_endpoints (provider_id, app_type, url, added_at)
|
||
VALUES (?1, ?2, ?3, ?4)",
|
||
params![provider.id, app_type, url, endpoint.added_at],
|
||
)
|
||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||
}
|
||
}
|
||
|
||
tx.commit().map_err(|e| AppError::Database(e.to_string()))?;
|
||
Ok(())
|
||
}
|
||
|
||
/// 删除供应商
|
||
pub fn delete_provider(&self, app_type: &str, id: &str) -> Result<(), AppError> {
|
||
let conn = lock_conn!(self.conn);
|
||
conn.execute(
|
||
"DELETE FROM providers WHERE id = ?1 AND app_type = ?2",
|
||
params![id, app_type],
|
||
)
|
||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||
Ok(())
|
||
}
|
||
|
||
/// 设置当前供应商
|
||
pub fn set_current_provider(&self, app_type: &str, id: &str) -> Result<(), AppError> {
|
||
let mut conn = lock_conn!(self.conn);
|
||
let tx = conn
|
||
.transaction()
|
||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||
|
||
// 重置所有为 0
|
||
tx.execute(
|
||
"UPDATE providers SET is_current = 0 WHERE app_type = ?1",
|
||
params![app_type],
|
||
)
|
||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||
|
||
// 设置新的当前供应商
|
||
tx.execute(
|
||
"UPDATE providers SET is_current = 1 WHERE id = ?1 AND app_type = ?2",
|
||
params![id, app_type],
|
||
)
|
||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||
|
||
tx.commit().map_err(|e| AppError::Database(e.to_string()))?;
|
||
Ok(())
|
||
}
|
||
|
||
/// 更新供应商的 settings_config(仅更新配置,不改变其他字段)
|
||
pub fn update_provider_settings_config(
|
||
&self,
|
||
app_type: &str,
|
||
provider_id: &str,
|
||
settings_config: &serde_json::Value,
|
||
) -> Result<(), AppError> {
|
||
let conn = lock_conn!(self.conn);
|
||
conn.execute(
|
||
"UPDATE providers SET settings_config = ?1 WHERE id = ?2 AND app_type = ?3",
|
||
params![
|
||
serde_json::to_string(settings_config).map_err(|e| AppError::Database(format!(
|
||
"Failed to serialize settings_config: {e}"
|
||
)))?,
|
||
provider_id,
|
||
app_type
|
||
],
|
||
)
|
||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||
Ok(())
|
||
}
|
||
|
||
/// 添加自定义端点
|
||
pub fn add_custom_endpoint(
|
||
&self,
|
||
app_type: &str,
|
||
provider_id: &str,
|
||
url: &str,
|
||
) -> Result<(), AppError> {
|
||
let conn = lock_conn!(self.conn);
|
||
let added_at = chrono::Utc::now().timestamp_millis();
|
||
conn.execute(
|
||
"INSERT INTO provider_endpoints (provider_id, app_type, url, added_at) VALUES (?1, ?2, ?3, ?4)",
|
||
params![provider_id, app_type, url, added_at],
|
||
).map_err(|e| AppError::Database(e.to_string()))?;
|
||
Ok(())
|
||
}
|
||
|
||
/// 移除自定义端点
|
||
pub fn remove_custom_endpoint(
|
||
&self,
|
||
app_type: &str,
|
||
provider_id: &str,
|
||
url: &str,
|
||
) -> Result<(), AppError> {
|
||
let conn = lock_conn!(self.conn);
|
||
conn.execute(
|
||
"DELETE FROM provider_endpoints WHERE provider_id = ?1 AND app_type = ?2 AND url = ?3",
|
||
params![provider_id, app_type, url],
|
||
)
|
||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||
Ok(())
|
||
}
|
||
}
|