mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-06-16 13:34:04 +08:00
e7badb1a24
* refactor(ui): simplify UpdateBadge to minimal dot indicator * feat(provider): add individual test and proxy config for providers Add support for provider-specific model test and proxy configurations: - Add ProviderTestConfig and ProviderProxyConfig types in Rust and TypeScript - Create ProviderAdvancedConfig component with collapsible panels - Update stream_check service to merge provider config with global config - Proxy config UI follows global proxy style (single URL input) Provider-level configs stored in meta field, no database schema changes needed. * feat(ui): add failover toggle and improve proxy controls - Add FailoverToggle component with slide animation - Simplify ProxyToggle style to match FailoverToggle - Add usage statistics button when proxy is active - Fix i18n parameter passing for failover messages - Add missing failover translation keys (inQueue, addQueue, priority) - Replace AboutSection icon with app logo * fix(proxy): support system proxy fallback and provider-level proxy config - Remove no_proxy() calls in http_client.rs to allow system proxy fallback - Add get_for_provider() to build HTTP client with provider-specific proxy - Update forwarder.rs and stream_check.rs to use provider proxy config - Fix EditProviderDialog.tsx to include provider.meta in useMemo deps - Add useEffect in ProviderAdvancedConfig.tsx to sync expand state Fixes #636 Fixes #583 * fix(ui): sync toast theme with app setting * feat(settings): add log config management Fixes #612 Fixes #514 * fix(proxy): increase request body size limit to 200MB Fixes #666 * docs(proxy): update timeout config descriptions and defaults Fixes #612 * fix(proxy): filter x-goog-api-key header to prevent duplication * fix(proxy): prevent proxy recursion when system proxy points to localhost Detect if HTTP_PROXY, HTTPS_PROXY, or ALL_PROXY environment variables point to loopback addresses (localhost, 127.0.0.1), and bypass system proxy in such cases to avoid infinite request loops. * fix(i18n): add providerAdvanced i18n keys and fix failover toast parameter - Add providerAdvanced.* i18n keys to en.json, zh.json, and ja.json - Fix failover toggleFailed toast to pass detail parameter - Remove Chinese fallback text from UI for English/Japanese users * fix(tray): restore tray-provider events and enable Auto failover properly - Emit provider-switched event on tray provider click (backward compatibility) - Auto button now: starts proxy, takes over live config, enables failover * fix(log): enable dynamic log level and single file mode - Initialize log at Trace level for dynamic adjustment - Change rotation strategy to KeepSome(1) for single file - Set max file size to 1GB - Delete old log file on startup for clean start * fix(tray): fix clippy uninlined format args warning Use inline format arguments: {app_type_str} instead of {} * fix(provider): allow typing :// in endpoint URL inputs Change input type from "url" to "text" to prevent browser URL validation from blocking :// input. Closes #681 * fix(stream-check): use Gemini native streaming API format - Change endpoint from OpenAI-compatible to native streamGenerateContent - Add alt=sse parameter for SSE format response - Use x-goog-api-key header instead of Bearer token - Convert request body to Gemini contents/parts format * feat(proxy): add request logging for debugging Add debug logs for outgoing requests including URL and body content with byte size, matching the existing response logging format. * fix(log): prevent usize underflow in KeepSome rotation strategy KeepSome(n) internally computes n-2, so n=1 causes underflow. Use KeepSome(2) as the minimum safe value.
206 lines
5.8 KiB
Rust
206 lines
5.8 KiB
Rust
//! Panic Hook 模块
|
|
//!
|
|
//! 在应用崩溃时捕获 panic 信息并记录到 `<app_config_dir>/crash.log` 文件中(默认 `~/.cc-switch/crash.log`)。
|
|
//! 便于用户和开发者诊断闪退问题。
|
|
|
|
use std::fs::OpenOptions;
|
|
use std::io::Write;
|
|
use std::panic;
|
|
use std::path::PathBuf;
|
|
use std::sync::OnceLock;
|
|
|
|
/// 应用版本号(从 Cargo.toml 读取)
|
|
const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
|
|
static APP_CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
|
|
|
|
pub fn init_app_config_dir(dir: PathBuf) {
|
|
let _ = APP_CONFIG_DIR.set(dir);
|
|
}
|
|
|
|
/// 获取默认应用配置目录(不会 panic)
|
|
fn default_app_config_dir() -> PathBuf {
|
|
dirs::home_dir()
|
|
.unwrap_or_else(|| PathBuf::from("."))
|
|
.join(".cc-switch")
|
|
}
|
|
|
|
/// 获取应用配置目录(优先使用初始化时写入的值;不会 panic)
|
|
fn get_app_config_dir() -> PathBuf {
|
|
APP_CONFIG_DIR
|
|
.get()
|
|
.cloned()
|
|
.unwrap_or_else(default_app_config_dir)
|
|
}
|
|
|
|
/// 获取崩溃日志文件路径
|
|
fn get_crash_log_path() -> PathBuf {
|
|
get_app_config_dir().join("crash.log")
|
|
}
|
|
|
|
/// 获取日志目录路径
|
|
pub fn get_log_dir() -> PathBuf {
|
|
get_app_config_dir().join("logs")
|
|
}
|
|
|
|
/// 安全获取环境信息(不会 panic)
|
|
fn get_system_info() -> String {
|
|
let os = std::env::consts::OS;
|
|
let arch = std::env::consts::ARCH;
|
|
let family = std::env::consts::FAMILY;
|
|
|
|
// 安全获取当前工作目录
|
|
let cwd = std::env::current_dir()
|
|
.map(|p| p.display().to_string())
|
|
.unwrap_or_else(|_| "unknown".to_string());
|
|
|
|
// 安全获取当前线程信息
|
|
let thread = std::thread::current();
|
|
let thread_name = thread.name().unwrap_or("unnamed");
|
|
let thread_id = format!("{:?}", thread.id());
|
|
|
|
format!(
|
|
"OS: {os} ({family})\n\
|
|
Arch: {arch}\n\
|
|
App Version: {APP_VERSION}\n\
|
|
Working Dir: {cwd}\n\
|
|
Thread: {thread_name} (ID: {thread_id})"
|
|
)
|
|
}
|
|
|
|
/// 设置 panic hook,捕获崩溃信息并写入日志文件
|
|
///
|
|
/// 在应用启动时调用此函数,确保任何 panic 都会被记录。
|
|
/// 日志格式包含:
|
|
/// - 时间戳
|
|
/// - 应用版本和系统信息
|
|
/// - Panic 信息
|
|
/// - 发生位置(文件:行号)
|
|
/// - Backtrace(完整调用栈)
|
|
pub fn setup_panic_hook() {
|
|
// 启用 backtrace(确保 release 模式也能捕获)
|
|
if std::env::var("RUST_BACKTRACE").is_err() {
|
|
std::env::set_var("RUST_BACKTRACE", "1");
|
|
}
|
|
|
|
let default_hook = panic::take_hook();
|
|
|
|
panic::set_hook(Box::new(move |panic_info| {
|
|
let log_path = get_crash_log_path();
|
|
|
|
// 确保目录存在
|
|
if let Some(parent) = log_path.parent() {
|
|
let _ = std::fs::create_dir_all(parent);
|
|
}
|
|
|
|
// 构建崩溃信息(使用 catch_unwind 保护时间格式化,避免嵌套 panic)
|
|
let timestamp = std::panic::catch_unwind(|| {
|
|
chrono::Local::now()
|
|
.format("%Y-%m-%d %H:%M:%S%.3f")
|
|
.to_string()
|
|
})
|
|
.unwrap_or_else(|_| {
|
|
// chrono panic 时回退到 unix timestamp
|
|
std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| format!("unix:{}.{:03}", d.as_secs(), d.subsec_millis()))
|
|
.unwrap_or_else(|_| "unknown".to_string())
|
|
});
|
|
|
|
// 获取系统信息
|
|
let system_info = std::panic::catch_unwind(get_system_info)
|
|
.unwrap_or_else(|_| "Failed to get system info".to_string());
|
|
|
|
// 获取 panic 消息(尝试多种方式提取)
|
|
let message = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
|
|
s.to_string()
|
|
} else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
|
|
s.clone()
|
|
} else {
|
|
// 尝试使用 Display trait
|
|
format!("{panic_info}")
|
|
};
|
|
|
|
// 获取位置信息
|
|
let location = if let Some(loc) = panic_info.location() {
|
|
format!(
|
|
"File: {}\n Line: {}\n Column: {}",
|
|
loc.file(),
|
|
loc.line(),
|
|
loc.column()
|
|
)
|
|
} else {
|
|
"Unknown location".to_string()
|
|
};
|
|
|
|
// 捕获 backtrace(完整调用栈)
|
|
let backtrace = std::backtrace::Backtrace::force_capture();
|
|
let backtrace_str = format!("{backtrace}");
|
|
|
|
// 格式化日志条目
|
|
let separator = "=".repeat(80);
|
|
let sub_separator = "-".repeat(40);
|
|
let crash_entry = format!(
|
|
r#"
|
|
{separator}
|
|
[CRASH REPORT] {timestamp}
|
|
{separator}
|
|
|
|
{sub_separator}
|
|
System Information
|
|
{sub_separator}
|
|
{system_info}
|
|
|
|
{sub_separator}
|
|
Error Details
|
|
{sub_separator}
|
|
Message: {message}
|
|
|
|
Location: {location}
|
|
|
|
{sub_separator}
|
|
Stack Trace (Backtrace)
|
|
{sub_separator}
|
|
{backtrace_str}
|
|
|
|
{separator}
|
|
"#
|
|
);
|
|
|
|
// 写入文件(追加模式)
|
|
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&log_path) {
|
|
let _ = file.write_all(crash_entry.as_bytes());
|
|
let _ = file.flush();
|
|
|
|
// 记录日志文件位置到 stderr
|
|
eprintln!("\n[CC-Switch] Crash log saved to: {}", log_path.display());
|
|
}
|
|
|
|
// 同时输出到 stderr(便于开发调试)
|
|
eprintln!("{crash_entry}");
|
|
|
|
// 调用默认 hook
|
|
default_hook(panic_info);
|
|
}));
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_crash_log_path() {
|
|
let path = get_crash_log_path();
|
|
assert!(path.ends_with("crash.log"));
|
|
assert!(path.to_string_lossy().contains(".cc-switch"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_system_info() {
|
|
let info = get_system_info();
|
|
assert!(info.contains("OS:"));
|
|
assert!(info.contains("Arch:"));
|
|
assert!(info.contains("App Version:"));
|
|
}
|
|
}
|