mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-06-16 13:34:04 +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
264 lines
8.0 KiB
Rust
264 lines
8.0 KiB
Rust
use serde::{Deserialize, Serialize};
|
||
use std::fs;
|
||
use std::io::Write;
|
||
use std::path::{Path, PathBuf};
|
||
|
||
use crate::error::AppError;
|
||
|
||
/// 获取用户主目录,带回退和日志
|
||
fn get_home_dir() -> PathBuf {
|
||
dirs::home_dir().unwrap_or_else(|| {
|
||
log::warn!("无法获取用户主目录,回退到当前目录");
|
||
PathBuf::from(".")
|
||
})
|
||
}
|
||
|
||
/// 获取 Claude Code 配置目录路径
|
||
pub fn get_claude_config_dir() -> PathBuf {
|
||
if let Some(custom) = crate::settings::get_claude_override_dir() {
|
||
return custom;
|
||
}
|
||
|
||
get_home_dir().join(".claude")
|
||
}
|
||
|
||
/// 默认 Claude MCP 配置文件路径 (~/.claude.json)
|
||
pub fn get_default_claude_mcp_path() -> PathBuf {
|
||
get_home_dir().join(".claude.json")
|
||
}
|
||
|
||
fn derive_mcp_path_from_override(dir: &Path) -> Option<PathBuf> {
|
||
let file_name = dir
|
||
.file_name()
|
||
.map(|name| name.to_string_lossy().to_string())?
|
||
.trim()
|
||
.to_string();
|
||
if file_name.is_empty() {
|
||
return None;
|
||
}
|
||
let parent = dir.parent().unwrap_or_else(|| Path::new(""));
|
||
Some(parent.join(format!("{file_name}.json")))
|
||
}
|
||
|
||
/// 获取 Claude MCP 配置文件路径,若设置了目录覆盖则与覆盖目录同级
|
||
pub fn get_claude_mcp_path() -> PathBuf {
|
||
if let Some(custom_dir) = crate::settings::get_claude_override_dir() {
|
||
if let Some(path) = derive_mcp_path_from_override(&custom_dir) {
|
||
return path;
|
||
}
|
||
}
|
||
get_default_claude_mcp_path()
|
||
}
|
||
|
||
/// 获取 Claude Code 主配置文件路径
|
||
pub fn get_claude_settings_path() -> PathBuf {
|
||
let dir = get_claude_config_dir();
|
||
let settings = dir.join("settings.json");
|
||
if settings.exists() {
|
||
return settings;
|
||
}
|
||
// 兼容旧版命名:若存在旧文件则继续使用
|
||
let legacy = dir.join("claude.json");
|
||
if legacy.exists() {
|
||
return legacy;
|
||
}
|
||
// 默认新建:回落到标准文件名 settings.json(不再生成 claude.json)
|
||
settings
|
||
}
|
||
|
||
/// 获取应用配置目录路径 (~/.cc-switch)
|
||
pub fn get_app_config_dir() -> PathBuf {
|
||
if let Some(custom) = crate::app_store::get_app_config_dir_override() {
|
||
return custom;
|
||
}
|
||
|
||
dirs::home_dir()
|
||
.expect("无法获取用户主目录")
|
||
.join(".cc-switch")
|
||
}
|
||
|
||
/// 获取应用配置文件路径
|
||
pub fn get_app_config_path() -> PathBuf {
|
||
get_app_config_dir().join("config.json")
|
||
}
|
||
|
||
/// 清理供应商名称,确保文件名安全
|
||
#[allow(dead_code)]
|
||
pub fn sanitize_provider_name(name: &str) -> String {
|
||
name.chars()
|
||
.map(|c| match c {
|
||
'<' | '>' | ':' | '"' | '/' | '\\' | '|' | '?' | '*' => '-',
|
||
_ => c,
|
||
})
|
||
.collect::<String>()
|
||
.to_lowercase()
|
||
}
|
||
|
||
/// 获取供应商配置文件路径
|
||
#[allow(dead_code)]
|
||
pub fn get_provider_config_path(provider_id: &str, provider_name: Option<&str>) -> PathBuf {
|
||
let base_name = provider_name
|
||
.map(sanitize_provider_name)
|
||
.unwrap_or_else(|| sanitize_provider_name(provider_id));
|
||
|
||
get_claude_config_dir().join(format!("settings-{base_name}.json"))
|
||
}
|
||
|
||
/// 读取 JSON 配置文件
|
||
pub fn read_json_file<T: for<'a> Deserialize<'a>>(path: &Path) -> Result<T, AppError> {
|
||
if !path.exists() {
|
||
return Err(AppError::Config(format!("文件不存在: {}", path.display())));
|
||
}
|
||
|
||
let content = fs::read_to_string(path).map_err(|e| AppError::io(path, e))?;
|
||
|
||
serde_json::from_str(&content).map_err(|e| AppError::json(path, e))
|
||
}
|
||
|
||
/// 写入 JSON 配置文件
|
||
pub fn write_json_file<T: Serialize>(path: &Path, data: &T) -> Result<(), AppError> {
|
||
// 确保目录存在
|
||
if let Some(parent) = path.parent() {
|
||
fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
|
||
}
|
||
|
||
let json =
|
||
serde_json::to_string_pretty(data).map_err(|e| AppError::JsonSerialize { source: e })?;
|
||
|
||
atomic_write(path, json.as_bytes())
|
||
}
|
||
|
||
/// 原子写入文本文件(用于 TOML/纯文本)
|
||
pub fn write_text_file(path: &Path, data: &str) -> Result<(), AppError> {
|
||
if let Some(parent) = path.parent() {
|
||
fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
|
||
}
|
||
atomic_write(path, data.as_bytes())
|
||
}
|
||
|
||
/// 原子写入:写入临时文件后 rename 替换,避免半写状态
|
||
pub fn atomic_write(path: &Path, data: &[u8]) -> Result<(), AppError> {
|
||
if let Some(parent) = path.parent() {
|
||
fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
|
||
}
|
||
|
||
let parent = path
|
||
.parent()
|
||
.ok_or_else(|| AppError::Config("无效的路径".to_string()))?;
|
||
let mut tmp = parent.to_path_buf();
|
||
let file_name = path
|
||
.file_name()
|
||
.ok_or_else(|| AppError::Config("无效的文件名".to_string()))?
|
||
.to_string_lossy()
|
||
.to_string();
|
||
let ts = std::time::SystemTime::now()
|
||
.duration_since(std::time::UNIX_EPOCH)
|
||
.unwrap_or_default()
|
||
.as_nanos();
|
||
tmp.push(format!("{file_name}.tmp.{ts}"));
|
||
|
||
{
|
||
let mut f = fs::File::create(&tmp).map_err(|e| AppError::io(&tmp, e))?;
|
||
f.write_all(data).map_err(|e| AppError::io(&tmp, e))?;
|
||
f.flush().map_err(|e| AppError::io(&tmp, e))?;
|
||
}
|
||
|
||
#[cfg(unix)]
|
||
{
|
||
use std::os::unix::fs::PermissionsExt;
|
||
if let Ok(meta) = fs::metadata(path) {
|
||
let perm = meta.permissions().mode();
|
||
let _ = fs::set_permissions(&tmp, fs::Permissions::from_mode(perm));
|
||
}
|
||
}
|
||
|
||
#[cfg(windows)]
|
||
{
|
||
// Windows 上 rename 目标存在会失败,先移除再重命名(尽量接近原子性)
|
||
if path.exists() {
|
||
let _ = fs::remove_file(path);
|
||
}
|
||
fs::rename(&tmp, path).map_err(|e| AppError::IoContext {
|
||
context: format!("原子替换失败: {} -> {}", tmp.display(), path.display()),
|
||
source: e,
|
||
})?;
|
||
}
|
||
|
||
#[cfg(not(windows))]
|
||
{
|
||
fs::rename(&tmp, path).map_err(|e| AppError::IoContext {
|
||
context: format!("原子替换失败: {} -> {}", tmp.display(), path.display()),
|
||
source: e,
|
||
})?;
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn derive_mcp_path_from_override_preserves_folder_name() {
|
||
let override_dir = PathBuf::from("/tmp/profile/.claude");
|
||
let derived = derive_mcp_path_from_override(&override_dir)
|
||
.expect("should derive path for nested dir");
|
||
assert_eq!(derived, PathBuf::from("/tmp/profile/.claude.json"));
|
||
}
|
||
|
||
#[test]
|
||
fn derive_mcp_path_from_override_handles_non_hidden_folder() {
|
||
let override_dir = PathBuf::from("/data/claude-config");
|
||
let derived = derive_mcp_path_from_override(&override_dir)
|
||
.expect("should derive path for standard dir");
|
||
assert_eq!(derived, PathBuf::from("/data/claude-config.json"));
|
||
}
|
||
|
||
#[test]
|
||
fn derive_mcp_path_from_override_supports_relative_rootless_dir() {
|
||
let override_dir = PathBuf::from("claude");
|
||
let derived = derive_mcp_path_from_override(&override_dir)
|
||
.expect("should derive path for single segment");
|
||
assert_eq!(derived, PathBuf::from("claude.json"));
|
||
}
|
||
|
||
#[test]
|
||
fn derive_mcp_path_from_root_like_dir_returns_none() {
|
||
let override_dir = PathBuf::from("/");
|
||
assert!(derive_mcp_path_from_override(&override_dir).is_none());
|
||
}
|
||
}
|
||
|
||
/// 复制文件
|
||
pub fn copy_file(from: &Path, to: &Path) -> Result<(), AppError> {
|
||
fs::copy(from, to).map_err(|e| AppError::IoContext {
|
||
context: format!("复制文件失败 ({} -> {})", from.display(), to.display()),
|
||
source: e,
|
||
})?;
|
||
Ok(())
|
||
}
|
||
|
||
/// 删除文件
|
||
pub fn delete_file(path: &Path) -> Result<(), AppError> {
|
||
if path.exists() {
|
||
fs::remove_file(path).map_err(|e| AppError::io(path, e))?;
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// 检查 Claude Code 配置状态
|
||
#[derive(Serialize, Deserialize)]
|
||
pub struct ConfigStatus {
|
||
pub exists: bool,
|
||
pub path: String,
|
||
}
|
||
|
||
/// 获取 Claude Code 配置状态
|
||
pub fn get_claude_config_status() -> ConfigStatus {
|
||
let path = get_claude_settings_path();
|
||
ConfigStatus {
|
||
exists: path.exists(),
|
||
path: path.to_string_lossy().to_string(),
|
||
}
|
||
}
|