use anyhow::Result; use async_trait::async_trait; use reqwest::Client; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::collections::HashMap; use std::time::Duration; use tauri::{command, State}; use crate::commands::agents::AgentDb; use crate::commands::relay_stations::{RelayStationAdapter, RelayStation}; use crate::i18n; // 创建HTTP客户端的辅助函数 fn create_http_client() -> Client { Client::builder() .timeout(Duration::from_secs(10)) .build() .expect("Failed to create HTTP client") } /// 中转站信息 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StationInfo { pub name: String, pub announcement: Option, pub api_url: String, pub version: Option, pub metadata: Option>, pub quota_per_unit: Option, } /// 用户信息 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UserInfo { pub id: String, pub username: String, pub display_name: Option, pub email: Option, pub quota: i64, pub used_quota: i64, pub request_count: i64, pub group: String, pub status: String, } /// 连接测试结果 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConnectionTestResult { pub success: bool, pub response_time: u64, // 响应时间(毫秒) pub message: String, pub details: Option, } /// Token 信息 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TokenInfo { pub id: String, pub name: String, pub key: String, pub quota: i64, pub used_quota: i64, pub unlimited_quota: bool, pub request_count: i64, pub status: String, pub created_at: u64, pub accessed_at: Option, } /// 分页信息 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PaginationInfo { pub current_page: usize, pub total_pages: usize, pub has_next: bool, pub total_items: usize, } /// Token 分页响应 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TokenPaginationResponse { pub tokens: Vec, pub pagination: PaginationInfo, } /// 中转站适配器 trait #[async_trait] pub trait StationAdapter: Send + Sync { /// 获取中转站信息 async fn get_station_info(&self, station: &RelayStation) -> Result; /// 获取用户信息 async fn get_user_info(&self, station: &RelayStation, user_id: &str) -> Result; /// 测试连接 async fn test_connection(&self, station: &RelayStation) -> Result; /// 获取使用日志 async fn get_usage_logs(&self, station: &RelayStation, user_id: &str, page: Option, size: Option) -> Result; /// 列出 Tokens async fn list_tokens(&self, station: &RelayStation, page: Option, size: Option) -> Result; /// 创建 Token async fn create_token(&self, station: &RelayStation, name: &str, quota: Option) -> Result; /// 更新 Token async fn update_token(&self, station: &RelayStation, token_id: &str, name: Option<&str>, quota: Option) -> Result; /// 删除 Token async fn delete_token(&self, station: &RelayStation, token_id: &str) -> Result; } /// PackyCode 适配器(默认使用 API Key 认证) pub struct PackycodeAdapter; #[async_trait] impl StationAdapter for PackycodeAdapter { async fn get_station_info(&self, station: &RelayStation) -> Result { // PackyCode 使用简单的健康检查端点 let url = format!("{}/health", station.api_url.trim_end_matches('/')); let client = create_http_client(); let response = client .get(&url) .header("X-API-Key", &station.system_token) .send() .await?; if response.status().is_success() { Ok(StationInfo { name: station.name.clone(), announcement: Some("PackyCode 服务运行正常".to_string()), api_url: station.api_url.clone(), version: Some("PackyCode v1.0".to_string()), metadata: Some({ let mut map = HashMap::new(); map.insert("adapter_type".to_string(), json!("packycode")); map.insert("support_features".to_string(), json!(["quota_query", "usage_stats"])); map }), quota_per_unit: Some(1), }) } else { Err(anyhow::anyhow!("PackyCode service unavailable")) } } async fn get_user_info(&self, station: &RelayStation, _user_id: &str) -> Result { // PackyCode 用户信息获取 let url = format!("{}/user/info", station.api_url.trim_end_matches('/')); let client = create_http_client(); let response = client .get(&url) .header("X-API-Key", &station.system_token) .send() .await?; let data: Value = response.json().await?; Ok(UserInfo { id: "packycode_user".to_string(), username: data.get("username") .and_then(|v| v.as_str()) .unwrap_or("PackyCode用户") .to_string(), display_name: Some("PackyCode用户".to_string()), email: data.get("email") .and_then(|v| v.as_str()) .map(|s| s.to_string()), quota: data.get("quota") .and_then(|v| v.as_i64()) .unwrap_or(0), used_quota: data.get("used_quota") .and_then(|v| v.as_i64()) .unwrap_or(0), request_count: data.get("request_count") .and_then(|v| v.as_i64()) .unwrap_or(0), group: "default".to_string(), status: "active".to_string(), }) } async fn test_connection(&self, station: &RelayStation) -> Result { let start_time = std::time::Instant::now(); match self.get_station_info(station).await { Ok(info) => { let response_time = start_time.elapsed().as_millis() as u64; Ok(ConnectionTestResult { success: true, response_time, message: format!("{} - 连接成功", info.name), details: Some(format!("服务版本: {}", info.version.unwrap_or_else(|| "Unknown".to_string()))), }) } Err(e) => { let response_time = start_time.elapsed().as_millis() as u64; Ok(ConnectionTestResult { success: false, response_time, message: format!("连接失败: {}", e), details: None, }) } } } async fn get_usage_logs(&self, _station: &RelayStation, _user_id: &str, _page: Option, _size: Option) -> Result { // PackyCode 暂不支持详细使用日志 Ok(json!({ "logs": [], "message": "PackyCode 暂不支持详细使用日志查询" })) } async fn list_tokens(&self, _station: &RelayStation, _page: Option, _size: Option) -> Result { // PackyCode 使用单一 Token,不支持多 Token 管理 Err(anyhow::anyhow!(i18n::t("relay_adapter.packycode_single_token"))) } async fn create_token(&self, _station: &RelayStation, _name: &str, _quota: Option) -> Result { Err(anyhow::anyhow!(i18n::t("relay_adapter.packycode_single_token"))) } async fn update_token(&self, _station: &RelayStation, _token_id: &str, _name: Option<&str>, _quota: Option) -> Result { Err(anyhow::anyhow!(i18n::t("relay_adapter.packycode_single_token"))) } async fn delete_token(&self, _station: &RelayStation, _token_id: &str) -> Result { Err(anyhow::anyhow!(i18n::t("relay_adapter.packycode_single_token"))) } } /// Custom 适配器(简化版本,仅提供基本信息) pub struct CustomAdapter; #[async_trait] impl StationAdapter for CustomAdapter { async fn get_station_info(&self, station: &RelayStation) -> Result { Ok(StationInfo { name: station.name.clone(), announcement: None, api_url: station.api_url.clone(), version: Some("Custom".to_string()), metadata: Some({ let mut map = HashMap::new(); map.insert("adapter_type".to_string(), json!("custom")); map }), quota_per_unit: None, }) } async fn get_user_info(&self, _station: &RelayStation, user_id: &str) -> Result { Ok(UserInfo { id: user_id.to_string(), username: "自定义用户".to_string(), display_name: Some("自定义适配器用户".to_string()), email: None, quota: 0, used_quota: 0, request_count: 0, group: "custom".to_string(), status: "active".to_string(), }) } async fn test_connection(&self, station: &RelayStation) -> Result { let start_time = std::time::Instant::now(); // 尝试简单的 GET 请求测试连接 let client = create_http_client(); let response = client .get(&station.api_url) .header("Authorization", format!("Bearer {}", station.system_token)) .timeout(Duration::from_secs(5)) .send() .await; let response_time = start_time.elapsed().as_millis() as u64; match response { Ok(resp) => { Ok(ConnectionTestResult { success: resp.status().is_success(), response_time, message: if resp.status().is_success() { format!("{} - 连接成功", station.name) } else { format!("HTTP {}: 服务器响应错误", resp.status()) }, details: Some(format!("响应状态: {}", resp.status())), }) } Err(e) => { Ok(ConnectionTestResult { success: false, response_time, message: format!("连接失败: {}", e), details: None, }) } } } async fn get_usage_logs(&self, _station: &RelayStation, _user_id: &str, _page: Option, _size: Option) -> Result { Ok(json!({ "logs": [], "message": "自定义适配器暂不支持使用日志查询" })) } async fn list_tokens(&self, _station: &RelayStation, _page: Option, _size: Option) -> Result { Err(anyhow::anyhow!(i18n::t("relay_adapter.token_management_not_available"))) } async fn create_token(&self, _station: &RelayStation, _name: &str, _quota: Option) -> Result { Err(anyhow::anyhow!(i18n::t("relay_adapter.token_management_not_available"))) } async fn update_token(&self, _station: &RelayStation, _token_id: &str, _name: Option<&str>, _quota: Option) -> Result { Err(anyhow::anyhow!(i18n::t("relay_adapter.token_management_not_available"))) } async fn delete_token(&self, _station: &RelayStation, _token_id: &str) -> Result { Err(anyhow::anyhow!(i18n::t("relay_adapter.token_management_not_available"))) } } /// 适配器工厂函数 pub fn create_adapter(adapter_type: &RelayStationAdapter) -> Box { match adapter_type { RelayStationAdapter::Packycode => Box::new(PackycodeAdapter), // DeepSeek、GLM、Qwen、Kimi 都使用简单的自定义适配器 RelayStationAdapter::Deepseek => Box::new(CustomAdapter), RelayStationAdapter::Glm => Box::new(CustomAdapter), RelayStationAdapter::Qwen => Box::new(CustomAdapter), RelayStationAdapter::Kimi => Box::new(CustomAdapter), RelayStationAdapter::Custom => Box::new(CustomAdapter), } } /// 获取中转站信息 #[command] pub async fn relay_station_get_info( station_id: String, db: State<'_, AgentDb> ) -> Result { // 获取中转站配置 let station = crate::commands::relay_stations::relay_station_get(station_id, db).await?; // 创建适配器 let adapter = create_adapter(&station.adapter); // 获取站点信息 adapter.get_station_info(&station).await .map_err(|e| { log::error!("Failed to get station info: {}", e); i18n::t("relay_adapter.get_info_failed") }) } /// 获取用户信息 #[command] pub async fn relay_station_get_user_info( station_id: String, user_id: String, db: State<'_, AgentDb> ) -> Result { let station = crate::commands::relay_stations::relay_station_get(station_id, db).await?; let adapter = create_adapter(&station.adapter); adapter.get_user_info(&station, &user_id).await .map_err(|e| { log::error!("Failed to get user info: {}", e); i18n::t("relay_adapter.get_user_info_failed") }) } /// 测试中转站连接 #[command] pub async fn relay_station_test_connection( station_id: String, db: State<'_, AgentDb> ) -> Result { let station = crate::commands::relay_stations::relay_station_get(station_id, db).await?; let adapter = create_adapter(&station.adapter); adapter.test_connection(&station).await .map_err(|e| { log::error!("Connection test failed: {}", e); i18n::t("relay_adapter.connection_test_failed") }) } /// 获取使用日志 #[command] pub async fn relay_station_get_usage_logs( station_id: String, user_id: String, page: Option, size: Option, db: State<'_, AgentDb> ) -> Result { let station = crate::commands::relay_stations::relay_station_get(station_id, db).await?; let adapter = create_adapter(&station.adapter); adapter.get_usage_logs(&station, &user_id, page, size).await .map_err(|e| { log::error!("Failed to get usage logs: {}", e); i18n::t("relay_adapter.get_usage_logs_failed") }) } /// 列出 Token #[command] pub async fn relay_station_list_tokens( station_id: String, page: Option, size: Option, db: State<'_, AgentDb> ) -> Result { let station = crate::commands::relay_stations::relay_station_get(station_id, db).await?; let adapter = create_adapter(&station.adapter); adapter.list_tokens(&station, page, size).await .map_err(|e| { log::error!("Failed to list tokens: {}", e); i18n::t("relay_adapter.list_tokens_failed") }) } /// 创建 Token #[command] pub async fn relay_station_create_token( station_id: String, name: String, quota: Option, db: State<'_, AgentDb> ) -> Result { let station = crate::commands::relay_stations::relay_station_get(station_id, db).await?; let adapter = create_adapter(&station.adapter); adapter.create_token(&station, &name, quota).await .map_err(|e| { log::error!("Failed to create token: {}", e); i18n::t("relay_adapter.create_token_failed") }) } /// 更新 Token #[command] pub async fn relay_station_update_token( station_id: String, token_id: String, name: Option, quota: Option, db: State<'_, AgentDb> ) -> Result { let station = crate::commands::relay_stations::relay_station_get(station_id, db).await?; let adapter = create_adapter(&station.adapter); adapter.update_token(&station, &token_id, name.as_deref(), quota).await .map_err(|e| { log::error!("Failed to update token: {}", e); i18n::t("relay_adapter.update_token_failed") }) } /// 删除 Token #[command] pub async fn relay_station_delete_token( station_id: String, token_id: String, db: State<'_, AgentDb> ) -> Result { let station = crate::commands::relay_stations::relay_station_get(station_id, db).await?; let adapter = create_adapter(&station.adapter); adapter.delete_token(&station, &token_id).await .map_err(|e| { log::error!("Failed to delete token: {}", e); i18n::t("relay_adapter.delete_token_failed") }) } /// PackyCode 用户额度信息 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PackycodeUserQuota { pub daily_budget_usd: f64, // 日预算(美元) pub daily_spent_usd: f64, // 日已使用(美元) pub monthly_budget_usd: f64, // 月预算(美元) pub monthly_spent_usd: f64, // 月已使用(美元) pub balance_usd: f64, // 账户余额(美元) pub total_spent_usd: f64, // 总消费(美元) pub plan_type: String, // 计划类型 (pro, basic, etc.) pub plan_expires_at: Option, // 计划过期时间 pub username: Option, // 用户名 pub email: Option, // 邮箱 pub opus_enabled: Option, // 是否启用Opus模型 } /// 获取 PackyCode 用户额度(专用) #[command] pub async fn packycode_get_user_quota( station_id: String, db: State<'_, AgentDb> ) -> Result { let station = crate::commands::relay_stations::relay_station_get(station_id, db).await .map_err(|e| format!("Failed to get station: {}", e))?; if station.adapter.as_str() != "packycode" { return Err("此功能仅支持 PackyCode 中转站".to_string()); } // 根据服务类型构建不同的 URL let url = if station.api_url.contains("share-api") || station.api_url.contains("share.packycode") { // 滴滴车服务 "https://share.packycode.com/api/backend/users/info" } else { // 公交车服务 "https://www.packycode.com/api/backend/users/info" }; let client = Client::builder() .timeout(Duration::from_secs(30)) .no_proxy() // 禁用所有代理 .build() .map_err(|e| format!("创建 HTTP 客户端失败: {}", e))?; log::info!("正在请求 PackyCode 用户信息: {}", url); let response = client .get(url) .header("Authorization", format!("Bearer {}", station.system_token)) .header("User-Agent", "Claudia") .header("Accept", "*/*") .send() .await .map_err(|e| { log::error!("请求 PackyCode API 失败: {}", e); if e.is_connect() { format!("网络连接失败: {}", e) } else if e.is_timeout() { format!("请求超时: {}", e) } else { format!("请求失败: {}", e) } })?; if !response.status().is_success() { let status = response.status(); let error_text = response.text().await.unwrap_or_default(); return Err(match status.as_u16() { 401 => "Token 无效或已过期".to_string(), 403 => "权限不足".to_string(), 400 => format!("请求参数错误: {}", error_text), _ => format!("请求失败 ({}): {}", status, error_text), }); } let data: Value = response.json().await .map_err(|e| format!("解析响应失败: {}", e))?; // 辅助函数:将值转换为 f64 let to_f64 = |v: &Value| -> f64 { if v.is_null() { 0.0 } else if v.is_string() { v.as_str().and_then(|s| s.parse::().ok()).unwrap_or(0.0) } else if v.is_f64() { v.as_f64().unwrap_or(0.0) } else if v.is_i64() { v.as_i64().map(|i| i as f64).unwrap_or(0.0) } else { 0.0 } }; Ok(PackycodeUserQuota { daily_budget_usd: to_f64(data.get("daily_budget_usd").unwrap_or(&Value::Null)), daily_spent_usd: to_f64(data.get("daily_spent_usd").unwrap_or(&Value::Null)), monthly_budget_usd: to_f64(data.get("monthly_budget_usd").unwrap_or(&Value::Null)), monthly_spent_usd: to_f64(data.get("monthly_spent_usd").unwrap_or(&Value::Null)), balance_usd: to_f64(data.get("balance_usd").unwrap_or(&Value::Null)), total_spent_usd: to_f64(data.get("total_spent_usd").unwrap_or(&Value::Null)), plan_type: data.get("plan_type") .and_then(|v| v.as_str()) .unwrap_or("basic") .to_string(), plan_expires_at: data.get("plan_expires_at") .and_then(|v| v.as_str()) .map(|s| s.to_string()), username: data.get("username") .and_then(|v| v.as_str()) .map(|s| s.to_string()), email: data.get("email") .and_then(|v| v.as_str()) .map(|s| s.to_string()), opus_enabled: data.get("opus_enabled") .and_then(|v| v.as_bool()), }) }