mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
edf28b6422
- Normalize OpenAI/Gemini input_tokens semantics in SQL via the new fresh_input_sql helper (cache_read subtracted at query time, no data migration). Recovers correct cache hit rates for Codex/Gemini. - Add get_usage_summary_by_app endpoint for per-app split (single UNION ALL + GROUP BY, avoids N+1). - Replace UsageSummaryCards + AppBreakdownRail with a single filter-driven UsageHero card; clicking a filter button now truly changes the displayed numbers and the title accent color. - Tighten KNOWN_APP_TYPES to the 3 app_types whose token data is reliably collected (claude/codex/gemini); hide claude-desktop, hermes, opencode, openclaw filter buttons and i18n keys. - Flag cache_creation as N/A for OpenAI-style protocols (Codex, Gemini); show a "partial" tooltip when the All view mixes both protocol families.
255 lines
7.3 KiB
Rust
255 lines
7.3 KiB
Rust
//! 使用统计相关命令
|
|
|
|
use crate::error::AppError;
|
|
use crate::services::usage_stats::*;
|
|
use crate::store::AppState;
|
|
use tauri::State;
|
|
|
|
/// 获取使用量汇总
|
|
#[tauri::command]
|
|
pub fn get_usage_summary(
|
|
state: State<'_, AppState>,
|
|
start_date: Option<i64>,
|
|
end_date: Option<i64>,
|
|
app_type: Option<String>,
|
|
) -> Result<UsageSummary, AppError> {
|
|
state
|
|
.db
|
|
.get_usage_summary(start_date, end_date, app_type.as_deref())
|
|
}
|
|
|
|
/// 获取按 app_type 拆分的使用量汇总
|
|
#[tauri::command]
|
|
pub fn get_usage_summary_by_app(
|
|
state: State<'_, AppState>,
|
|
start_date: Option<i64>,
|
|
end_date: Option<i64>,
|
|
) -> Result<Vec<UsageSummaryByApp>, AppError> {
|
|
state.db.get_usage_summary_by_app(start_date, end_date)
|
|
}
|
|
|
|
/// 获取每日趋势
|
|
#[tauri::command]
|
|
pub fn get_usage_trends(
|
|
state: State<'_, AppState>,
|
|
start_date: Option<i64>,
|
|
end_date: Option<i64>,
|
|
app_type: Option<String>,
|
|
) -> Result<Vec<DailyStats>, AppError> {
|
|
state
|
|
.db
|
|
.get_daily_trends(start_date, end_date, app_type.as_deref())
|
|
}
|
|
|
|
/// 获取 Provider 统计
|
|
#[tauri::command]
|
|
pub fn get_provider_stats(
|
|
state: State<'_, AppState>,
|
|
start_date: Option<i64>,
|
|
end_date: Option<i64>,
|
|
app_type: Option<String>,
|
|
) -> Result<Vec<ProviderStats>, AppError> {
|
|
state
|
|
.db
|
|
.get_provider_stats(start_date, end_date, app_type.as_deref())
|
|
}
|
|
|
|
/// 获取模型统计
|
|
#[tauri::command]
|
|
pub fn get_model_stats(
|
|
state: State<'_, AppState>,
|
|
start_date: Option<i64>,
|
|
end_date: Option<i64>,
|
|
app_type: Option<String>,
|
|
) -> Result<Vec<ModelStats>, AppError> {
|
|
state
|
|
.db
|
|
.get_model_stats(start_date, end_date, app_type.as_deref())
|
|
}
|
|
|
|
/// 获取请求日志列表
|
|
#[tauri::command]
|
|
pub fn get_request_logs(
|
|
state: State<'_, AppState>,
|
|
filters: LogFilters,
|
|
page: u32,
|
|
page_size: u32,
|
|
) -> Result<PaginatedLogs, AppError> {
|
|
state.db.get_request_logs(&filters, page, page_size)
|
|
}
|
|
|
|
/// 获取单个请求详情
|
|
#[tauri::command]
|
|
pub fn get_request_detail(
|
|
state: State<'_, AppState>,
|
|
request_id: String,
|
|
) -> Result<Option<RequestLogDetail>, AppError> {
|
|
state.db.get_request_detail(&request_id)
|
|
}
|
|
|
|
/// 获取模型定价列表
|
|
#[tauri::command]
|
|
pub fn get_model_pricing(state: State<'_, AppState>) -> Result<Vec<ModelPricingInfo>, AppError> {
|
|
log::info!("获取模型定价列表");
|
|
state.db.ensure_model_pricing_seeded()?;
|
|
|
|
let db = state.db.clone();
|
|
let conn = crate::database::lock_conn!(db.conn);
|
|
|
|
// 检查表是否存在
|
|
let table_exists: bool = conn
|
|
.query_row(
|
|
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='model_pricing'",
|
|
[],
|
|
|row| row.get::<_, i64>(0).map(|count| count > 0),
|
|
)
|
|
.unwrap_or(false);
|
|
|
|
if !table_exists {
|
|
log::error!("model_pricing 表不存在,可能需要重启应用以触发数据库迁移");
|
|
return Ok(Vec::new());
|
|
}
|
|
|
|
let mut stmt = conn.prepare(
|
|
"SELECT model_id, display_name, input_cost_per_million, output_cost_per_million,
|
|
cache_read_cost_per_million, cache_creation_cost_per_million
|
|
FROM model_pricing
|
|
ORDER BY display_name",
|
|
)?;
|
|
|
|
let rows = stmt.query_map([], |row| {
|
|
Ok(ModelPricingInfo {
|
|
model_id: row.get(0)?,
|
|
display_name: row.get(1)?,
|
|
input_cost_per_million: row.get(2)?,
|
|
output_cost_per_million: row.get(3)?,
|
|
cache_read_cost_per_million: row.get(4)?,
|
|
cache_creation_cost_per_million: row.get(5)?,
|
|
})
|
|
})?;
|
|
|
|
let mut pricing = Vec::new();
|
|
for row in rows {
|
|
pricing.push(row?);
|
|
}
|
|
|
|
log::info!("成功获取 {} 条模型定价数据", pricing.len());
|
|
Ok(pricing)
|
|
}
|
|
|
|
/// 更新模型定价
|
|
#[tauri::command]
|
|
pub fn update_model_pricing(
|
|
state: State<'_, AppState>,
|
|
model_id: String,
|
|
display_name: String,
|
|
input_cost: String,
|
|
output_cost: String,
|
|
cache_read_cost: String,
|
|
cache_creation_cost: String,
|
|
) -> Result<(), AppError> {
|
|
let db = state.db.clone();
|
|
let conn = crate::database::lock_conn!(db.conn);
|
|
|
|
conn.execute(
|
|
"INSERT OR REPLACE INTO model_pricing (
|
|
model_id, display_name, input_cost_per_million, output_cost_per_million,
|
|
cache_read_cost_per_million, cache_creation_cost_per_million
|
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
|
rusqlite::params![
|
|
model_id,
|
|
display_name,
|
|
input_cost,
|
|
output_cost,
|
|
cache_read_cost,
|
|
cache_creation_cost
|
|
],
|
|
)
|
|
.map_err(|e| AppError::Database(format!("更新模型定价失败: {e}")))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// 检查 Provider 使用限额
|
|
#[tauri::command]
|
|
pub fn check_provider_limits(
|
|
state: State<'_, AppState>,
|
|
provider_id: String,
|
|
app_type: String,
|
|
) -> Result<crate::services::usage_stats::ProviderLimitStatus, AppError> {
|
|
state.db.check_provider_limits(&provider_id, &app_type)
|
|
}
|
|
|
|
/// 删除模型定价
|
|
#[tauri::command]
|
|
pub fn delete_model_pricing(state: State<'_, AppState>, model_id: String) -> Result<(), AppError> {
|
|
let db = state.db.clone();
|
|
let conn = crate::database::lock_conn!(db.conn);
|
|
|
|
conn.execute(
|
|
"DELETE FROM model_pricing WHERE model_id = ?1",
|
|
rusqlite::params![model_id],
|
|
)
|
|
.map_err(|e| AppError::Database(format!("删除模型定价失败: {e}")))?;
|
|
|
|
log::info!("已删除模型定价: {model_id}");
|
|
Ok(())
|
|
}
|
|
|
|
/// 手动触发会话日志同步
|
|
#[tauri::command]
|
|
pub fn sync_session_usage(
|
|
state: State<'_, AppState>,
|
|
) -> Result<crate::services::session_usage::SessionSyncResult, AppError> {
|
|
// 同步 Claude 会话日志
|
|
let mut result = crate::services::session_usage::sync_claude_session_logs(&state.db)?;
|
|
|
|
// 同步 Codex 使用数据
|
|
match crate::services::session_usage_codex::sync_codex_usage(&state.db) {
|
|
Ok(codex_result) => {
|
|
result.imported += codex_result.imported;
|
|
result.skipped += codex_result.skipped;
|
|
result.files_scanned += codex_result.files_scanned;
|
|
result.errors.extend(codex_result.errors);
|
|
}
|
|
Err(e) => {
|
|
result.errors.push(format!("Codex 同步失败: {e}"));
|
|
}
|
|
}
|
|
|
|
// 同步 Gemini 使用数据
|
|
match crate::services::session_usage_gemini::sync_gemini_usage(&state.db) {
|
|
Ok(gemini_result) => {
|
|
result.imported += gemini_result.imported;
|
|
result.skipped += gemini_result.skipped;
|
|
result.files_scanned += gemini_result.files_scanned;
|
|
result.errors.extend(gemini_result.errors);
|
|
}
|
|
Err(e) => {
|
|
result.errors.push(format!("Gemini 同步失败: {e}"));
|
|
}
|
|
}
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
/// 获取数据来源分布
|
|
#[tauri::command]
|
|
pub fn get_usage_data_sources(
|
|
state: State<'_, AppState>,
|
|
) -> Result<Vec<crate::services::session_usage::DataSourceSummary>, AppError> {
|
|
crate::services::session_usage::get_data_source_breakdown(&state.db)
|
|
}
|
|
|
|
/// 模型定价信息
|
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ModelPricingInfo {
|
|
pub model_id: String,
|
|
pub display_name: String,
|
|
pub input_cost_per_million: String,
|
|
pub output_cost_per_million: String,
|
|
pub cache_read_cost_per_million: String,
|
|
pub cache_creation_cost_per_million: String,
|
|
}
|