feat(commands): add usage statistics Tauri commands

Register usage commands for summary, trends, logs, and pricing.
Expose usage stats service through Tauri command layer.
This commit is contained in:
YoVinchen
2025-12-03 00:19:36 +08:00
parent 6423e2e8ab
commit 96ec6c561e
4 changed files with 76 additions and 31 deletions
+2
View File
@@ -12,6 +12,7 @@ mod provider;
mod proxy; mod proxy;
mod settings; mod settings;
pub mod skill; pub mod skill;
mod usage;
pub use config::*; pub use config::*;
pub use deeplink::*; pub use deeplink::*;
@@ -25,3 +26,4 @@ pub use provider::*;
pub use proxy::*; pub use proxy::*;
pub use settings::*; pub use settings::*;
pub use skill::*; pub use skill::*;
pub use usage::*;
+57 -31
View File
@@ -1,82 +1,89 @@
//! 使用统计相关命令 //! 使用统计相关命令
use crate::database::Database;
use crate::error::AppError; use crate::error::AppError;
use crate::services::usage_stats::*; use crate::services::usage_stats::*;
use crate::store::AppState;
use tauri::State; use tauri::State;
/// 获取使用量汇总 /// 获取使用量汇总
#[tauri::command] #[tauri::command]
pub fn get_usage_summary( pub fn get_usage_summary(
db: State<'_, Database>, state: State<'_, AppState>,
start_date: Option<i64>, start_date: Option<i64>,
end_date: Option<i64>, end_date: Option<i64>,
) -> Result<UsageSummary, AppError> { ) -> Result<UsageSummary, AppError> {
db.get_usage_summary(start_date, end_date) state.db.get_usage_summary(start_date, end_date)
} }
/// 获取每日趋势 /// 获取每日趋势
#[tauri::command] #[tauri::command]
pub fn get_usage_trends( pub fn get_usage_trends(
db: State<'_, Database>, state: State<'_, AppState>,
days: u32, days: u32,
) -> Result<Vec<DailyStats>, AppError> { ) -> Result<Vec<DailyStats>, AppError> {
db.get_daily_trends(days) state.db.get_daily_trends(days)
} }
/// 获取 Provider 统计 /// 获取 Provider 统计
#[tauri::command] #[tauri::command]
pub fn get_provider_stats(db: State<'_, Database>) -> Result<Vec<ProviderStats>, AppError> { pub fn get_provider_stats(state: State<'_, AppState>) -> Result<Vec<ProviderStats>, AppError> {
db.get_provider_stats() state.db.get_provider_stats()
} }
/// 获取模型统计 /// 获取模型统计
#[tauri::command] #[tauri::command]
pub fn get_model_stats(db: State<'_, Database>) -> Result<Vec<ModelStats>, AppError> { pub fn get_model_stats(state: State<'_, AppState>) -> Result<Vec<ModelStats>, AppError> {
db.get_model_stats() state.db.get_model_stats()
} }
/// 获取请求日志列表 /// 获取请求日志列表
#[tauri::command] #[tauri::command]
pub fn get_request_logs( pub fn get_request_logs(
db: State<'_, Database>, state: State<'_, AppState>,
provider_id: Option<String>, filters: LogFilters,
model: Option<String>,
status_code: Option<u16>,
start_date: Option<i64>,
end_date: Option<i64>,
limit: u32, limit: u32,
offset: u32, offset: u32,
) -> Result<Vec<RequestLogDetail>, AppError> { ) -> Result<Vec<RequestLogDetail>, AppError> {
let filters = LogFilters { state.db.get_request_logs(&filters, limit, offset)
provider_id,
model,
status_code,
start_date,
end_date,
};
db.get_request_logs(&filters, limit, offset)
} }
/// 获取单个请求详情 /// 获取单个请求详情
#[tauri::command] #[tauri::command]
pub fn get_request_detail( pub fn get_request_detail(
db: State<'_, Database>, state: State<'_, AppState>,
request_id: String, request_id: String,
) -> Result<Option<RequestLogDetail>, AppError> { ) -> Result<Option<RequestLogDetail>, AppError> {
db.get_request_detail(&request_id) state.db.get_request_detail(&request_id)
} }
/// 获取模型定价列表 /// 获取模型定价列表
#[tauri::command] #[tauri::command]
pub fn get_model_pricing(db: State<'_, Database>) -> Result<Vec<ModelPricingInfo>, AppError> { 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 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( let mut stmt = conn.prepare(
"SELECT model_id, display_name, input_cost_per_million, output_cost_per_million, "SELECT model_id, display_name, input_cost_per_million, output_cost_per_million,
cache_read_cost_per_million, cache_creation_cost_per_million cache_read_cost_per_million, cache_creation_cost_per_million
FROM model_pricing FROM model_pricing
ORDER BY display_name" ORDER BY display_name",
)?; )?;
let rows = stmt.query_map([], |row| { let rows = stmt.query_map([], |row| {
@@ -95,13 +102,14 @@ pub fn get_model_pricing(db: State<'_, Database>) -> Result<Vec<ModelPricingInfo
pricing.push(row?); pricing.push(row?);
} }
log::info!("成功获取 {} 条模型定价数据", pricing.len());
Ok(pricing) Ok(pricing)
} }
/// 更新模型定价 /// 更新模型定价
#[tauri::command] #[tauri::command]
pub fn update_model_pricing( pub fn update_model_pricing(
db: State<'_, Database>, state: State<'_, AppState>,
model_id: String, model_id: String,
display_name: String, display_name: String,
input_cost: String, input_cost: String,
@@ -109,6 +117,7 @@ pub fn update_model_pricing(
cache_read_cost: String, cache_read_cost: String,
cache_creation_cost: String, cache_creation_cost: String,
) -> Result<(), AppError> { ) -> Result<(), AppError> {
let db = state.db.clone();
let conn = crate::database::lock_conn!(db.conn); let conn = crate::database::lock_conn!(db.conn);
conn.execute( conn.execute(
@@ -133,15 +142,32 @@ pub fn update_model_pricing(
/// 检查 Provider 使用限额 /// 检查 Provider 使用限额
#[tauri::command] #[tauri::command]
pub fn check_provider_limits( pub fn check_provider_limits(
db: State<'_, Database>, state: State<'_, AppState>,
provider_id: String, provider_id: String,
app_type: String, app_type: String,
) -> Result<crate::services::usage_stats::ProviderLimitStatus, AppError> { ) -> Result<crate::services::usage_stats::ProviderLimitStatus, AppError> {
db.check_provider_limits(&provider_id, &app_type) 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(())
} }
/// 模型定价信息 /// 模型定价信息
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelPricingInfo { pub struct ModelPricingInfo {
pub model_id: String, pub model_id: String,
pub display_name: String, pub display_name: String,
+11
View File
@@ -650,6 +650,17 @@ pub fn run() {
commands::get_proxy_config, commands::get_proxy_config,
commands::update_proxy_config, commands::update_proxy_config,
commands::is_proxy_running, commands::is_proxy_running,
// Usage statistics
commands::get_usage_summary,
commands::get_usage_trends,
commands::get_provider_stats,
commands::get_model_stats,
commands::get_request_logs,
commands::get_request_detail,
commands::get_model_pricing,
commands::update_model_pricing,
commands::delete_model_pricing,
commands::check_provider_limits,
]); ]);
let app = builder let app = builder
+6
View File
@@ -7,6 +7,7 @@ pub mod provider;
pub mod proxy; pub mod proxy;
pub mod skill; pub mod skill;
pub mod speedtest; pub mod speedtest;
pub mod usage_stats;
pub use config::ConfigService; pub use config::ConfigService;
pub use mcp::McpService; pub use mcp::McpService;
@@ -15,3 +16,8 @@ pub use provider::{ProviderService, ProviderSortUpdate};
pub use proxy::ProxyService; pub use proxy::ProxyService;
pub use skill::{Skill, SkillRepo, SkillService}; pub use skill::{Skill, SkillRepo, SkillService};
pub use speedtest::{EndpointLatency, SpeedtestService}; pub use speedtest::{EndpointLatency, SpeedtestService};
#[allow(unused_imports)]
pub use usage_stats::{
DailyStats, LogFilters, ModelStats, ProviderLimitStatus, ProviderStats, RequestLogDetail,
UsageSummary,
};