mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-27 16:26:16 +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
231 lines
8.2 KiB
Rust
231 lines
8.2 KiB
Rust
use indexmap::IndexMap;
|
|
|
|
use crate::app_config::AppType;
|
|
use crate::config::write_text_file;
|
|
use crate::error::AppError;
|
|
use crate::prompt::Prompt;
|
|
use crate::prompt_files::prompt_file_path;
|
|
use crate::store::AppState;
|
|
|
|
/// 安全地获取当前 Unix 时间戳
|
|
fn get_unix_timestamp() -> Result<i64, AppError> {
|
|
std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_secs() as i64)
|
|
.map_err(|e| AppError::Message(format!("Failed to get system time: {e}")))
|
|
}
|
|
|
|
pub struct PromptService;
|
|
|
|
impl PromptService {
|
|
pub fn get_prompts(
|
|
state: &AppState,
|
|
app: AppType,
|
|
) -> Result<IndexMap<String, Prompt>, AppError> {
|
|
state.db.get_prompts(app.as_str())
|
|
}
|
|
|
|
pub fn upsert_prompt(
|
|
state: &AppState,
|
|
app: AppType,
|
|
_id: &str,
|
|
prompt: Prompt,
|
|
) -> Result<(), AppError> {
|
|
// 检查是否为已启用的提示词
|
|
let is_enabled = prompt.enabled;
|
|
|
|
state.db.save_prompt(app.as_str(), &prompt)?;
|
|
|
|
// 如果是已启用的提示词,同步更新到对应的文件
|
|
if is_enabled {
|
|
let target_path = prompt_file_path(&app)?;
|
|
write_text_file(&target_path, &prompt.content)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn delete_prompt(state: &AppState, app: AppType, id: &str) -> Result<(), AppError> {
|
|
let prompts = state.db.get_prompts(app.as_str())?;
|
|
|
|
if let Some(prompt) = prompts.get(id) {
|
|
if prompt.enabled {
|
|
return Err(AppError::InvalidInput("无法删除已启用的提示词".to_string()));
|
|
}
|
|
}
|
|
|
|
state.db.delete_prompt(app.as_str(), id)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn enable_prompt(state: &AppState, app: AppType, id: &str) -> Result<(), AppError> {
|
|
// 回填当前 live 文件内容到已启用的提示词,或创建备份
|
|
let target_path = prompt_file_path(&app)?;
|
|
if target_path.exists() {
|
|
if let Ok(live_content) = std::fs::read_to_string(&target_path) {
|
|
if !live_content.trim().is_empty() {
|
|
let mut prompts = state.db.get_prompts(app.as_str())?;
|
|
|
|
// 尝试回填到当前已启用的提示词
|
|
if let Some((enabled_id, enabled_prompt)) = prompts
|
|
.iter_mut()
|
|
.find(|(_, p)| p.enabled)
|
|
.map(|(id, p)| (id.clone(), p))
|
|
{
|
|
let timestamp = get_unix_timestamp()?;
|
|
enabled_prompt.content = live_content.clone();
|
|
enabled_prompt.updated_at = Some(timestamp);
|
|
log::info!("回填 live 提示词内容到已启用项: {enabled_id}");
|
|
state.db.save_prompt(app.as_str(), enabled_prompt)?;
|
|
} else {
|
|
// 没有已启用的提示词,则创建一次备份(避免重复备份)
|
|
let content_exists = prompts
|
|
.values()
|
|
.any(|p| p.content.trim() == live_content.trim());
|
|
if !content_exists {
|
|
let timestamp = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_secs() as i64;
|
|
let backup_id = format!("backup-{timestamp}");
|
|
let backup_prompt = Prompt {
|
|
id: backup_id.clone(),
|
|
name: format!(
|
|
"原始提示词 {}",
|
|
chrono::Local::now().format("%Y-%m-%d %H:%M")
|
|
),
|
|
content: live_content,
|
|
description: Some("自动备份的原始提示词".to_string()),
|
|
enabled: false,
|
|
created_at: Some(timestamp),
|
|
updated_at: Some(timestamp),
|
|
};
|
|
log::info!("回填 live 提示词内容,创建备份: {backup_id}");
|
|
state.db.save_prompt(app.as_str(), &backup_prompt)?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 启用目标提示词并写入文件
|
|
let mut prompts = state.db.get_prompts(app.as_str())?;
|
|
|
|
for prompt in prompts.values_mut() {
|
|
prompt.enabled = false;
|
|
}
|
|
|
|
if let Some(prompt) = prompts.get_mut(id) {
|
|
prompt.enabled = true;
|
|
write_text_file(&target_path, &prompt.content)?; // 原子写入
|
|
state.db.save_prompt(app.as_str(), prompt)?;
|
|
} else {
|
|
return Err(AppError::InvalidInput(format!("提示词 {id} 不存在")));
|
|
}
|
|
|
|
// Save all prompts to disable others
|
|
for (_, prompt) in prompts.iter() {
|
|
state.db.save_prompt(app.as_str(), prompt)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn import_from_file(state: &AppState, app: AppType) -> Result<String, AppError> {
|
|
let file_path = prompt_file_path(&app)?;
|
|
|
|
if !file_path.exists() {
|
|
return Err(AppError::Message("提示词文件不存在".to_string()));
|
|
}
|
|
|
|
let content =
|
|
std::fs::read_to_string(&file_path).map_err(|e| AppError::io(&file_path, e))?;
|
|
let timestamp = get_unix_timestamp()?;
|
|
|
|
let id = format!("imported-{timestamp}");
|
|
let prompt = Prompt {
|
|
id: id.clone(),
|
|
name: format!(
|
|
"导入的提示词 {}",
|
|
chrono::Local::now().format("%Y-%m-%d %H:%M")
|
|
),
|
|
content,
|
|
description: Some("从现有配置文件导入".to_string()),
|
|
enabled: false,
|
|
created_at: Some(timestamp),
|
|
updated_at: Some(timestamp),
|
|
};
|
|
|
|
Self::upsert_prompt(state, app, &id, prompt)?;
|
|
Ok(id)
|
|
}
|
|
|
|
pub fn get_current_file_content(app: AppType) -> Result<Option<String>, AppError> {
|
|
let file_path = prompt_file_path(&app)?;
|
|
if !file_path.exists() {
|
|
return Ok(None);
|
|
}
|
|
let content =
|
|
std::fs::read_to_string(&file_path).map_err(|e| AppError::io(&file_path, e))?;
|
|
Ok(Some(content))
|
|
}
|
|
|
|
/// 首次启动时从现有提示词文件自动导入(如果存在)
|
|
/// 返回导入的数量
|
|
pub fn import_from_file_on_first_launch(
|
|
state: &AppState,
|
|
app: AppType,
|
|
) -> Result<usize, AppError> {
|
|
// 幂等性保护:该应用已有提示词则跳过
|
|
let existing = state.db.get_prompts(app.as_str())?;
|
|
if !existing.is_empty() {
|
|
return Ok(0);
|
|
}
|
|
|
|
let file_path = prompt_file_path(&app)?;
|
|
|
|
// 检查文件是否存在
|
|
if !file_path.exists() {
|
|
return Ok(0);
|
|
}
|
|
|
|
// 读取文件内容
|
|
let content = match std::fs::read_to_string(&file_path) {
|
|
Ok(c) => c,
|
|
Err(e) => {
|
|
log::warn!("读取提示词文件失败: {file_path:?}, 错误: {e}");
|
|
return Ok(0);
|
|
}
|
|
};
|
|
|
|
// 检查内容是否为空
|
|
if content.trim().is_empty() {
|
|
return Ok(0);
|
|
}
|
|
|
|
log::info!("发现提示词文件,自动导入: {file_path:?}");
|
|
|
|
// 创建提示词对象
|
|
let timestamp = get_unix_timestamp()?;
|
|
let id = format!("auto-imported-{timestamp}");
|
|
let prompt = Prompt {
|
|
id: id.clone(),
|
|
name: format!(
|
|
"Auto-imported Prompt {}",
|
|
chrono::Local::now().format("%Y-%m-%d %H:%M")
|
|
),
|
|
content,
|
|
description: Some("Automatically imported on first launch".to_string()),
|
|
enabled: true, // 首次导入时自动启用
|
|
created_at: Some(timestamp),
|
|
updated_at: Some(timestamp),
|
|
};
|
|
|
|
// 保存到数据库
|
|
state.db.save_prompt(app.as_str(), &prompt)?;
|
|
|
|
log::info!("自动导入完成: {}", app.as_str());
|
|
Ok(1)
|
|
}
|
|
}
|