mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-04-26 20:39:04 +08:00
Compare commits
2 Commits
fix/proxy-
...
fix/usage-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8919152b73 | ||
|
|
2651b65b10 |
@@ -75,8 +75,7 @@ mod tests {
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_macos_app_bundle_path_valid() {
|
fn test_get_macos_app_bundle_path_valid() {
|
||||||
let exe_path =
|
let exe_path = std::path::Path::new("/Applications/CC Switch.app/Contents/MacOS/CC Switch");
|
||||||
std::path::Path::new("/Applications/CC Switch.app/Contents/MacOS/CC Switch");
|
|
||||||
let result = get_macos_app_bundle_path(exe_path);
|
let result = get_macos_app_bundle_path(exe_path);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result,
|
result,
|
||||||
@@ -92,7 +91,9 @@ mod tests {
|
|||||||
let result = get_macos_app_bundle_path(exe_path);
|
let result = get_macos_app_bundle_path(exe_path);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result,
|
result,
|
||||||
Some(std::path::PathBuf::from("/Users/test/My Apps/CC Switch.app"))
|
Some(std::path::PathBuf::from(
|
||||||
|
"/Users/test/My Apps/CC Switch.app"
|
||||||
|
))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ const UNIVERSAL_PROVIDERS_KEY: &str = "universal_providers";
|
|||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
/// 获取所有统一供应商
|
/// 获取所有统一供应商
|
||||||
pub fn get_all_universal_providers(&self) -> Result<HashMap<String, UniversalProvider>, AppError> {
|
pub fn get_all_universal_providers(
|
||||||
|
&self,
|
||||||
|
) -> Result<HashMap<String, UniversalProvider>, AppError> {
|
||||||
let conn = lock_conn!(self.conn);
|
let conn = lock_conn!(self.conn);
|
||||||
|
|
||||||
let mut stmt = conn
|
let mut stmt = conn
|
||||||
@@ -24,10 +26,8 @@ impl Database {
|
|||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Some(json) => {
|
Some(json) => serde_json::from_str(&json)
|
||||||
serde_json::from_str(&json)
|
.map_err(|e| AppError::Database(format!("解析统一供应商数据失败: {e}"))),
|
||||||
.map_err(|e| AppError::Database(format!("解析统一供应商数据失败: {e}")))
|
|
||||||
}
|
|
||||||
None => Ok(HashMap::new()),
|
None => Ok(HashMap::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,4 +72,3 @@ impl Database {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -233,6 +233,24 @@ impl Database {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 尝试添加基础配置列到 proxy_config 表(兼容 v3.9.0-2 升级)
|
||||||
|
let _ = conn.execute(
|
||||||
|
"ALTER TABLE proxy_config ADD COLUMN proxy_enabled INTEGER NOT NULL DEFAULT 0",
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
let _ = conn.execute(
|
||||||
|
"ALTER TABLE proxy_config ADD COLUMN listen_address TEXT NOT NULL DEFAULT '127.0.0.1'",
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
let _ = conn.execute(
|
||||||
|
"ALTER TABLE proxy_config ADD COLUMN listen_port INTEGER NOT NULL DEFAULT 5000",
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
let _ = conn.execute(
|
||||||
|
"ALTER TABLE proxy_config ADD COLUMN enable_logging INTEGER NOT NULL DEFAULT 1",
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
// 尝试添加超时配置列到 proxy_config 表
|
// 尝试添加超时配置列到 proxy_config 表
|
||||||
let _ = conn.execute(
|
let _ = conn.execute(
|
||||||
"ALTER TABLE proxy_config ADD COLUMN streaming_first_byte_timeout INTEGER NOT NULL DEFAULT 30",
|
"ALTER TABLE proxy_config ADD COLUMN streaming_first_byte_timeout INTEGER NOT NULL DEFAULT 30",
|
||||||
@@ -247,6 +265,14 @@ impl Database {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 兼容:若旧版 proxy_config 仍为单例结构(无 app_type),则在启动时直接转换为三行结构
|
||||||
|
// 说明:user_version=2 时不会再触发 v1->v2 迁移,但新代码查询依赖 app_type 列。
|
||||||
|
if Self::table_exists(conn, "proxy_config")?
|
||||||
|
&& !Self::has_column(conn, "proxy_config", "app_type")?
|
||||||
|
{
|
||||||
|
Self::migrate_proxy_config_to_per_app(conn)?;
|
||||||
|
}
|
||||||
|
|
||||||
// 确保 in_failover_queue 列存在(对于已存在的 v2 数据库)
|
// 确保 in_failover_queue 列存在(对于已存在的 v2 数据库)
|
||||||
Self::add_column_if_missing(
|
Self::add_column_if_missing(
|
||||||
conn,
|
conn,
|
||||||
@@ -411,6 +437,32 @@ impl Database {
|
|||||||
|
|
||||||
// 添加代理超时配置字段
|
// 添加代理超时配置字段
|
||||||
if Self::table_exists(conn, "proxy_config")? {
|
if Self::table_exists(conn, "proxy_config")? {
|
||||||
|
// 兼容旧版本缺失的基础字段
|
||||||
|
Self::add_column_if_missing(
|
||||||
|
conn,
|
||||||
|
"proxy_config",
|
||||||
|
"proxy_enabled",
|
||||||
|
"INTEGER NOT NULL DEFAULT 0",
|
||||||
|
)?;
|
||||||
|
Self::add_column_if_missing(
|
||||||
|
conn,
|
||||||
|
"proxy_config",
|
||||||
|
"listen_address",
|
||||||
|
"TEXT NOT NULL DEFAULT '127.0.0.1'",
|
||||||
|
)?;
|
||||||
|
Self::add_column_if_missing(
|
||||||
|
conn,
|
||||||
|
"proxy_config",
|
||||||
|
"listen_port",
|
||||||
|
"INTEGER NOT NULL DEFAULT 5000",
|
||||||
|
)?;
|
||||||
|
Self::add_column_if_missing(
|
||||||
|
conn,
|
||||||
|
"proxy_config",
|
||||||
|
"enable_logging",
|
||||||
|
"INTEGER NOT NULL DEFAULT 1",
|
||||||
|
)?;
|
||||||
|
|
||||||
Self::add_column_if_missing(
|
Self::add_column_if_missing(
|
||||||
conn,
|
conn,
|
||||||
"proxy_config",
|
"proxy_config",
|
||||||
|
|||||||
@@ -201,6 +201,53 @@ fn migration_aligns_column_defaults_and_types() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_tables_repairs_legacy_proxy_config_singleton_to_per_app() {
|
||||||
|
let conn = Connection::open_in_memory().expect("open memory db");
|
||||||
|
|
||||||
|
// 模拟测试版 v2:user_version=2,但 proxy_config 仍是单例结构(无 app_type)
|
||||||
|
Database::set_user_version(&conn, 2).expect("set user_version");
|
||||||
|
conn.execute_batch(
|
||||||
|
r#"
|
||||||
|
CREATE TABLE proxy_config (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
enabled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
listen_address TEXT NOT NULL DEFAULT '127.0.0.1',
|
||||||
|
listen_port INTEGER NOT NULL DEFAULT 5000,
|
||||||
|
max_retries INTEGER NOT NULL DEFAULT 3,
|
||||||
|
request_timeout INTEGER NOT NULL DEFAULT 300,
|
||||||
|
enable_logging INTEGER NOT NULL DEFAULT 1,
|
||||||
|
target_app TEXT NOT NULL DEFAULT 'claude',
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
INSERT INTO proxy_config (id, enabled) VALUES (1, 1);
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.expect("seed legacy proxy_config");
|
||||||
|
|
||||||
|
Database::create_tables_on_conn(&conn).expect("create tables should repair proxy_config");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
Database::has_column(&conn, "proxy_config", "app_type").expect("check app_type"),
|
||||||
|
"proxy_config should be migrated to per-app structure"
|
||||||
|
);
|
||||||
|
|
||||||
|
let count: i32 = conn
|
||||||
|
.query_row("SELECT COUNT(*) FROM proxy_config", [], |r| r.get(0))
|
||||||
|
.expect("count rows");
|
||||||
|
assert_eq!(count, 3, "per-app proxy_config should have 3 rows");
|
||||||
|
|
||||||
|
// 新结构下应能按 app_type 查询
|
||||||
|
let _: i32 = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT COUNT(*) FROM proxy_config WHERE app_type = 'claude'",
|
||||||
|
[],
|
||||||
|
|r| r.get(0),
|
||||||
|
)
|
||||||
|
.expect("query by app_type");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dry_run_does_not_write_to_disk() {
|
fn dry_run_does_not_write_to_disk() {
|
||||||
// Create minimal valid config for migration
|
// Create minimal valid config for migration
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
use crate::database::{lock_conn, Database};
|
use crate::database::{lock_conn, Database};
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Local, TimeZone};
|
||||||
use rusqlite::{params, Connection, OptionalExtension};
|
use rusqlite::{params, Connection, OptionalExtension};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@@ -186,8 +186,17 @@ impl Database {
|
|||||||
let conn = lock_conn!(self.conn);
|
let conn = lock_conn!(self.conn);
|
||||||
|
|
||||||
if days <= 1 {
|
if days <= 1 {
|
||||||
|
let today = Local::now().date_naive();
|
||||||
|
let start_of_today = today.and_hms_opt(0, 0, 0).unwrap();
|
||||||
|
// 使用 earliest() 处理 DST 切换时的歧义时间,fallback 到当前时间减一天
|
||||||
|
let start_ts = Local
|
||||||
|
.from_local_datetime(&start_of_today)
|
||||||
|
.earliest()
|
||||||
|
.unwrap_or_else(|| Local::now() - Duration::days(1))
|
||||||
|
.timestamp();
|
||||||
|
|
||||||
let sql = "SELECT
|
let sql = "SELECT
|
||||||
strftime('%Y-%m-%dT%H:00:00Z', datetime(created_at, 'unixepoch')) as bucket,
|
strftime('%Y-%m-%dT%H:00:00', datetime(created_at, 'unixepoch', 'localtime')) as bucket,
|
||||||
COUNT(*) as request_count,
|
COUNT(*) as request_count,
|
||||||
COALESCE(SUM(CAST(total_cost_usd AS REAL)), 0) as total_cost,
|
COALESCE(SUM(CAST(total_cost_usd AS REAL)), 0) as total_cost,
|
||||||
COALESCE(SUM(input_tokens + output_tokens), 0) as total_tokens,
|
COALESCE(SUM(input_tokens + output_tokens), 0) as total_tokens,
|
||||||
@@ -196,12 +205,12 @@ impl Database {
|
|||||||
COALESCE(SUM(cache_creation_tokens), 0) as total_cache_creation_tokens,
|
COALESCE(SUM(cache_creation_tokens), 0) as total_cache_creation_tokens,
|
||||||
COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens
|
COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens
|
||||||
FROM proxy_request_logs
|
FROM proxy_request_logs
|
||||||
WHERE created_at >= strftime('%s', 'now', '-1 day')
|
WHERE created_at >= ?
|
||||||
GROUP BY bucket
|
GROUP BY bucket
|
||||||
ORDER BY bucket ASC";
|
ORDER BY bucket ASC";
|
||||||
|
|
||||||
let mut stmt = conn.prepare(sql)?;
|
let mut stmt = conn.prepare(sql)?;
|
||||||
let rows = stmt.query_map([], |row| {
|
let rows = stmt.query_map([start_ts], |row| {
|
||||||
Ok(DailyStats {
|
Ok(DailyStats {
|
||||||
date: row.get(0)?,
|
date: row.get(0)?,
|
||||||
request_count: row.get::<_, i64>(1)? as u64,
|
request_count: row.get::<_, i64>(1)? as u64,
|
||||||
@@ -221,12 +230,11 @@ impl Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut stats = Vec::new();
|
let mut stats = Vec::new();
|
||||||
let today = Utc::now().date_naive();
|
|
||||||
for hour in 0..24 {
|
for hour in 0..24 {
|
||||||
let bucket = today
|
let bucket = today
|
||||||
.and_hms_opt(hour, 0, 0)
|
.and_hms_opt(hour, 0, 0)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.format("%Y-%m-%dT%H:00:00Z")
|
.format("%Y-%m-%dT%H:00:00")
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
if let Some(stat) = buckets.remove(&bucket) {
|
if let Some(stat) = buckets.remove(&bucket) {
|
||||||
@@ -246,8 +254,19 @@ impl Database {
|
|||||||
}
|
}
|
||||||
Ok(stats)
|
Ok(stats)
|
||||||
} else {
|
} else {
|
||||||
|
let today = Local::now().date_naive();
|
||||||
|
let start_day =
|
||||||
|
today - Duration::days((days.saturating_sub(1)) as i64);
|
||||||
|
let start_of_window = start_day.and_hms_opt(0, 0, 0).unwrap();
|
||||||
|
// 使用 earliest() 处理 DST 切换时的歧义时间,fallback 到当前时间减 days 天
|
||||||
|
let start_ts = Local
|
||||||
|
.from_local_datetime(&start_of_window)
|
||||||
|
.earliest()
|
||||||
|
.unwrap_or_else(|| Local::now() - Duration::days(days as i64))
|
||||||
|
.timestamp();
|
||||||
|
|
||||||
let sql = "SELECT
|
let sql = "SELECT
|
||||||
date(created_at, 'unixepoch') as bucket,
|
strftime('%Y-%m-%dT00:00:00', datetime(created_at, 'unixepoch', 'localtime')) as bucket,
|
||||||
COUNT(*) as request_count,
|
COUNT(*) as request_count,
|
||||||
COALESCE(SUM(CAST(total_cost_usd AS REAL)), 0) as total_cost,
|
COALESCE(SUM(CAST(total_cost_usd AS REAL)), 0) as total_cost,
|
||||||
COALESCE(SUM(input_tokens + output_tokens), 0) as total_tokens,
|
COALESCE(SUM(input_tokens + output_tokens), 0) as total_tokens,
|
||||||
@@ -256,12 +275,12 @@ impl Database {
|
|||||||
COALESCE(SUM(cache_creation_tokens), 0) as total_cache_creation_tokens,
|
COALESCE(SUM(cache_creation_tokens), 0) as total_cache_creation_tokens,
|
||||||
COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens
|
COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens
|
||||||
FROM proxy_request_logs
|
FROM proxy_request_logs
|
||||||
WHERE created_at >= strftime('%s', 'now', ?)
|
WHERE created_at >= ?
|
||||||
GROUP BY bucket
|
GROUP BY bucket
|
||||||
ORDER BY bucket ASC";
|
ORDER BY bucket ASC";
|
||||||
|
|
||||||
let mut stmt = conn.prepare(sql)?;
|
let mut stmt = conn.prepare(sql)?;
|
||||||
let rows = stmt.query_map([format!("-{days} days")], |row| {
|
let rows = stmt.query_map([start_ts], |row| {
|
||||||
Ok(DailyStats {
|
Ok(DailyStats {
|
||||||
date: row.get(0)?,
|
date: row.get(0)?,
|
||||||
request_count: row.get::<_, i64>(1)? as u64,
|
request_count: row.get::<_, i64>(1)? as u64,
|
||||||
@@ -281,12 +300,10 @@ impl Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut stats = Vec::new();
|
let mut stats = Vec::new();
|
||||||
let start_day =
|
|
||||||
Utc::now().date_naive() - Duration::days((days.saturating_sub(1)) as i64);
|
|
||||||
|
|
||||||
for i in 0..days {
|
for i in 0..days {
|
||||||
let day = start_day + Duration::days(i as i64);
|
let day = start_day + Duration::days(i as i64);
|
||||||
let key = day.format("%Y-%m-%d").to_string();
|
let key = day.format("%Y-%m-%dT00:00:00").to_string();
|
||||||
if let Some(stat) = map.remove(&key) {
|
if let Some(stat) = map.remove(&key) {
|
||||||
stats.push(stat);
|
stats.push(stat);
|
||||||
} else {
|
} else {
|
||||||
@@ -617,7 +634,7 @@ impl Database {
|
|||||||
"SELECT COALESCE(SUM(CAST(total_cost_usd AS REAL)), 0)
|
"SELECT COALESCE(SUM(CAST(total_cost_usd AS REAL)), 0)
|
||||||
FROM proxy_request_logs
|
FROM proxy_request_logs
|
||||||
WHERE provider_id = ? AND app_type = ?
|
WHERE provider_id = ? AND app_type = ?
|
||||||
AND date(created_at, 'unixepoch') = date('now')",
|
AND date(datetime(created_at, 'unixepoch', 'localtime')) = date('now', 'localtime')",
|
||||||
params![provider_id, app_type],
|
params![provider_id, app_type],
|
||||||
|row| row.get(0),
|
|row| row.get(0),
|
||||||
)
|
)
|
||||||
@@ -629,7 +646,7 @@ impl Database {
|
|||||||
"SELECT COALESCE(SUM(CAST(total_cost_usd AS REAL)), 0)
|
"SELECT COALESCE(SUM(CAST(total_cost_usd AS REAL)), 0)
|
||||||
FROM proxy_request_logs
|
FROM proxy_request_logs
|
||||||
WHERE provider_id = ? AND app_type = ?
|
WHERE provider_id = ? AND app_type = ?
|
||||||
AND strftime('%Y-%m', created_at, 'unixepoch') = strftime('%Y-%m', 'now')",
|
AND strftime('%Y-%m', datetime(created_at, 'unixepoch', 'localtime')) = strftime('%Y-%m', 'now', 'localtime')",
|
||||||
params![provider_id, app_type],
|
params![provider_id, app_type],
|
||||||
|row| row.get(0),
|
|row| row.get(0),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user