diff --git a/src-tauri/src/claude_config.rs b/src-tauri/src/claude_config.rs index 7cd47fe..c83852e 100644 --- a/src-tauri/src/claude_config.rs +++ b/src-tauri/src/claude_config.rs @@ -204,15 +204,6 @@ pub fn apply_relay_station_to_config(station: &RelayStation) -> Result<(), Strin "packycode" => { // PackyCode 使用原始配置,不做特殊处理 } - "newapi" | "oneapi" => { - // NewAPI 和 OneAPI 兼容 OpenAI 格式,不需要特殊处理 - } - "yourapi" => { - // YourAPI 可能需要特殊的路径格式 - if !station.api_url.ends_with("/v1") { - config.env.anthropic_base_url = Some(format!("{}/v1", station.api_url)); - } - } "custom" => { // 自定义适配器,使用原始配置 } diff --git a/src-tauri/src/commands/relay_adapters.rs b/src-tauri/src/commands/relay_adapters.rs index 6bcdb6d..a9fa899 100644 --- a/src-tauri/src/commands/relay_adapters.rs +++ b/src-tauri/src/commands/relay_adapters.rs @@ -1,33 +1,93 @@ +use anyhow::Result; use async_trait::async_trait; -use anyhow::{anyhow, Result}; use reqwest::Client; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::collections::HashMap; -use std::time::{Duration, Instant}; +use std::time::Duration; use tauri::{command, State}; use crate::commands::agents::AgentDb; -use crate::commands::relay_stations::{ - RelayStation, StationInfo, UserInfo, ConnectionTestResult, - TokenInfo, TokenPaginationResponse, RelayStationAdapter -}; +use crate::commands::relay_stations::{RelayStationAdapter, RelayStation}; use crate::i18n; -/// HTTP 客户端单例 -static HTTP_CLIENT: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { +// 创建HTTP客户端的辅助函数 +fn create_http_client() -> Client { Client::builder() - .timeout(Duration::from_secs(30)) - .pool_max_idle_per_host(10) - .pool_idle_timeout(Duration::from_secs(90)) + .timeout(Duration::from_secs(10)) .build() - .unwrap() -}); + .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; /// 获取用户信息 @@ -36,10 +96,10 @@ pub trait StationAdapter: Send + Sync { /// 测试连接 async fn test_connection(&self, station: &RelayStation) -> Result; - /// 获取用户使用日志 + /// 获取使用日志 async fn get_usage_logs(&self, station: &RelayStation, user_id: &str, page: Option, size: Option) -> Result; - /// 列出用户 Token + /// 列出 Tokens async fn list_tokens(&self, station: &RelayStation, page: Option, size: Option) -> Result; /// 创建 Token @@ -58,216 +118,109 @@ pub struct PackycodeAdapter; #[async_trait] impl StationAdapter for PackycodeAdapter { async fn get_station_info(&self, station: &RelayStation) -> Result { - let url = format!("{}/api/status", station.api_url.trim_end_matches('/')); + // PackyCode 使用简单的健康检查端点 + let url = format!("{}/health", station.api_url.trim_end_matches('/')); - let response = HTTP_CLIENT + let client = create_http_client(); + let response = client .get(&url) - .header("Authorization", format!("sk-{}", station.system_token)) + .header("X-API-Key", &station.system_token) .send() .await?; - let data: Value = response.json().await?; - - if !data.get("success").and_then(|v| v.as_bool()).unwrap_or(false) { - return Err(anyhow::anyhow!("API Error: {}", - data.get("message").and_then(|v| v.as_str()).unwrap_or("Unknown error"))); + 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")) } - - let default_data = json!({}); - let data = data.get("data").unwrap_or(&default_data); - - Ok(StationInfo { - name: data.get("system_name") - .and_then(|v| v.as_str()) - .unwrap_or("PackyCode") - .to_string(), - announcement: data.get("announcement") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()), - api_url: station.api_url.clone(), - version: data.get("version") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()), - metadata: Some({ - let mut map = HashMap::new(); - map.insert("adapter_type".to_string(), json!("packycode")); - map.insert("service_type".to_string(), json!( - if station.api_url.contains("share.packycode.com") { - "bus" // 公交车 - } else { - "taxi" // 滴滴车 - } - )); - if let Some(quota_per_unit) = data.get("quota_per_unit").and_then(|v| v.as_i64()) { - map.insert("quota_per_unit".to_string(), json!(quota_per_unit)); - } - map - }), - quota_per_unit: data.get("quota_per_unit").and_then(|v| v.as_i64()), - }) } - async fn get_user_info(&self, station: &RelayStation, user_id: &str) -> Result { - let url = format!("{}/api/user/self", station.api_url.trim_end_matches('/')); + 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 response = HTTP_CLIENT + let client = create_http_client(); + let response = client .get(&url) - .header("Authorization", format!("sk-{}", station.system_token)) - .header("X-User-Id", user_id) + .header("X-API-Key", &station.system_token) .send() .await?; let data: Value = response.json().await?; - if !data.get("success").and_then(|v| v.as_bool()).unwrap_or(false) { - return Err(anyhow::anyhow!("API Error: {}", - data.get("message").and_then(|v| v.as_str()).unwrap_or("Unknown error"))); - } - - let user_data = data.get("data").ok_or_else(|| anyhow!("No user data returned"))?; - Ok(UserInfo { - user_id: user_data.get("id") - .and_then(|v| v.as_i64()) - .map(|id| id.to_string()) - .unwrap_or_else(|| user_id.to_string()), - username: user_data.get("username") + 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()), - email: user_data.get("email") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()), - balance_remaining: user_data.get("quota") + quota: data.get("quota") .and_then(|v| v.as_i64()) - .map(|q| q as f64 / 500000.0), // 转换为美元 - amount_used: user_data.get("used_quota") + .unwrap_or(0), + used_quota: data.get("used_quota") .and_then(|v| v.as_i64()) - .map(|q| q as f64 / 500000.0), - request_count: user_data.get("request_count") - .and_then(|v| v.as_i64()), - status: match user_data.get("status").and_then(|v| v.as_i64()) { - Some(1) => Some("active".to_string()), - Some(0) => Some("disabled".to_string()), - _ => Some("unknown".to_string()), - }, - metadata: Some({ - let mut map = HashMap::new(); - map.insert("raw_data".to_string(), user_data.clone()); - map - }), + .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 = Instant::now(); - let url = format!("{}/api/status", station.api_url.trim_end_matches('/')); + let start_time = std::time::Instant::now(); - match HTTP_CLIENT - .get(&url) - .header("Authorization", format!("sk-{}", station.system_token)) - .timeout(Duration::from_secs(10)) - .send() - .await - { - Ok(response) => { + match self.get_station_info(station).await { + Ok(info) => { let response_time = start_time.elapsed().as_millis() as u64; - - if response.status().is_success() { - match response.json::().await { - Ok(data) => { - let success = data.get("success").and_then(|v| v.as_bool()).unwrap_or(false); - if success { - Ok(ConnectionTestResult { - success: true, - response_time: Some(response_time), - message: i18n::t("relay_adapter.connection_success"), - error: None, - }) - } else { - let error_msg = data.get("message").and_then(|v| v.as_str()).unwrap_or("Unknown error"); - Ok(ConnectionTestResult { - success: false, - response_time: Some(response_time), - message: i18n::t("relay_adapter.api_error"), - error: Some(error_msg.to_string()), - }) - } - } - Err(e) => Ok(ConnectionTestResult { - success: false, - response_time: Some(response_time), - message: i18n::t("relay_adapter.parse_error"), - error: Some(e.to_string()), - }) - } - } else { - Ok(ConnectionTestResult { - success: false, - response_time: Some(response_time), - message: i18n::t("relay_adapter.http_error"), - error: Some(format!("HTTP {}", response.status())), - }) - } + 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: Some(response_time), - message: i18n::t("relay_adapter.network_error"), - error: Some(e.to_string()), + response_time, + message: format!("连接失败: {}", e), + details: None, }) } } } - async fn get_usage_logs(&self, station: &RelayStation, user_id: &str, page: Option, size: Option) -> Result { - let page = page.unwrap_or(1); - let size = size.unwrap_or(10); - let url = format!("{}/api/log/self?page={}&size={}", - station.api_url.trim_end_matches('/'), page, size); - - let response = HTTP_CLIENT - .get(&url) - .header("Authorization", format!("sk-{}", station.system_token)) - .header("X-User-Id", user_id) - .send() - .await?; - - let data: Value = response.json().await?; - - if !data.get("success").and_then(|v| v.as_bool()).unwrap_or(false) { - return Err(anyhow::anyhow!("API Error: {}", - data.get("message").and_then(|v| v.as_str()).unwrap_or("Unknown error"))); - } - - Ok(data.get("data").cloned().unwrap_or(json!([]))) + 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 管理 - let page = page.unwrap_or(1); - let size = size.unwrap_or(10); - - // 返回当前使用的 API Key 作为唯一 Token - let token = TokenInfo { - id: "1".to_string(), - name: "API Key".to_string(), - token: format!("sk-{}...", &station.system_token[..8]), - quota: None, - used_quota: None, - status: "active".to_string(), - created_at: station.created_at, - updated_at: station.updated_at, - }; - - Ok(TokenPaginationResponse { - tokens: vec![token], - total: 1, - page, - size, - has_more: false, - }) + 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 { @@ -283,501 +236,6 @@ impl StationAdapter for PackycodeAdapter { } } -/// NewAPI 适配器(支持 NewAPI 和 OneAPI) -pub struct NewApiAdapter; - -#[async_trait] -impl StationAdapter for NewApiAdapter { - async fn get_station_info(&self, station: &RelayStation) -> Result { - let url = format!("{}/api/status", station.api_url.trim_end_matches('/')); - - let response = HTTP_CLIENT - .get(&url) - .header("Authorization", format!("Bearer {}", station.system_token)) - .send() - .await?; - - let data: Value = response.json().await?; - - if !data.get("success").and_then(|v| v.as_bool()).unwrap_or(false) { - return Err(anyhow::anyhow!("API Error: {}", - data.get("message").and_then(|v| v.as_str()).unwrap_or("Unknown error"))); - } - - let default_data = json!({}); - let data = data.get("data").unwrap_or(&default_data); - - Ok(StationInfo { - name: data.get("system_name") - .and_then(|v| v.as_str()) - .unwrap_or(&station.name) - .to_string(), - announcement: data.get("announcement") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()), - api_url: station.api_url.clone(), - version: data.get("version") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()), - metadata: Some({ - let mut map = HashMap::new(); - map.insert("adapter_type".to_string(), json!("newapi")); - if let Some(quota_per_unit) = data.get("quota_per_unit").and_then(|v| v.as_i64()) { - map.insert("quota_per_unit".to_string(), json!(quota_per_unit)); - } - map - }), - quota_per_unit: data.get("quota_per_unit").and_then(|v| v.as_i64()), - }) - } - - async fn get_user_info(&self, station: &RelayStation, user_id: &str) -> Result { - let url = format!("{}/api/user/self", station.api_url.trim_end_matches('/')); - - let response = HTTP_CLIENT - .get(&url) - .header("Authorization", format!("Bearer {}", station.system_token)) - .header("New-API-User", user_id) - .send() - .await?; - - let data: Value = response.json().await?; - - if !data.get("success").and_then(|v| v.as_bool()).unwrap_or(false) { - return Err(anyhow::anyhow!("API Error: {}", - data.get("message").and_then(|v| v.as_str()).unwrap_or("Unknown error"))); - } - - let user_data = data.get("data").ok_or_else(|| anyhow!("No user data returned"))?; - - Ok(UserInfo { - user_id: user_data.get("id") - .and_then(|v| v.as_i64()) - .map(|id| id.to_string()) - .unwrap_or_else(|| user_id.to_string()), - username: user_data.get("username") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()), - email: user_data.get("email") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()), - balance_remaining: user_data.get("quota") - .and_then(|v| v.as_i64()) - .map(|q| q as f64 / 500000.0), // 转换为美元 - amount_used: user_data.get("used_quota") - .and_then(|v| v.as_i64()) - .map(|q| q as f64 / 500000.0), - request_count: user_data.get("request_count") - .and_then(|v| v.as_i64()), - status: match user_data.get("status").and_then(|v| v.as_i64()) { - Some(1) => Some("active".to_string()), - Some(0) => Some("disabled".to_string()), - _ => Some("unknown".to_string()), - }, - metadata: Some({ - let mut map = HashMap::new(); - map.insert("raw_data".to_string(), user_data.clone()); - map - }), - }) - } - - async fn test_connection(&self, station: &RelayStation) -> Result { - let start_time = Instant::now(); - let url = format!("{}/api/status", station.api_url.trim_end_matches('/')); - - match HTTP_CLIENT - .get(&url) - .header("Authorization", format!("Bearer {}", station.system_token)) - .timeout(Duration::from_secs(10)) - .send() - .await - { - Ok(response) => { - let response_time = start_time.elapsed().as_millis() as u64; - - if response.status().is_success() { - match response.json::().await { - Ok(data) => { - let success = data.get("success").and_then(|v| v.as_bool()).unwrap_or(false); - if success { - Ok(ConnectionTestResult { - success: true, - response_time: Some(response_time), - message: i18n::t("relay_adapter.connection_success"), - error: None, - }) - } else { - let error_msg = data.get("message").and_then(|v| v.as_str()).unwrap_or("Unknown error"); - Ok(ConnectionTestResult { - success: false, - response_time: Some(response_time), - message: i18n::t("relay_adapter.api_error"), - error: Some(error_msg.to_string()), - }) - } - } - Err(e) => Ok(ConnectionTestResult { - success: false, - response_time: Some(response_time), - message: i18n::t("relay_adapter.parse_error"), - error: Some(e.to_string()), - }) - } - } else { - Ok(ConnectionTestResult { - success: false, - response_time: Some(response_time), - message: i18n::t("relay_adapter.http_error"), - error: Some(format!("HTTP {}", response.status())), - }) - } - } - Err(e) => { - let response_time = start_time.elapsed().as_millis() as u64; - Ok(ConnectionTestResult { - success: false, - response_time: Some(response_time), - message: i18n::t("relay_adapter.network_error"), - error: Some(e.to_string()), - }) - } - } - } - - async fn get_usage_logs(&self, station: &RelayStation, user_id: &str, page: Option, size: Option) -> Result { - let page = page.unwrap_or(1); - let size = size.unwrap_or(10); - let url = format!("{}/api/log/self?page={}&size={}", - station.api_url.trim_end_matches('/'), page, size); - - let response = HTTP_CLIENT - .get(&url) - .header("Authorization", format!("Bearer {}", station.system_token)) - .header("New-API-User", user_id) - .send() - .await?; - - let data: Value = response.json().await?; - - if !data.get("success").and_then(|v| v.as_bool()).unwrap_or(false) { - return Err(anyhow::anyhow!("API Error: {}", - data.get("message").and_then(|v| v.as_str()).unwrap_or("Unknown error"))); - } - - Ok(data.get("data").cloned().unwrap_or(json!([]))) - } - - async fn list_tokens(&self, station: &RelayStation, page: Option, size: Option) -> Result { - let page = page.unwrap_or(1); - let size = size.unwrap_or(10); - let url = format!("{}/api/token?page={}&size={}", - station.api_url.trim_end_matches('/'), page, size); - - let response = HTTP_CLIENT - .get(&url) - .header("Authorization", format!("Bearer {}", station.system_token)) - .send() - .await?; - - let data: Value = response.json().await?; - - if !data.get("success").and_then(|v| v.as_bool()).unwrap_or(false) { - return Err(anyhow::anyhow!("API Error: {}", - data.get("message").and_then(|v| v.as_str()).unwrap_or("Unknown error"))); - } - - let data = data.get("data").ok_or_else(|| anyhow!("No data returned"))?; - let tokens_data = data.get("data").and_then(|v| v.as_array()) - .ok_or_else(|| anyhow!("Invalid response format: data is not an array"))?; - - let tokens: Result, _> = tokens_data.iter() - .map(|token| { - Ok::(TokenInfo { - id: token.get("id") - .and_then(|v| v.as_i64()) - .map(|id| id.to_string()) - .ok_or_else(|| anyhow::anyhow!("Missing token id"))?, - name: token.get("name") - .and_then(|v| v.as_str()) - .unwrap_or("Unnamed Token") - .to_string(), - token: token.get("key") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(), - quota: token.get("remain_quota") - .and_then(|v| v.as_i64()), - used_quota: token.get("used_quota") - .and_then(|v| v.as_i64()), - status: match token.get("status").and_then(|v| v.as_i64()) { - Some(1) => "active".to_string(), - Some(0) => "disabled".to_string(), - _ => "unknown".to_string(), - }, - created_at: token.get("created_time") - .and_then(|v| v.as_i64()) - .unwrap_or(0), - updated_at: token.get("updated_time") - .and_then(|v| v.as_i64()) - .unwrap_or(0), - }) - }) - .collect(); - - let tokens = tokens?; - let total = data.get("total").and_then(|v| v.as_i64()).unwrap_or(0); - let has_more = (page * size) < total as usize; - - Ok(TokenPaginationResponse { - tokens, - total, - page, - size, - has_more, - }) - } - - async fn create_token(&self, station: &RelayStation, name: &str, quota: Option) -> Result { - let url = format!("{}/api/token", station.api_url.trim_end_matches('/')); - - let mut body = json!({ - "name": name, - "unlimited_quota": quota.is_none(), - }); - - if let Some(q) = quota { - body["remain_quota"] = json!(q); - } - - let response = HTTP_CLIENT - .post(&url) - .header("Authorization", format!("Bearer {}", station.system_token)) - .header("Content-Type", "application/json") - .json(&body) - .send() - .await?; - - let data: Value = response.json().await?; - - if !data.get("success").and_then(|v| v.as_bool()).unwrap_or(false) { - return Err(anyhow::anyhow!("API Error: {}", - data.get("message").and_then(|v| v.as_str()).unwrap_or("Unknown error"))); - } - - let token_data = data.get("data").ok_or_else(|| anyhow!("No token data returned"))?; - - Ok(TokenInfo { - id: token_data.get("id") - .and_then(|v| v.as_i64()) - .map(|id| id.to_string()) - .ok_or_else(|| anyhow!("Missing token id"))?, - name: name.to_string(), - token: token_data.get("key") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(), - quota, - used_quota: Some(0), - status: "active".to_string(), - created_at: chrono::Utc::now().timestamp(), - updated_at: chrono::Utc::now().timestamp(), - }) - } - - async fn update_token(&self, station: &RelayStation, token_id: &str, name: Option<&str>, quota: Option) -> Result { - let url = format!("{}/api/token", station.api_url.trim_end_matches('/')); - - let mut body = json!({ - "id": token_id.parse::() - .map_err(|_| anyhow!("Invalid token ID format"))?, - }); - - if let Some(n) = name { - body["name"] = json!(n); - } - - if let Some(q) = quota { - body["remain_quota"] = json!(q); - body["unlimited_quota"] = json!(false); - } - - let response = HTTP_CLIENT - .put(&url) - .header("Authorization", format!("Bearer {}", station.system_token)) - .header("Content-Type", "application/json") - .json(&body) - .send() - .await?; - - let data: Value = response.json().await?; - - if !data.get("success").and_then(|v| v.as_bool()).unwrap_or(false) { - return Err(anyhow::anyhow!("API Error: {}", - data.get("message").and_then(|v| v.as_str()).unwrap_or("Unknown error"))); - } - - Ok(TokenInfo { - id: token_id.to_string(), - name: name.unwrap_or("Updated Token").to_string(), - token: "".to_string(), // 更新后不返回完整token - quota, - used_quota: None, - status: "active".to_string(), - created_at: 0, - updated_at: chrono::Utc::now().timestamp(), - }) - } - - async fn delete_token(&self, station: &RelayStation, token_id: &str) -> Result { - let url = format!("{}/api/token/{}", station.api_url.trim_end_matches('/'), token_id); - - let response = HTTP_CLIENT - .delete(&url) - .header("Authorization", format!("Bearer {}", station.system_token)) - .send() - .await?; - - let data: Value = response.json().await?; - - if !data.get("success").and_then(|v| v.as_bool()).unwrap_or(false) { - return Err(anyhow::anyhow!("API Error: {}", - data.get("message").and_then(|v| v.as_str()).unwrap_or("Unknown error"))); - } - - Ok(i18n::t("relay_adapter.token_deleted")) - } -} - -/// YourAPI 适配器(基于 NewAPI 的优化版本) -pub struct YourApiAdapter { - newapi: NewApiAdapter, -} - -impl YourApiAdapter { - pub fn new() -> Self { - Self { - newapi: NewApiAdapter, - } - } -} - -#[async_trait] -impl StationAdapter for YourApiAdapter { - async fn get_station_info(&self, station: &RelayStation) -> Result { - // 复用 NewAPI 的实现,但修改适配器类型 - let mut info = self.newapi.get_station_info(station).await?; - if let Some(ref mut metadata) = info.metadata { - metadata.insert("adapter_type".to_string(), json!("yourapi")); - } - Ok(info) - } - - async fn get_user_info(&self, station: &RelayStation, user_id: &str) -> Result { - self.newapi.get_user_info(station, user_id).await - } - - async fn test_connection(&self, station: &RelayStation) -> Result { - self.newapi.test_connection(station).await - } - - async fn get_usage_logs(&self, station: &RelayStation, user_id: &str, page: Option, size: Option) -> Result { - self.newapi.get_usage_logs(station, user_id, page, size).await - } - - async fn list_tokens(&self, station: &RelayStation, page: Option, size: Option) -> Result { - // YourAPI 特定的 Token 列表实现 - let page = page.unwrap_or(1); - let size = size.unwrap_or(10); - let url = format!("{}/api/token?page={}&size={}", - station.api_url.trim_end_matches('/'), page, size); - - let response = HTTP_CLIENT - .get(&url) - .header("Authorization", format!("Bearer {}", station.system_token)) - .send() - .await?; - - let data: Value = response.json().await?; - - if !data.get("success").and_then(|v| v.as_bool()).unwrap_or(false) { - return Err(anyhow::anyhow!("API Error: {}", - data.get("message").and_then(|v| v.as_str()).unwrap_or("Unknown error"))); - } - - // YourAPI 返回直接数组而非嵌套对象 - let tokens_data = data["data"].as_array() - .ok_or_else(|| anyhow!("Invalid response format: data is not an array"))?; - - let tokens: Result, _> = tokens_data.iter() - .map(|token| { - Ok::(TokenInfo { - id: token.get("id") - .and_then(|v| v.as_i64()) - .map(|id| id.to_string()) - .ok_or_else(|| anyhow::anyhow!("Missing token id"))?, - name: token.get("name") - .and_then(|v| v.as_str()) - .unwrap_or("Unnamed Token") - .to_string(), - token: token.get("key") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(), - quota: token.get("remain_quota") - .and_then(|v| v.as_i64()), - used_quota: token.get("used_quota") - .and_then(|v| v.as_i64()), - status: match token.get("status").and_then(|v| v.as_i64()) { - Some(1) => "active".to_string(), - Some(0) => "disabled".to_string(), - _ => "unknown".to_string(), - }, - created_at: token.get("created_time") - .and_then(|v| v.as_i64()) - .unwrap_or(0), - updated_at: token.get("updated_time") - .and_then(|v| v.as_i64()) - .unwrap_or(0), - }) - }) - .collect(); - - let tokens = tokens?; - let items_len = tokens.len(); - - // YourAPI 的智能分页估算 - let has_more_pages = items_len == size; - let estimated_total = if page == 1 && !has_more_pages { - items_len as i64 - } else if has_more_pages { - (page * size + 1) as i64 // 保守估计 - } else { - ((page - 1) * size + items_len) as i64 - }; - - Ok(TokenPaginationResponse { - tokens, - total: estimated_total, - page, - size, - has_more: has_more_pages, - }) - } - - async fn create_token(&self, station: &RelayStation, name: &str, quota: Option) -> Result { - self.newapi.create_token(station, name, quota).await - } - - async fn update_token(&self, station: &RelayStation, token_id: &str, name: Option<&str>, quota: Option) -> Result { - self.newapi.update_token(station, token_id, name, quota).await - } - - async fn delete_token(&self, station: &RelayStation, token_id: &str) -> Result { - self.newapi.delete_token(station, token_id).await - } -} - /// Custom 适配器(简化版本,仅提供基本信息) pub struct CustomAdapter; @@ -792,29 +250,69 @@ impl StationAdapter for CustomAdapter { metadata: Some({ let mut map = HashMap::new(); map.insert("adapter_type".to_string(), json!("custom")); - map.insert("note".to_string(), json!("This is a custom configuration that only provides URL and API key.")); map }), quota_per_unit: None, }) } - async fn get_user_info(&self, _station: &RelayStation, _user_id: &str) -> Result { - Err(anyhow::anyhow!(i18n::t("relay_adapter.user_info_not_available"))) - } - - async fn test_connection(&self, _station: &RelayStation) -> Result { - // Custom 适配器跳过连接测试,直接返回成功 - Ok(ConnectionTestResult { - success: true, - response_time: None, // 不显示响应时间 - message: i18n::t("relay_adapter.custom_no_test"), - error: 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 { - Err(anyhow::anyhow!(i18n::t("relay_adapter.usage_logs_not_available"))) + Ok(json!({ + "logs": [], + "message": "自定义适配器暂不支持使用日志查询" + })) } async fn list_tokens(&self, _station: &RelayStation, _page: Option, _size: Option) -> Result { @@ -838,14 +336,11 @@ impl StationAdapter for CustomAdapter { pub fn create_adapter(adapter_type: &RelayStationAdapter) -> Box { match adapter_type { RelayStationAdapter::Packycode => Box::new(PackycodeAdapter), - // DeepSeek、GLM、Qwen、Kimi 都使用类似 NewAPI 的适配器 - RelayStationAdapter::Deepseek => Box::new(NewApiAdapter), - RelayStationAdapter::Glm => Box::new(NewApiAdapter), - RelayStationAdapter::Qwen => Box::new(NewApiAdapter), - RelayStationAdapter::Kimi => Box::new(NewApiAdapter), - RelayStationAdapter::Newapi => Box::new(NewApiAdapter), - RelayStationAdapter::Oneapi => Box::new(NewApiAdapter), // OneAPI 兼容 NewAPI - RelayStationAdapter::Yourapi => Box::new(YourApiAdapter::new()), + // 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), } } @@ -997,46 +492,28 @@ pub async fn relay_station_delete_token( /// 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: String, // 计划过期时间 - pub username: Option, // 用户名 - pub email: Option, // 邮箱 - pub opus_enabled: Option, // 是否启用Opus模型 + 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 用户信息(额度等) +/// 获取 PackyCode 用户额度(专用) #[command] -pub async fn packycode_get_user_quota(station_id: String, db: State<'_, AgentDb>) -> Result { - // 先从数据库获取中转站信息,然后释放锁 - let station = { - let conn = db.0.lock().map_err(|e| { - log::error!("Failed to acquire database lock: {}", e); - i18n::t("database.lock_failed") - })?; - - // 获取中转站信息 - let mut stmt = conn.prepare("SELECT * FROM relay_stations WHERE id = ?1") - .map_err(|e| { - log::error!("Failed to prepare statement: {}", e); - i18n::t("database.query_failed") - })?; - - stmt.query_row([&station_id], |row| { - use crate::commands::relay_stations::RelayStation; - RelayStation::from_row(row) - }).map_err(|e| { - log::error!("Failed to get relay station: {}", e); - i18n::t("relay_station.not_found") - })? - }; // 这里释放数据库连接 +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))?; - // 只有 PackyCode 适配器支持此功能 if station.adapter.as_str() != "packycode" { return Err("此功能仅支持 PackyCode 中转站".to_string()); } @@ -1050,7 +527,6 @@ pub async fn packycode_get_user_quota(station_id: String, db: State<'_, AgentDb> "https://www.packycode.com/api/backend/users/info" }; - // 创建 HTTP 客户端 - 禁用系统代理 let client = Client::builder() .timeout(Duration::from_secs(30)) .no_proxy() // 禁用所有代理 @@ -1058,9 +534,7 @@ pub async fn packycode_get_user_quota(station_id: String, db: State<'_, AgentDb> .map_err(|e| format!("创建 HTTP 客户端失败: {}", e))?; log::info!("正在请求 PackyCode 用户信息: {}", url); - log::debug!("使用 Token: {}...", &station.system_token[..10.min(station.system_token.len())]); - // 发送请求 - 只使用必要的请求头 let response = client .get(url) .header("Authorization", format!("Bearer {}", station.system_token)) @@ -1070,23 +544,18 @@ pub async fn packycode_get_user_quota(station_id: String, db: State<'_, AgentDb> .await .map_err(|e| { log::error!("请求 PackyCode API 失败: {}", e); - // 提供更详细的错误信息 if e.is_connect() { format!("网络连接失败: {}", e) } else if e.is_timeout() { format!("请求超时: {}", e) - } else if e.is_request() { - 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(), @@ -1094,112 +563,46 @@ pub async fn packycode_get_user_quota(station_id: String, db: State<'_, AgentDb> _ => format!("请求失败 ({}): {}", status, error_text), }); } - - // 解析响应 - let response_data: serde_json::Value = response.json().await + + let data: Value = response.json().await .map_err(|e| format!("解析响应失败: {}", e))?; - // 提取额度信息 - let quota = PackycodeUserQuota { - daily_budget_usd: response_data.get("daily_budget_usd") - .and_then(|v| { - if v.is_string() { - v.as_str().and_then(|s| s.parse::().ok()) - } else if v.is_f64() { - v.as_f64() - } else if v.is_i64() { - v.as_i64().map(|i| i as f64) - } else { - None - } - }) - .unwrap_or(0.0), - daily_spent_usd: response_data.get("daily_spent_usd") - .and_then(|v| { - if v.is_null() { - Some(0.0) - } else if v.is_string() { - v.as_str().and_then(|s| s.parse::().ok()) - } else if v.is_f64() { - v.as_f64() - } else if v.is_i64() { - v.as_i64().map(|i| i as f64) - } else { - None - } - }) - .unwrap_or(0.0), - monthly_budget_usd: response_data.get("monthly_budget_usd") - .and_then(|v| { - if v.is_string() { - v.as_str().and_then(|s| s.parse::().ok()) - } else if v.is_f64() { - v.as_f64() - } else if v.is_i64() { - v.as_i64().map(|i| i as f64) - } else { - None - } - }) - .unwrap_or(0.0), - monthly_spent_usd: response_data.get("monthly_spent_usd") - .and_then(|v| { - if v.is_null() { - Some(0.0) - } else if v.is_string() { - v.as_str().and_then(|s| s.parse::().ok()) - } else if v.is_f64() { - v.as_f64() - } else if v.is_i64() { - v.as_i64().map(|i| i as f64) - } else { - None - } - }) - .unwrap_or(0.0), - balance_usd: response_data.get("balance_usd") - .and_then(|v| { - if v.is_string() { - v.as_str().and_then(|s| s.parse::().ok()) - } else if v.is_f64() { - v.as_f64() - } else if v.is_i64() { - v.as_i64().map(|i| i as f64) - } else { - None - } - }) - .unwrap_or(0.0), - total_spent_usd: response_data.get("total_spent_usd") - .and_then(|v| { - if v.is_string() { - v.as_str().and_then(|s| s.parse::().ok()) - } else if v.is_f64() { - v.as_f64() - } else if v.is_i64() { - v.as_i64().map(|i| i as f64) - } else { - None - } - }) - .unwrap_or(0.0), - plan_type: response_data.get("plan_type") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - .unwrap_or_else(|| "unknown".to_string()), - plan_expires_at: response_data.get("plan_expires_at") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - .unwrap_or_else(|| "".to_string()), - username: response_data.get("username") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()), - email: response_data.get("email") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()), - opus_enabled: response_data.get("opus_enabled") - .and_then(|v| v.as_bool()), + // 辅助函数:将值转换为 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(quota) + 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()), + }) } \ No newline at end of file diff --git a/src-tauri/src/commands/relay_stations.rs b/src-tauri/src/commands/relay_stations.rs index 10b6a96..98da128 100644 --- a/src-tauri/src/commands/relay_stations.rs +++ b/src-tauri/src/commands/relay_stations.rs @@ -19,9 +19,6 @@ pub enum RelayStationAdapter { Glm, // 智谱GLM Qwen, // 千问Qwen Kimi, // Kimi k2 - Newapi, // NewAPI 兼容平台 - Oneapi, // OneAPI 兼容平台 - Yourapi, // YourAPI 特定平台 Custom, // 自定义简单配置 } @@ -33,9 +30,6 @@ impl RelayStationAdapter { RelayStationAdapter::Glm => "glm", RelayStationAdapter::Qwen => "qwen", RelayStationAdapter::Kimi => "kimi", - RelayStationAdapter::Newapi => "newapi", - RelayStationAdapter::Oneapi => "oneapi", - RelayStationAdapter::Yourapi => "yourapi", RelayStationAdapter::Custom => "custom", } } @@ -60,7 +54,7 @@ pub struct RelayStation { pub adapter: RelayStationAdapter, // 适配器类型 pub auth_method: AuthMethod, // 认证方式 pub system_token: String, // 系统令牌 - pub user_id: Option, // 用户 ID(NewAPI 必需) + pub user_id: Option, // 用户 ID(可选) pub adapter_config: Option>, // 适配器特定配置 pub enabled: bool, // 启用状态 pub created_at: i64, // 创建时间 diff --git a/src/components/RelayStationManager.tsx b/src/components/RelayStationManager.tsx index 75c326b..fdf6474 100644 --- a/src/components/RelayStationManager.tsx +++ b/src/components/RelayStationManager.tsx @@ -73,6 +73,17 @@ const RelayStationManager: React.FC = ({ onBack }) => const { t } = useTranslation(); + // Token 脱敏函数 + const maskToken = (token: string): string => { + if (!token || token.length <= 8) { + return '*'.repeat(token?.length || 0); + } + const start = token.substring(0, 4); + const end = token.substring(token.length - 4); + const middleLength = Math.max(token.length - 8, 8); + return `${start}${'*'.repeat(middleLength)}${end}`; + }; + // 显示Toast const showToast = (message: string, type: "success" | "error" = "success") => { setToast({ message, type }); @@ -98,13 +109,13 @@ const RelayStationManager: React.FC = ({ onBack }) => setLoadingConfig(true); // 读取完整的 ~/.claude/settings.json 文件 const settings = await api.getClaudeSettings(); - + // 保存配置用于简单视图显示 setCurrentConfig({ api_url: settings.env?.ANTHROPIC_BASE_URL || '', api_token: settings.env?.ANTHROPIC_AUTH_TOKEN || '' }); - + // 格式化完整的JSON字符串 setConfigJson(JSON.stringify(settings, null, 2)); } catch (error) { @@ -140,10 +151,10 @@ const RelayStationManager: React.FC = ({ onBack }) => setSavingConfig(true); // 验证JSON格式 const parsedConfig = JSON.parse(configJson); - + // 保存配置到 ~/.claude/settings.json await api.saveClaudeSettings(parsedConfig); - + showToast(t('relayStation.configSaved'), "success"); setEditingConfig(false); loadCurrentConfig(); @@ -218,9 +229,6 @@ const RelayStationManager: React.FC = ({ onBack }) => case 'glm': return '智谱GLM'; case 'qwen': return '千问Qwen'; case 'kimi': return 'Kimi k2'; - case 'newapi': return 'NewAPI'; - case 'oneapi': return 'OneAPI'; - case 'yourapi': return 'YourAPI'; case 'custom': return t('relayStation.custom'); default: return adapter; } @@ -322,27 +330,9 @@ const RelayStationManager: React.FC = ({ onBack }) => {/* 当前配置状态 */} -
-
- - {t('relayStation.currentConfig')} -
- +
+ + {t('relayStation.currentConfig')}
@@ -422,49 +412,71 @@ const RelayStationManager: React.FC = ({ onBack }) =>
) : ( -
-
- {t('relayStation.configPreview')} -
- - +
+ {/* 左侧数据展示 */} +
+
{t('relayStation.configPreview')}
+
+
+ API URL: + + {currentConfig.api_url || t('relayStation.notConfigured')} + +
+
+ API Token: + + {currentConfig.api_token ? maskToken(currentConfig.api_token) : t('relayStation.notConfigured')} + +
+
+ {t('relayStation.configLocation')}: ~/.claude/settings.json +
-
-
- API URL: - - {currentConfig.api_url || t('relayStation.notConfigured')} - -
-
- API Token: - - {currentConfig.api_token || t('relayStation.notConfigured')} - -
-
- {t('relayStation.configLocation')}: ~/.claude/settings.json -
+ + {/* 右侧按钮区域 */} +
+ + +
)} @@ -542,7 +554,7 @@ const RelayStationManager: React.FC = ({ onBack }) =>
{quotaData[station.id].plan_expires_at && ( - 到期: {new Date(quotaData[station.id].plan_expires_at).toLocaleDateString()} + 到期: {new Date(quotaData[station.id].plan_expires_at!).toLocaleDateString()} )}
@@ -561,7 +573,7 @@ const RelayStationManager: React.FC = ({ onBack }) => 日额度:
{(() => { - const daily_spent = Number(quotaData[station.id].daily_spent_usd || 0); + const daily_spent = Number(quotaData[station.id].daily_spent_usd); const daily_budget = Number(quotaData[station.id].daily_budget_usd); return ( <> @@ -579,14 +591,14 @@ const RelayStationManager: React.FC = ({ onBack }) =>
{ - const daily_spent = Number(quotaData[station.id].daily_spent_usd || 0); + const daily_spent = Number(quotaData[station.id].daily_spent_usd); const daily_budget = Number(quotaData[station.id].daily_budget_usd); return daily_spent / daily_budget > 0.8; })() ? 'bg-orange-500' : 'bg-green-500' }`} style={{ width: `${Math.min( (() => { - const daily_spent = Number(quotaData[station.id].daily_spent_usd || 0); + const daily_spent = Number(quotaData[station.id].daily_spent_usd); const daily_budget = Number(quotaData[station.id].daily_budget_usd); return (daily_spent / daily_budget) * 100; })(), 100)}%` }} @@ -600,7 +612,7 @@ const RelayStationManager: React.FC = ({ onBack }) => 月额度:
{(() => { - const monthly_spent = Number(quotaData[station.id].monthly_spent_usd || 0); + const monthly_spent = Number(quotaData[station.id].monthly_spent_usd); const monthly_budget = Number(quotaData[station.id].monthly_budget_usd); return ( <> @@ -618,14 +630,14 @@ const RelayStationManager: React.FC = ({ onBack }) =>
{ - const monthly_spent = Number(quotaData[station.id].monthly_spent_usd || 0); + const monthly_spent = Number(quotaData[station.id].monthly_spent_usd); const monthly_budget = Number(quotaData[station.id].monthly_budget_usd); return monthly_spent / monthly_budget > 0.8; })() ? 'bg-orange-500' : 'bg-green-500' }`} style={{ width: `${Math.min( (() => { - const monthly_spent = Number(quotaData[station.id].monthly_spent_usd || 0); + const monthly_spent = Number(quotaData[station.id].monthly_spent_usd); const monthly_budget = Number(quotaData[station.id].monthly_budget_usd); return (monthly_spent / monthly_budget) * 100; })(), 100)}%` }} @@ -786,7 +798,7 @@ const CreateStationDialog: React.FC<{ const [packycodeService, setPackycodeService] = useState('bus'); // 默认公交车 const [packycodeNode, setPackycodeNode] = useState('https://api.packycode.com'); // 默认节点(公交车用) const [packycodeTaxiNode, setPackycodeTaxiNode] = useState('https://share-api.packycode.com'); // 滴滴车节点 - + // 测速弹出框状态 const [showSpeedTestModal, setShowSpeedTestModal] = useState(false); const [speedTestResults, setSpeedTestResults] = useState<{ url: string; name: string; responseTime: number | null; status: 'testing' | 'success' | 'failed' }[]>([]); @@ -828,7 +840,7 @@ const CreateStationDialog: React.FC<{ const performSpeedTest = async (nodes: { url: string; name: string }[], onComplete: (bestNode: { url: string; name: string }) => void) => { setShowSpeedTestModal(true); setSpeedTestInProgress(true); - + // 初始化测速结果 const initialResults = nodes.map(node => ({ url: node.url, @@ -837,42 +849,41 @@ const CreateStationDialog: React.FC<{ status: 'testing' as const })); setSpeedTestResults(initialResults); - + let bestNode = nodes[0]; let minTime = Infinity; - + // 并行测试所有节点 const testPromises = nodes.map(async (node, index) => { try { const startTime = Date.now(); - await fetch(node.url, { + await fetch(node.url, { method: 'HEAD', - timeout: 5000, mode: 'no-cors' }); const responseTime = Date.now() - startTime; - + // 更新单个节点的测试结果 - setSpeedTestResults(prev => prev.map((result, i) => + setSpeedTestResults(prev => prev.map((result, i) => i === index ? { ...result, responseTime, status: 'success' } : result )); - + if (responseTime < minTime) { minTime = responseTime; bestNode = node; } - + return { node, responseTime }; } catch (error) { console.log(`Node ${node.url} failed:`, error); // 标记节点为失败 - setSpeedTestResults(prev => prev.map((result, i) => + setSpeedTestResults(prev => prev.map((result, i) => i === index ? { ...result, responseTime: null, status: 'failed' } : result )); return { node, responseTime: null }; } }); - + try { await Promise.all(testPromises); // 测试完成后等待2秒让用户看到结果 @@ -898,10 +909,7 @@ const CreateStationDialog: React.FC<{ if (formData.adapter === 'packycode') { setFormData(prev => ({ ...prev, - auth_method: 'api_key', // PackyCode 固定使用 API Key - api_url: packycodeService === 'taxi' - ? packycodeTaxiNode - : packycodeNode + auth_method: 'api_key' // PackyCode 固定使用 API Key })); } else if (formData.adapter === 'custom') { setFormData(prev => ({ @@ -914,7 +922,7 @@ const CreateStationDialog: React.FC<{ auth_method: 'bearer_token' })); } - }, [formData.adapter, packycodeService, packycodeNode, packycodeTaxiNode]); + }, [formData.adapter]); // 自动填充中转站名称 const fillStationName = (serviceType: string) => { @@ -979,8 +987,8 @@ const CreateStationDialog: React.FC<{ ? 'bg-blue-600 hover:bg-blue-700 text-white border-2 border-blue-700' : 'hover:bg-blue-50 dark:hover:bg-blue-950 border-2 border-transparent' }`} - onClick={() => setFormData(prev => ({ - ...prev, + onClick={() => setFormData(prev => ({ + ...prev, adapter: 'packycode', name: 'PackyCode', api_url: 'https://api.packycode.com' @@ -1001,8 +1009,8 @@ const CreateStationDialog: React.FC<{ ? 'bg-indigo-600 hover:bg-indigo-700 text-white border-2 border-indigo-700' : 'hover:bg-indigo-50 dark:hover:bg-indigo-950 border-2 border-transparent' }`} - onClick={() => setFormData(prev => ({ - ...prev, + onClick={() => setFormData(prev => ({ + ...prev, adapter: 'deepseek', name: 'DeepSeek v3.1', api_url: 'https://api.deepseek.com/anthropic' @@ -1023,8 +1031,8 @@ const CreateStationDialog: React.FC<{ ? 'bg-cyan-600 hover:bg-cyan-700 text-white border-2 border-cyan-700' : 'hover:bg-cyan-50 dark:hover:bg-cyan-950 border-2 border-transparent' }`} - onClick={() => setFormData(prev => ({ - ...prev, + onClick={() => setFormData(prev => ({ + ...prev, adapter: 'glm', name: '智谱GLM', api_url: 'https://open.bigmodel.cn/api/anthropic' @@ -1046,8 +1054,8 @@ const CreateStationDialog: React.FC<{ ? 'bg-amber-600 hover:bg-amber-700 text-white border-2 border-amber-700' : 'hover:bg-amber-50 dark:hover:bg-amber-950 border-2 border-transparent' }`} - onClick={() => setFormData(prev => ({ - ...prev, + onClick={() => setFormData(prev => ({ + ...prev, adapter: 'qwen', name: '千问Qwen', api_url: 'https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy' @@ -1068,8 +1076,8 @@ const CreateStationDialog: React.FC<{ ? 'bg-violet-600 hover:bg-violet-700 text-white border-2 border-violet-700' : 'hover:bg-violet-50 dark:hover:bg-violet-950 border-2 border-transparent' }`} - onClick={() => setFormData(prev => ({ - ...prev, + onClick={() => setFormData(prev => ({ + ...prev, adapter: 'kimi', name: 'Kimi k2', api_url: 'https://api.moonshot.cn/anthropic' @@ -1082,69 +1090,6 @@ const CreateStationDialog: React.FC<{
- - - {/* 第三行:其他适配器 */} - - - - - - {/* 第三行:其他适配器 */} - - -
- {(formData.adapter === 'newapi' || formData.adapter === 'oneapi') && ( -
- - setFormData(prev => ({ ...prev, user_id: e.target.value }))} - placeholder={t('relayStation.userIdPlaceholder')} - className="w-full" - /> -
- )}
diff --git a/src/lib/api.ts b/src/lib/api.ts index 808a8c4..947fe3c 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -457,9 +457,6 @@ export type RelayStationAdapter = | 'glm' // 智谱GLM | 'qwen' // 千问Qwen | 'kimi' // Kimi k2 - | 'newapi' // NewAPI 兼容平台 - | 'oneapi' // OneAPI 兼容平台 - | 'yourapi' // YourAPI 特定平台 | 'custom'; // 自定义简单配置 /** 认证方式 */ @@ -477,7 +474,7 @@ export interface RelayStation { adapter: RelayStationAdapter; // 适配器类型 auth_method: AuthMethod; // 认证方式 system_token: string; // 系统令牌 - user_id?: string; // 用户 ID(NewAPI 必需) + user_id?: string; // 用户 ID(可选) adapter_config?: Record; // 适配器特定配置 enabled: boolean; // 启用状态 created_at: number; // 创建时间 @@ -589,17 +586,17 @@ export interface NodeSpeedTestResult { /** PackyCode 用户额度信息 */ export interface PackycodeUserQuota { - daily_budget_usd: string | number; // 日预算(美元) - daily_spent_usd: string | number | null; // 日已使用(美元) - monthly_budget_usd: string | number; // 月预算(美元) - monthly_spent_usd: string | number | null; // 月已使用(美元) - balance_usd: string | number; // 账户余额(美元) - total_spent_usd: string | number; // 总消费(美元) - plan_type: string; // 计划类型 (pro, basic, etc.) - plan_expires_at: string; // 计划过期时间 - username?: string; // 用户名 - email?: string; // 邮箱 - opus_enabled?: boolean; // 是否启用Opus模型 + daily_budget_usd: number; // 日预算(美元) + daily_spent_usd: number; // 日已使用(美元) + monthly_budget_usd: number; // 月预算(美元) + monthly_spent_usd: number; // 月已使用(美元) + balance_usd: number; // 账户余额(美元) + total_spent_usd: number; // 总消费(美元) + plan_type: string; // 计划类型 (pro, basic, etc.) + plan_expires_at?: string; // 计划过期时间 + username?: string; // 用户名 + email?: string; // 邮箱 + opus_enabled?: boolean; // 是否启用Opus模型 } /** diff --git a/src/locales/en/common.json b/src/locales/en/common.json index 4336f0c..1adbac5 100644 --- a/src/locales/en/common.json +++ b/src/locales/en/common.json @@ -867,7 +867,7 @@ "tokenPlaceholder": "Enter your API token", "tokenRequired": "System token is required", "userId": "User ID", - "userIdPlaceholder": "Required for NewAPI/OneAPI", + "userIdPlaceholder": "Optional", "enabled": "Enabled", "testConnection": "Test Connection", "connectionSuccess": "Connection successful", diff --git a/src/locales/zh/common.json b/src/locales/zh/common.json index a13d046..b0720d6 100644 --- a/src/locales/zh/common.json +++ b/src/locales/zh/common.json @@ -794,7 +794,7 @@ "tokenPlaceholder": "输入您的 API 令牌", "tokenRequired": "系统令牌必填", "userId": "用户 ID", - "userIdPlaceholder": "NewAPI/OneAPI 必需", + "userIdPlaceholder": "可选", "enabled": "启用", "testConnection": "测试连接", "connectionSuccess": "连接成功",