增加配置转换

This commit is contained in:
2025-08-07 23:46:55 +08:00
parent 5910362683
commit ca56cc83f0
16 changed files with 2837 additions and 2 deletions

View File

@@ -0,0 +1,757 @@
use async_trait::async_trait;
use anyhow::{anyhow, Result};
use reqwest::Client;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::time::{Duration, Instant};
use tauri::{command, State};
use crate::commands::agents::AgentDb;
use crate::commands::relay_stations::{
RelayStation, StationInfo, UserInfo, ConnectionTestResult,
TokenInfo, TokenPaginationResponse, RelayStationAdapter
};
use crate::i18n;
/// HTTP 客户端单例
static HTTP_CLIENT: once_cell::sync::Lazy<Client> = once_cell::sync::Lazy::new(|| {
Client::builder()
.timeout(Duration::from_secs(30))
.pool_max_idle_per_host(10)
.pool_idle_timeout(Duration::from_secs(90))
.build()
.unwrap()
});
/// 中转站适配器 trait
#[async_trait]
pub trait StationAdapter: Send + Sync {
/// 获取站点信息
async fn get_station_info(&self, station: &RelayStation) -> Result<StationInfo>;
/// 获取用户信息
async fn get_user_info(&self, station: &RelayStation, user_id: &str) -> Result<UserInfo>;
/// 测试连接
async fn test_connection(&self, station: &RelayStation) -> Result<ConnectionTestResult>;
/// 获取用户使用日志
async fn get_usage_logs(&self, station: &RelayStation, user_id: &str, page: Option<usize>, size: Option<usize>) -> Result<Value>;
/// 列出用户 Token
async fn list_tokens(&self, station: &RelayStation, page: Option<usize>, size: Option<usize>) -> Result<TokenPaginationResponse>;
/// 创建 Token
async fn create_token(&self, station: &RelayStation, name: &str, quota: Option<i64>) -> Result<TokenInfo>;
/// 更新 Token
async fn update_token(&self, station: &RelayStation, token_id: &str, name: Option<&str>, quota: Option<i64>) -> Result<TokenInfo>;
/// 删除 Token
async fn delete_token(&self, station: &RelayStation, token_id: &str) -> Result<String>;
}
/// NewAPI 适配器(支持 NewAPI 和 OneAPI
pub struct NewApiAdapter;
#[async_trait]
impl StationAdapter for NewApiAdapter {
async fn get_station_info(&self, station: &RelayStation) -> Result<StationInfo> {
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<UserInfo> {
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<ConnectionTestResult> {
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::<Value>().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<usize>, size: Option<usize>) -> Result<Value> {
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<usize>, size: Option<usize>) -> Result<TokenPaginationResponse> {
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<Vec<TokenInfo>, _> = tokens_data.iter()
.map(|token| {
Ok::<TokenInfo, anyhow::Error>(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<i64>) -> Result<TokenInfo> {
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<i64>) -> Result<TokenInfo> {
let url = format!("{}/api/token", station.api_url.trim_end_matches('/'));
let mut body = json!({
"id": token_id.parse::<i64>()
.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<String> {
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<StationInfo> {
// 复用 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<UserInfo> {
self.newapi.get_user_info(station, user_id).await
}
async fn test_connection(&self, station: &RelayStation) -> Result<ConnectionTestResult> {
self.newapi.test_connection(station).await
}
async fn get_usage_logs(&self, station: &RelayStation, user_id: &str, page: Option<usize>, size: Option<usize>) -> Result<Value> {
self.newapi.get_usage_logs(station, user_id, page, size).await
}
async fn list_tokens(&self, station: &RelayStation, page: Option<usize>, size: Option<usize>) -> Result<TokenPaginationResponse> {
// 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<Vec<TokenInfo>, _> = tokens_data.iter()
.map(|token| {
Ok::<TokenInfo, anyhow::Error>(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<i64>) -> Result<TokenInfo> {
self.newapi.create_token(station, name, quota).await
}
async fn update_token(&self, station: &RelayStation, token_id: &str, name: Option<&str>, quota: Option<i64>) -> Result<TokenInfo> {
self.newapi.update_token(station, token_id, name, quota).await
}
async fn delete_token(&self, station: &RelayStation, token_id: &str) -> Result<String> {
self.newapi.delete_token(station, token_id).await
}
}
/// Custom 适配器(简化版本,仅提供基本信息)
pub struct CustomAdapter;
#[async_trait]
impl StationAdapter for CustomAdapter {
async fn get_station_info(&self, station: &RelayStation) -> Result<StationInfo> {
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.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<UserInfo> {
Err(anyhow::anyhow!(i18n::t("relay_adapter.user_info_not_available")))
}
async fn test_connection(&self, _station: &RelayStation) -> Result<ConnectionTestResult> {
// Custom 适配器跳过连接测试,直接返回成功
Ok(ConnectionTestResult {
success: true,
response_time: Some(0),
message: i18n::t("relay_adapter.custom_no_test"),
error: None,
})
}
async fn get_usage_logs(&self, _station: &RelayStation, _user_id: &str, _page: Option<usize>, _size: Option<usize>) -> Result<Value> {
Err(anyhow::anyhow!(i18n::t("relay_adapter.usage_logs_not_available")))
}
async fn list_tokens(&self, _station: &RelayStation, _page: Option<usize>, _size: Option<usize>) -> Result<TokenPaginationResponse> {
Err(anyhow::anyhow!(i18n::t("relay_adapter.token_management_not_available")))
}
async fn create_token(&self, _station: &RelayStation, _name: &str, _quota: Option<i64>) -> Result<TokenInfo> {
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<i64>) -> Result<TokenInfo> {
Err(anyhow::anyhow!(i18n::t("relay_adapter.token_management_not_available")))
}
async fn delete_token(&self, _station: &RelayStation, _token_id: &str) -> Result<String> {
Err(anyhow::anyhow!(i18n::t("relay_adapter.token_management_not_available")))
}
}
/// 适配器工厂函数
pub fn create_adapter(adapter_type: &RelayStationAdapter) -> Box<dyn StationAdapter> {
match adapter_type {
RelayStationAdapter::Newapi => Box::new(NewApiAdapter),
RelayStationAdapter::Oneapi => Box::new(NewApiAdapter), // OneAPI 兼容 NewAPI
RelayStationAdapter::Yourapi => Box::new(YourApiAdapter::new()),
RelayStationAdapter::Custom => Box::new(CustomAdapter),
}
}
/// 获取中转站信息
#[command]
pub async fn relay_station_get_info(
station_id: String,
db: State<'_, AgentDb>
) -> Result<StationInfo, String> {
// 获取中转站配置
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<UserInfo, String> {
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<ConnectionTestResult, String> {
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<usize>,
size: Option<usize>,
db: State<'_, AgentDb>
) -> Result<Value, String> {
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<usize>,
size: Option<usize>,
db: State<'_, AgentDb>
) -> Result<TokenPaginationResponse, String> {
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<i64>,
db: State<'_, AgentDb>
) -> Result<TokenInfo, String> {
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<String>,
quota: Option<i64>,
db: State<'_, AgentDb>
) -> Result<TokenInfo, String> {
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<String, String> {
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")
})
}