增加配置转换
This commit is contained in:
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@@ -654,6 +654,7 @@ dependencies = [
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"unic-langid",
|
||||
"url",
|
||||
"uuid",
|
||||
"walkdir",
|
||||
"which",
|
||||
|
@@ -57,6 +57,7 @@ fluent = "0.16"
|
||||
fluent-bundle = "0.15"
|
||||
unic-langid = "0.9"
|
||||
once_cell = "1.19"
|
||||
url = "2.5"
|
||||
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
|
@@ -38,6 +38,48 @@ project-not-found = Project not found
|
||||
project-access-denied = Access denied to project
|
||||
session-not-found = Session not found
|
||||
|
||||
# Relay Station Messages
|
||||
relay-station-not-found = Relay station not found
|
||||
relay-station-create-failed = Failed to create relay station
|
||||
relay-station-update-failed = Failed to update relay station
|
||||
relay-station-delete-failed = Failed to delete relay station
|
||||
relay-station-delete-success = Relay station deleted successfully
|
||||
relay-station-name-required = Relay station name is required
|
||||
relay-station-api-url-required = API URL is required
|
||||
relay-station-invalid-url = Invalid URL format
|
||||
relay-station-https-required = Only HTTPS URLs are allowed for security
|
||||
relay-station-token-required = System token is required
|
||||
relay-station-token-too-short = Token is too short (minimum 10 characters)
|
||||
relay-station-token-invalid-chars = Token contains invalid characters
|
||||
relay-station-invalid-adapter = Invalid adapter type
|
||||
relay-station-invalid-auth-method = Invalid authentication method
|
||||
relay-station-invalid-config = Invalid adapter configuration
|
||||
|
||||
# Relay Adapter Messages
|
||||
relay-adapter-connection-success = Connection successful
|
||||
relay-adapter-api-error = API returned error
|
||||
relay-adapter-parse-error = Failed to parse response
|
||||
relay-adapter-http-error = HTTP request failed
|
||||
relay-adapter-network-error = Network connection failed
|
||||
relay-adapter-custom-no-test = Custom configuration, connection test skipped
|
||||
relay-adapter-user-info-not-available = User info not available for this configuration
|
||||
relay-adapter-usage-logs-not-available = Usage logs not available for this configuration
|
||||
relay-adapter-token-management-not-available = Token management not available for this configuration
|
||||
relay-adapter-token-deleted = Token deleted successfully
|
||||
relay-adapter-get-info-failed = Failed to get station information
|
||||
relay-adapter-get-user-info-failed = Failed to get user information
|
||||
relay-adapter-connection-test-failed = Connection test failed
|
||||
relay-adapter-get-usage-logs-failed = Failed to get usage logs
|
||||
relay-adapter-list-tokens-failed = Failed to list tokens
|
||||
relay-adapter-create-token-failed = Failed to create token
|
||||
relay-adapter-update-token-failed = Failed to update token
|
||||
relay-adapter-delete-token-failed = Failed to delete token
|
||||
|
||||
# Database Messages
|
||||
database-lock-failed = Failed to acquire database lock
|
||||
database-init-failed = Failed to initialize database
|
||||
database-query-failed = Database query failed
|
||||
|
||||
# General Messages
|
||||
operation-cancelled = Operation cancelled
|
||||
timeout-error = Operation timed out
|
||||
|
@@ -38,6 +38,48 @@ project-not-found = 未找到项目
|
||||
project-access-denied = 拒绝访问项目
|
||||
session-not-found = 未找到会话
|
||||
|
||||
# 中转站消息
|
||||
relay-station-not-found = 未找到中转站
|
||||
relay-station-create-failed = 创建中转站失败
|
||||
relay-station-update-failed = 更新中转站失败
|
||||
relay-station-delete-failed = 删除中转站失败
|
||||
relay-station-delete-success = 中转站删除成功
|
||||
relay-station-name-required = 中转站名称必填
|
||||
relay-station-api-url-required = API 地址必填
|
||||
relay-station-invalid-url = URL 格式无效
|
||||
relay-station-https-required = 为了安全,仅允许 HTTPS URL
|
||||
relay-station-token-required = 系统令牌必填
|
||||
relay-station-token-too-short = 令牌太短(最少10个字符)
|
||||
relay-station-token-invalid-chars = 令牌包含无效字符
|
||||
relay-station-invalid-adapter = 适配器类型无效
|
||||
relay-station-invalid-auth-method = 认证方式无效
|
||||
relay-station-invalid-config = 适配器配置无效
|
||||
|
||||
# 中转站适配器消息
|
||||
relay-adapter-connection-success = 连接成功
|
||||
relay-adapter-api-error = API 返回错误
|
||||
relay-adapter-parse-error = 解析响应失败
|
||||
relay-adapter-http-error = HTTP 请求失败
|
||||
relay-adapter-network-error = 网络连接失败
|
||||
relay-adapter-custom-no-test = 自定义配置,跳过连接测试
|
||||
relay-adapter-user-info-not-available = 该配置不支持用户信息查询
|
||||
relay-adapter-usage-logs-not-available = 该配置不支持使用日志查询
|
||||
relay-adapter-token-management-not-available = 该配置不支持 Token 管理
|
||||
relay-adapter-token-deleted = Token 删除成功
|
||||
relay-adapter-get-info-failed = 获取站点信息失败
|
||||
relay-adapter-get-user-info-failed = 获取用户信息失败
|
||||
relay-adapter-connection-test-failed = 连接测试失败
|
||||
relay-adapter-get-usage-logs-failed = 获取使用日志失败
|
||||
relay-adapter-list-tokens-failed = 获取 Token 列表失败
|
||||
relay-adapter-create-token-failed = 创建 Token 失败
|
||||
relay-adapter-update-token-failed = 更新 Token 失败
|
||||
relay-adapter-delete-token-failed = 删除 Token 失败
|
||||
|
||||
# 数据库消息
|
||||
database-lock-failed = 获取数据库锁失败
|
||||
database-init-failed = 初始化数据库失败
|
||||
database-query-failed = 数据库查询失败
|
||||
|
||||
# 通用消息
|
||||
operation-cancelled = 操作已取消
|
||||
timeout-error = 操作超时
|
||||
|
@@ -6,3 +6,5 @@ pub mod storage;
|
||||
pub mod slash_commands;
|
||||
pub mod proxy;
|
||||
pub mod language;
|
||||
pub mod relay_stations;
|
||||
pub mod relay_adapters;
|
||||
|
757
src-tauri/src/commands/relay_adapters.rs
Normal file
757
src-tauri/src/commands/relay_adapters.rs
Normal 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")
|
||||
})
|
||||
}
|
696
src-tauri/src/commands/relay_stations.rs
Normal file
696
src-tauri/src/commands/relay_stations.rs
Normal file
@@ -0,0 +1,696 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use tauri::{command, State};
|
||||
use anyhow::Result;
|
||||
use chrono::Utc;
|
||||
use rusqlite::{params, Connection, Row, OptionalExtension};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::commands::agents::AgentDb;
|
||||
use crate::i18n;
|
||||
use crate::claude_config;
|
||||
|
||||
/// 中转站适配器类型
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RelayStationAdapter {
|
||||
Newapi, // NewAPI 兼容平台
|
||||
Oneapi, // OneAPI 兼容平台
|
||||
Yourapi, // YourAPI 特定平台
|
||||
Custom, // 自定义简单配置
|
||||
}
|
||||
|
||||
impl RelayStationAdapter {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
RelayStationAdapter::Newapi => "newapi",
|
||||
RelayStationAdapter::Oneapi => "oneapi",
|
||||
RelayStationAdapter::Yourapi => "yourapi",
|
||||
RelayStationAdapter::Custom => "custom",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 认证方式
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AuthMethod {
|
||||
BearerToken, // Bearer Token 认证(推荐)
|
||||
ApiKey, // API Key 认证
|
||||
Custom, // 自定义认证方式
|
||||
}
|
||||
|
||||
/// 中转站配置(完整版本)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RelayStation {
|
||||
pub id: String, // 唯一标识符
|
||||
pub name: String, // 显示名称
|
||||
pub description: Option<String>, // 描述信息
|
||||
pub api_url: String, // API 基础 URL
|
||||
pub adapter: RelayStationAdapter, // 适配器类型
|
||||
pub auth_method: AuthMethod, // 认证方式
|
||||
pub system_token: String, // 系统令牌
|
||||
pub user_id: Option<String>, // 用户 ID(NewAPI 必需)
|
||||
pub adapter_config: Option<HashMap<String, serde_json::Value>>, // 适配器特定配置
|
||||
pub enabled: bool, // 启用状态
|
||||
pub created_at: i64, // 创建时间
|
||||
pub updated_at: i64, // 更新时间
|
||||
}
|
||||
|
||||
/// 创建中转站请求(无自动生成字段)
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CreateRelayStationRequest {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub api_url: String,
|
||||
pub adapter: RelayStationAdapter,
|
||||
pub auth_method: AuthMethod,
|
||||
pub system_token: String,
|
||||
pub user_id: Option<String>,
|
||||
pub adapter_config: Option<HashMap<String, serde_json::Value>>,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
/// 更新中转站请求
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UpdateRelayStationRequest {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub api_url: String,
|
||||
pub adapter: RelayStationAdapter,
|
||||
pub auth_method: AuthMethod,
|
||||
pub system_token: String,
|
||||
pub user_id: Option<String>,
|
||||
pub adapter_config: Option<HashMap<String, serde_json::Value>>,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
/// 站点信息(统一格式)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StationInfo {
|
||||
pub name: String, // 站点名称
|
||||
pub announcement: Option<String>, // 公告信息
|
||||
pub api_url: String, // API 地址
|
||||
pub version: Option<String>, // 版本信息
|
||||
pub metadata: Option<HashMap<String, serde_json::Value>>, // 扩展元数据
|
||||
pub quota_per_unit: Option<i64>, // 单位配额(用于价格转换)
|
||||
}
|
||||
|
||||
/// 用户信息(统一格式)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserInfo {
|
||||
pub user_id: String, // 用户 ID
|
||||
pub username: Option<String>, // 用户名
|
||||
pub email: Option<String>, // 邮箱
|
||||
pub balance_remaining: Option<f64>, // 剩余余额(美元)
|
||||
pub amount_used: Option<f64>, // 已用金额(美元)
|
||||
pub request_count: Option<i64>, // 请求次数
|
||||
pub status: Option<String>, // 账户状态
|
||||
pub metadata: Option<HashMap<String, serde_json::Value>>, // 原始数据
|
||||
}
|
||||
|
||||
/// 连接测试结果
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConnectionTestResult {
|
||||
pub success: bool, // 连接是否成功
|
||||
pub response_time: Option<u64>, // 响应时间(毫秒)
|
||||
pub message: String, // 结果消息
|
||||
pub error: Option<String>, // 错误信息
|
||||
}
|
||||
|
||||
/// Token 信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TokenInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub token: String,
|
||||
pub quota: Option<i64>,
|
||||
pub used_quota: Option<i64>,
|
||||
pub status: String,
|
||||
pub created_at: i64,
|
||||
pub updated_at: i64,
|
||||
}
|
||||
|
||||
/// Token 分页响应
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TokenPaginationResponse {
|
||||
pub tokens: Vec<TokenInfo>,
|
||||
pub total: i64,
|
||||
pub page: usize,
|
||||
pub size: usize,
|
||||
pub has_more: bool,
|
||||
}
|
||||
|
||||
impl RelayStation {
|
||||
fn from_row(row: &Row) -> Result<Self, rusqlite::Error> {
|
||||
let adapter_str: String = row.get("adapter")?;
|
||||
let auth_method_str: String = row.get("auth_method")?;
|
||||
let adapter_config_str: Option<String> = row.get("adapter_config")?;
|
||||
|
||||
let adapter = serde_json::from_str(&format!("\"{}\"", adapter_str))
|
||||
.map_err(|_| rusqlite::Error::InvalidColumnType(0, "adapter".to_string(), rusqlite::types::Type::Text))?;
|
||||
|
||||
let auth_method = serde_json::from_str(&format!("\"{}\"", auth_method_str))
|
||||
.map_err(|_| rusqlite::Error::InvalidColumnType(0, "auth_method".to_string(), rusqlite::types::Type::Text))?;
|
||||
|
||||
let adapter_config = if let Some(config_str) = adapter_config_str {
|
||||
if config_str.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(serde_json::from_str(&config_str)
|
||||
.map_err(|_| rusqlite::Error::InvalidColumnType(0, "adapter_config".to_string(), rusqlite::types::Type::Text))?)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(RelayStation {
|
||||
id: row.get("id")?,
|
||||
name: row.get("name")?,
|
||||
description: row.get("description")?,
|
||||
api_url: row.get("api_url")?,
|
||||
adapter,
|
||||
auth_method,
|
||||
system_token: row.get("system_token")?,
|
||||
user_id: row.get("user_id")?,
|
||||
adapter_config,
|
||||
enabled: row.get::<_, i32>("enabled")? == 1,
|
||||
created_at: row.get("created_at")?,
|
||||
updated_at: row.get("updated_at")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 初始化中转站数据库表
|
||||
pub fn init_relay_stations_tables(conn: &Connection) -> Result<()> {
|
||||
// 中转站表
|
||||
conn.execute(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS relay_stations (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
api_url TEXT NOT NULL,
|
||||
adapter TEXT NOT NULL,
|
||||
auth_method TEXT NOT NULL,
|
||||
system_token TEXT NOT NULL,
|
||||
user_id TEXT,
|
||||
adapter_config TEXT,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
)
|
||||
"#,
|
||||
[],
|
||||
)?;
|
||||
|
||||
// 中转站使用日志表
|
||||
conn.execute(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS relay_station_usage_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
station_id TEXT NOT NULL,
|
||||
request_type TEXT NOT NULL,
|
||||
response_time INTEGER,
|
||||
success INTEGER NOT NULL,
|
||||
error_message TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (station_id) REFERENCES relay_stations (id) ON DELETE CASCADE
|
||||
)
|
||||
"#,
|
||||
[],
|
||||
)?;
|
||||
|
||||
log::info!("Relay stations database tables initialized");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取所有中转站
|
||||
#[command]
|
||||
pub async fn relay_stations_list(db: State<'_, AgentDb>) -> Result<Vec<RelayStation>, String> {
|
||||
let conn = db.0.lock().map_err(|e| {
|
||||
log::error!("Failed to acquire database lock: {}", e);
|
||||
i18n::t("database.lock_failed")
|
||||
})?;
|
||||
|
||||
// 确保表存在
|
||||
init_relay_stations_tables(&conn).map_err(|e| {
|
||||
log::error!("Failed to initialize relay stations tables: {}", e);
|
||||
i18n::t("database.init_failed")
|
||||
})?;
|
||||
|
||||
let mut stmt = conn.prepare("SELECT * FROM relay_stations ORDER BY created_at DESC")
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to prepare statement: {}", e);
|
||||
i18n::t("database.query_failed")
|
||||
})?;
|
||||
|
||||
let stations = stmt.query_map([], |row| RelayStation::from_row(row))
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to query relay stations: {}", e);
|
||||
i18n::t("database.query_failed")
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to collect relay stations: {}", e);
|
||||
i18n::t("database.query_failed")
|
||||
})?;
|
||||
|
||||
log::info!("Retrieved {} relay stations", stations.len());
|
||||
Ok(stations)
|
||||
}
|
||||
|
||||
/// 获取单个中转站
|
||||
#[command]
|
||||
pub async fn relay_station_get(
|
||||
id: String,
|
||||
db: State<'_, AgentDb>
|
||||
) -> Result<RelayStation, String> {
|
||||
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")
|
||||
})?;
|
||||
|
||||
let station = stmt.query_row(params![id], |row| RelayStation::from_row(row))
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to get relay station {}: {}", id, e);
|
||||
i18n::t("relay_station.not_found")
|
||||
})?;
|
||||
|
||||
log::info!("Retrieved relay station: {}", id);
|
||||
Ok(station)
|
||||
}
|
||||
|
||||
/// 创建中转站
|
||||
#[command]
|
||||
pub async fn relay_station_create(
|
||||
request: CreateRelayStationRequest,
|
||||
db: State<'_, AgentDb>
|
||||
) -> Result<RelayStation, String> {
|
||||
let conn = db.0.lock().map_err(|e| {
|
||||
log::error!("Failed to acquire database lock: {}", e);
|
||||
i18n::t("database.lock_failed")
|
||||
})?;
|
||||
|
||||
// 确保表存在
|
||||
init_relay_stations_tables(&conn).map_err(|e| {
|
||||
log::error!("Failed to initialize relay stations tables: {}", e);
|
||||
i18n::t("database.init_failed")
|
||||
})?;
|
||||
|
||||
// 验证输入
|
||||
validate_relay_station_request(&request.name, &request.api_url, &request.system_token)?;
|
||||
|
||||
let id = Uuid::new_v4().to_string();
|
||||
let now = Utc::now().timestamp();
|
||||
|
||||
let adapter_str = serde_json::to_string(&request.adapter)
|
||||
.map_err(|_| i18n::t("relay_station.invalid_adapter"))?
|
||||
.trim_matches('"').to_string();
|
||||
|
||||
let auth_method_str = serde_json::to_string(&request.auth_method)
|
||||
.map_err(|_| i18n::t("relay_station.invalid_auth_method"))?
|
||||
.trim_matches('"').to_string();
|
||||
|
||||
let adapter_config_str = request.adapter_config.as_ref()
|
||||
.map(|config| serde_json::to_string(config))
|
||||
.transpose()
|
||||
.map_err(|_| i18n::t("relay_station.invalid_config"))?;
|
||||
|
||||
// 如果要启用这个新中转站,先禁用所有其他中转站
|
||||
if request.enabled {
|
||||
conn.execute(
|
||||
"UPDATE relay_stations SET enabled = 0",
|
||||
[],
|
||||
).map_err(|e| {
|
||||
log::error!("Failed to disable other relay stations: {}", e);
|
||||
i18n::t("relay_station.create_failed")
|
||||
})?;
|
||||
}
|
||||
|
||||
conn.execute(
|
||||
r#"
|
||||
INSERT INTO relay_stations
|
||||
(id, name, description, api_url, adapter, auth_method, system_token, user_id, adapter_config, enabled, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)
|
||||
"#,
|
||||
params![
|
||||
id,
|
||||
request.name,
|
||||
request.description,
|
||||
request.api_url,
|
||||
adapter_str,
|
||||
auth_method_str,
|
||||
request.system_token,
|
||||
request.user_id,
|
||||
adapter_config_str,
|
||||
if request.enabled { 1 } else { 0 },
|
||||
now,
|
||||
now
|
||||
],
|
||||
).map_err(|e| {
|
||||
log::error!("Failed to create relay station: {}", e);
|
||||
i18n::t("relay_station.create_failed")
|
||||
})?;
|
||||
|
||||
let station = RelayStation {
|
||||
id: id.clone(),
|
||||
name: request.name,
|
||||
description: request.description,
|
||||
api_url: request.api_url,
|
||||
adapter: request.adapter,
|
||||
auth_method: request.auth_method,
|
||||
system_token: request.system_token,
|
||||
user_id: request.user_id,
|
||||
adapter_config: request.adapter_config,
|
||||
enabled: request.enabled,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
log::info!("Created relay station: {} ({})", station.name, id);
|
||||
Ok(station)
|
||||
}
|
||||
|
||||
/// 更新中转站
|
||||
#[command]
|
||||
pub async fn relay_station_update(
|
||||
request: UpdateRelayStationRequest,
|
||||
db: State<'_, AgentDb>
|
||||
) -> Result<RelayStation, String> {
|
||||
let conn = db.0.lock().map_err(|e| {
|
||||
log::error!("Failed to acquire database lock: {}", e);
|
||||
i18n::t("database.lock_failed")
|
||||
})?;
|
||||
|
||||
// 验证输入
|
||||
validate_relay_station_request(&request.name, &request.api_url, &request.system_token)?;
|
||||
|
||||
let now = Utc::now().timestamp();
|
||||
|
||||
let adapter_str = serde_json::to_string(&request.adapter)
|
||||
.map_err(|_| i18n::t("relay_station.invalid_adapter"))?
|
||||
.trim_matches('"').to_string();
|
||||
|
||||
let auth_method_str = serde_json::to_string(&request.auth_method)
|
||||
.map_err(|_| i18n::t("relay_station.invalid_auth_method"))?
|
||||
.trim_matches('"').to_string();
|
||||
|
||||
let adapter_config_str = request.adapter_config.as_ref()
|
||||
.map(|config| serde_json::to_string(config))
|
||||
.transpose()
|
||||
.map_err(|_| i18n::t("relay_station.invalid_config"))?;
|
||||
|
||||
// 如果要启用这个中转站,先禁用所有其他中转站
|
||||
if request.enabled {
|
||||
conn.execute(
|
||||
"UPDATE relay_stations SET enabled = 0 WHERE id != ?1",
|
||||
params![request.id],
|
||||
).map_err(|e| {
|
||||
log::error!("Failed to disable other relay stations: {}", e);
|
||||
i18n::t("relay_station.update_failed")
|
||||
})?;
|
||||
}
|
||||
|
||||
let rows_affected = conn.execute(
|
||||
r#"
|
||||
UPDATE relay_stations
|
||||
SET name = ?2, description = ?3, api_url = ?4, adapter = ?5, auth_method = ?6,
|
||||
system_token = ?7, user_id = ?8, adapter_config = ?9, enabled = ?10, updated_at = ?11
|
||||
WHERE id = ?1
|
||||
"#,
|
||||
params![
|
||||
request.id,
|
||||
request.name,
|
||||
request.description,
|
||||
request.api_url,
|
||||
adapter_str,
|
||||
auth_method_str,
|
||||
request.system_token,
|
||||
request.user_id,
|
||||
adapter_config_str,
|
||||
if request.enabled { 1 } else { 0 },
|
||||
now
|
||||
],
|
||||
).map_err(|e| {
|
||||
log::error!("Failed to update relay station: {}", e);
|
||||
i18n::t("relay_station.update_failed")
|
||||
})?;
|
||||
|
||||
if rows_affected == 0 {
|
||||
return Err(i18n::t("relay_station.not_found"));
|
||||
}
|
||||
|
||||
let station = RelayStation {
|
||||
id: request.id.clone(),
|
||||
name: request.name,
|
||||
description: request.description,
|
||||
api_url: request.api_url,
|
||||
adapter: request.adapter,
|
||||
auth_method: request.auth_method,
|
||||
system_token: request.system_token,
|
||||
user_id: request.user_id,
|
||||
adapter_config: request.adapter_config,
|
||||
enabled: request.enabled,
|
||||
created_at: 0, // 不重要,前端可以重新获取
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
log::info!("Updated relay station: {} ({})", station.name, request.id);
|
||||
Ok(station)
|
||||
}
|
||||
|
||||
/// 删除中转站
|
||||
#[command]
|
||||
pub async fn relay_station_delete(
|
||||
id: String,
|
||||
db: State<'_, AgentDb>
|
||||
) -> Result<String, String> {
|
||||
let conn = db.0.lock().map_err(|e| {
|
||||
log::error!("Failed to acquire database lock: {}", e);
|
||||
i18n::t("database.lock_failed")
|
||||
})?;
|
||||
|
||||
let rows_affected = conn.execute("DELETE FROM relay_stations WHERE id = ?1", params![id])
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to delete relay station: {}", e);
|
||||
i18n::t("relay_station.delete_failed")
|
||||
})?;
|
||||
|
||||
if rows_affected == 0 {
|
||||
return Err(i18n::t("relay_station.not_found"));
|
||||
}
|
||||
|
||||
log::info!("Deleted relay station: {}", id);
|
||||
Ok(i18n::t("relay_station.delete_success"))
|
||||
}
|
||||
|
||||
/// 切换中转站启用状态(确保只有一个中转站启用)
|
||||
#[command]
|
||||
pub async fn relay_station_toggle_enable(
|
||||
id: String,
|
||||
enabled: bool,
|
||||
db: State<'_, AgentDb>
|
||||
) -> Result<String, String> {
|
||||
let conn = db.0.lock().map_err(|e| {
|
||||
log::error!("Failed to acquire database lock: {}", e);
|
||||
i18n::t("database.lock_failed")
|
||||
})?;
|
||||
|
||||
let now = Utc::now().timestamp();
|
||||
|
||||
// 如果要启用这个中转站,先禁用所有其他中转站
|
||||
if enabled {
|
||||
conn.execute(
|
||||
"UPDATE relay_stations SET enabled = 0, updated_at = ?1 WHERE id != ?2",
|
||||
params![now, id],
|
||||
).map_err(|e| {
|
||||
log::error!("Failed to disable other relay stations: {}", e);
|
||||
i18n::t("relay_station.update_failed")
|
||||
})?;
|
||||
|
||||
// 获取要启用的中转站信息
|
||||
let station = relay_station_get_internal(&conn, &id)?;
|
||||
|
||||
// 将中转站配置应用到 Claude 配置文件
|
||||
if let Err(e) = claude_config::apply_relay_station_to_config(&station) {
|
||||
log::error!("Failed to apply relay station config: {}", e);
|
||||
// 不中断流程,但记录错误
|
||||
} else {
|
||||
log::info!("Applied relay station config to Claude settings");
|
||||
}
|
||||
} else {
|
||||
// 如果禁用中转站,清除 Claude 配置中的相关设置
|
||||
if let Err(e) = claude_config::clear_relay_station_from_config() {
|
||||
log::error!("Failed to clear relay station config: {}", e);
|
||||
} else {
|
||||
log::info!("Cleared relay station config from Claude settings");
|
||||
}
|
||||
}
|
||||
|
||||
// 更新目标中转站的启用状态
|
||||
let rows_affected = conn.execute(
|
||||
"UPDATE relay_stations SET enabled = ?1, updated_at = ?2 WHERE id = ?3",
|
||||
params![if enabled { 1 } else { 0 }, now, id],
|
||||
).map_err(|e| {
|
||||
log::error!("Failed to toggle relay station enable status: {}", e);
|
||||
i18n::t("relay_station.update_failed")
|
||||
})?;
|
||||
|
||||
if rows_affected == 0 {
|
||||
return Err(i18n::t("relay_station.not_found"));
|
||||
}
|
||||
|
||||
log::info!("Toggled relay station enable status: {} -> {}", id, enabled);
|
||||
Ok(if enabled {
|
||||
i18n::t("relay_station.enabled_success")
|
||||
} else {
|
||||
i18n::t("relay_station.disabled_success")
|
||||
})
|
||||
}
|
||||
|
||||
/// 内部方法:获取单个中转站
|
||||
fn relay_station_get_internal(conn: &Connection, id: &str) -> Result<RelayStation, String> {
|
||||
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")
|
||||
})?;
|
||||
|
||||
let station = stmt.query_row(params![id], |row| {
|
||||
RelayStation::from_row(row)
|
||||
}).map_err(|e| {
|
||||
log::error!("Failed to get relay station: {}", e);
|
||||
i18n::t("relay_station.not_found")
|
||||
})?;
|
||||
|
||||
Ok(station)
|
||||
}
|
||||
|
||||
/// 输入验证
|
||||
fn validate_relay_station_request(name: &str, api_url: &str, system_token: &str) -> Result<(), String> {
|
||||
if name.trim().is_empty() {
|
||||
return Err(i18n::t("relay_station.name_required"));
|
||||
}
|
||||
|
||||
if api_url.trim().is_empty() {
|
||||
return Err(i18n::t("relay_station.api_url_required"));
|
||||
}
|
||||
|
||||
// 验证 URL 格式
|
||||
if let Err(_) = url::Url::parse(api_url) {
|
||||
return Err(i18n::t("relay_station.invalid_url"));
|
||||
}
|
||||
|
||||
// 验证是否为 HTTPS
|
||||
if !api_url.starts_with("https://") {
|
||||
return Err(i18n::t("relay_station.https_required"));
|
||||
}
|
||||
|
||||
if system_token.trim().is_empty() {
|
||||
return Err(i18n::t("relay_station.token_required"));
|
||||
}
|
||||
|
||||
if system_token.len() < 10 {
|
||||
return Err(i18n::t("relay_station.token_too_short"));
|
||||
}
|
||||
|
||||
// 检查 Token 是否包含特殊字符
|
||||
if system_token.chars().any(|c| c.is_whitespace() || c.is_control()) {
|
||||
return Err(i18n::t("relay_station.token_invalid_chars"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Token 脱敏显示
|
||||
#[allow(dead_code)]
|
||||
pub fn mask_token(token: &str) -> String {
|
||||
if token.len() <= 8 {
|
||||
"*".repeat(token.len())
|
||||
} else {
|
||||
format!("{}...{}", &token[..4], &token[token.len()-4..])
|
||||
}
|
||||
}
|
||||
|
||||
/// 手动同步中转站配置到 Claude 配置文件
|
||||
#[command]
|
||||
pub async fn relay_station_sync_config(
|
||||
db: State<'_, AgentDb>
|
||||
) -> Result<String, String> {
|
||||
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 enabled = 1 LIMIT 1"
|
||||
).map_err(|e| {
|
||||
log::error!("Failed to prepare statement: {}", e);
|
||||
i18n::t("database.query_failed")
|
||||
})?;
|
||||
|
||||
let station_opt = stmt.query_row([], |row| {
|
||||
RelayStation::from_row(row)
|
||||
}).optional().map_err(|e| {
|
||||
log::error!("Failed to query enabled relay station: {}", e);
|
||||
i18n::t("database.query_failed")
|
||||
})?;
|
||||
|
||||
if let Some(station) = station_opt {
|
||||
// 应用中转站配置
|
||||
claude_config::apply_relay_station_to_config(&station)
|
||||
.map_err(|e| format!("配置同步失败: {}", e))?;
|
||||
|
||||
log::info!("Synced relay station {} config to Claude settings", station.name);
|
||||
Ok(format!("已同步中转站 {} 的配置到 Claude 设置", station.name))
|
||||
} else {
|
||||
// 没有启用的中转站,清除配置
|
||||
claude_config::clear_relay_station_from_config()
|
||||
.map_err(|e| format!("清除配置失败: {}", e))?;
|
||||
|
||||
log::info!("Cleared relay station config from Claude settings");
|
||||
Ok("已清除 Claude 设置中的中转站配置".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// 恢复 Claude 配置备份
|
||||
#[command]
|
||||
pub async fn relay_station_restore_config() -> Result<String, String> {
|
||||
claude_config::restore_claude_config()
|
||||
.map_err(|e| format!("恢复配置失败: {}", e))?;
|
||||
|
||||
log::info!("Restored Claude config from backup");
|
||||
Ok("已从备份恢复 Claude 配置".to_string())
|
||||
}
|
||||
|
||||
/// 获取当前 Claude 配置中的 API 信息
|
||||
#[command]
|
||||
pub async fn relay_station_get_current_config() -> Result<HashMap<String, Option<String>>, String> {
|
||||
let mut config = HashMap::new();
|
||||
|
||||
config.insert(
|
||||
"api_url".to_string(),
|
||||
claude_config::get_current_api_url().unwrap_or(None)
|
||||
);
|
||||
|
||||
config.insert(
|
||||
"api_token".to_string(),
|
||||
claude_config::get_current_api_token().unwrap_or(None)
|
||||
.map(|token: String| {
|
||||
// 脱敏显示 token
|
||||
mask_token(&token)
|
||||
})
|
||||
);
|
||||
|
||||
Ok(config)
|
||||
}
|
@@ -43,6 +43,19 @@ impl SimpleI18n {
|
||||
("en-US", "agent-not-found") => "Agent not found".to_string(),
|
||||
("en-US", "claude-not-installed") => "Claude Code is not installed".to_string(),
|
||||
|
||||
// Relay Station English translations
|
||||
("en-US", "relay_adapter.custom_no_test") => "Custom configuration, connection test skipped".to_string(),
|
||||
("en-US", "relay_adapter.user_info_not_available") => "User info not available for this configuration".to_string(),
|
||||
("en-US", "relay_adapter.usage_logs_not_available") => "Usage logs not available for this configuration".to_string(),
|
||||
("en-US", "relay_adapter.token_management_not_available") => "Token management not available for this configuration".to_string(),
|
||||
("en-US", "relay_adapter.connection_success") => "Connection successful".to_string(),
|
||||
("en-US", "relay_adapter.api_error") => "API returned error".to_string(),
|
||||
("en-US", "relay_adapter.parse_error") => "Failed to parse response".to_string(),
|
||||
("en-US", "relay_adapter.http_error") => "HTTP request failed".to_string(),
|
||||
("en-US", "relay_adapter.network_error") => "Network connection failed".to_string(),
|
||||
("en-US", "relay_station.enabled_success") => "Relay station enabled successfully".to_string(),
|
||||
("en-US", "relay_station.disabled_success") => "Relay station disabled successfully".to_string(),
|
||||
|
||||
// 中文翻译
|
||||
("zh-CN", "error-failed-to-create") => "创建失败".to_string(),
|
||||
("zh-CN", "error-failed-to-update") => "更新失败".to_string(),
|
||||
@@ -50,6 +63,19 @@ impl SimpleI18n {
|
||||
("zh-CN", "agent-not-found") => "未找到智能体".to_string(),
|
||||
("zh-CN", "claude-not-installed") => "未安装 Claude Code".to_string(),
|
||||
|
||||
// Relay Station Chinese translations
|
||||
("zh-CN", "relay_adapter.custom_no_test") => "自定义配置,跳过连接测试".to_string(),
|
||||
("zh-CN", "relay_adapter.user_info_not_available") => "该配置不支持用户信息查询".to_string(),
|
||||
("zh-CN", "relay_adapter.usage_logs_not_available") => "该配置不支持使用日志查询".to_string(),
|
||||
("zh-CN", "relay_adapter.token_management_not_available") => "该配置不支持 Token 管理".to_string(),
|
||||
("zh-CN", "relay_adapter.connection_success") => "连接成功".to_string(),
|
||||
("zh-CN", "relay_adapter.api_error") => "API 返回错误".to_string(),
|
||||
("zh-CN", "relay_adapter.parse_error") => "解析响应失败".to_string(),
|
||||
("zh-CN", "relay_adapter.http_error") => "HTTP 请求失败".to_string(),
|
||||
("zh-CN", "relay_adapter.network_error") => "网络连接失败".to_string(),
|
||||
("zh-CN", "relay_station.enabled_success") => "中转站启用成功".to_string(),
|
||||
("zh-CN", "relay_station.disabled_success") => "中转站禁用成功".to_string(),
|
||||
|
||||
// 默认情况
|
||||
_ => key.to_string(),
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@
|
||||
// Declare modules
|
||||
pub mod checkpoint;
|
||||
pub mod claude_binary;
|
||||
pub mod claude_config;
|
||||
pub mod commands;
|
||||
pub mod process;
|
||||
pub mod i18n;
|
||||
|
@@ -6,6 +6,7 @@ mod claude_binary;
|
||||
mod commands;
|
||||
mod process;
|
||||
mod i18n;
|
||||
mod claude_config;
|
||||
|
||||
use checkpoint::state::CheckpointState;
|
||||
use commands::agents::{
|
||||
@@ -45,6 +46,16 @@ use commands::storage::{
|
||||
};
|
||||
use commands::proxy::{get_proxy_settings, save_proxy_settings, apply_proxy_settings};
|
||||
use commands::language::{get_current_language, set_language, get_supported_languages};
|
||||
use commands::relay_stations::{
|
||||
relay_stations_list, relay_station_get, relay_station_create, relay_station_update,
|
||||
relay_station_delete, relay_station_toggle_enable, relay_station_sync_config,
|
||||
relay_station_restore_config, relay_station_get_current_config,
|
||||
};
|
||||
use commands::relay_adapters::{
|
||||
relay_station_get_info, relay_station_get_user_info,
|
||||
relay_station_test_connection, relay_station_get_usage_logs, relay_station_list_tokens,
|
||||
relay_station_create_token, relay_station_update_token, relay_station_delete_token,
|
||||
};
|
||||
use process::ProcessRegistryState;
|
||||
use std::sync::Mutex;
|
||||
use tauri::Manager;
|
||||
@@ -256,6 +267,25 @@ fn main() {
|
||||
get_current_language,
|
||||
set_language,
|
||||
get_supported_languages,
|
||||
|
||||
// Relay Stations
|
||||
relay_stations_list,
|
||||
relay_station_get,
|
||||
relay_station_create,
|
||||
relay_station_update,
|
||||
relay_station_delete,
|
||||
relay_station_toggle_enable,
|
||||
relay_station_sync_config,
|
||||
relay_station_restore_config,
|
||||
relay_station_get_current_config,
|
||||
relay_station_get_info,
|
||||
relay_station_get_user_info,
|
||||
relay_station_test_connection,
|
||||
relay_station_get_usage_logs,
|
||||
relay_station_list_tokens,
|
||||
relay_station_create_token,
|
||||
relay_station_update_token,
|
||||
relay_station_delete_token,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
@@ -28,6 +28,7 @@ import { AnalyticsConsentBanner } from "@/components/AnalyticsConsent";
|
||||
import { useAppLifecycle, useTrackEvent } from "@/hooks";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import { WelcomePage } from "@/components/WelcomePage";
|
||||
import RelayStationManager from "@/components/RelayStationManager";
|
||||
|
||||
type View =
|
||||
| "welcome"
|
||||
@@ -41,6 +42,7 @@ type View =
|
||||
| "agent-execution"
|
||||
| "agent-run-view"
|
||||
| "mcp"
|
||||
| "relay-stations"
|
||||
| "usage-dashboard"
|
||||
| "project-settings"
|
||||
| "tabs"; // New view for tab-based interface
|
||||
@@ -255,6 +257,11 @@ function AppContent() {
|
||||
/>
|
||||
);
|
||||
|
||||
case "relay-stations":
|
||||
return (
|
||||
<RelayStationManager onBack={() => handleViewChange("welcome")} />
|
||||
);
|
||||
|
||||
case "cc-agents":
|
||||
return (
|
||||
<CCAgents
|
||||
|
691
src/components/RelayStationManager.tsx
Normal file
691
src/components/RelayStationManager.tsx
Normal file
@@ -0,0 +1,691 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import {
|
||||
RelayStation,
|
||||
CreateRelayStationRequest,
|
||||
UpdateRelayStationRequest,
|
||||
RelayStationAdapter,
|
||||
AuthMethod,
|
||||
ConnectionTestResult,
|
||||
api
|
||||
} from '@/lib/api';
|
||||
import {
|
||||
Plus,
|
||||
Edit,
|
||||
Trash2,
|
||||
Globe,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Wifi,
|
||||
WifiOff,
|
||||
Server,
|
||||
ArrowLeft,
|
||||
Settings,
|
||||
RefreshCw
|
||||
} from 'lucide-react';
|
||||
|
||||
interface RelayStationManagerProps {
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const RelayStationManager: React.FC<RelayStationManagerProps> = ({ onBack }) => {
|
||||
const [stations, setStations] = useState<RelayStation[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedStation, setSelectedStation] = useState<RelayStation | null>(null);
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||
const [showEditDialog, setShowEditDialog] = useState(false);
|
||||
const [connectionTests, setConnectionTests] = useState<Record<string, ConnectionTestResult>>({});
|
||||
const [testingConnections, setTestingConnections] = useState<Record<string, boolean>>({});
|
||||
const [togglingEnable, setTogglingEnable] = useState<Record<string, boolean>>({});
|
||||
const [currentConfig, setCurrentConfig] = useState<Record<string, string | null>>({});
|
||||
const [loadingConfig, setLoadingConfig] = useState(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 加载中转站列表
|
||||
const loadStations = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const stationList = await api.relayStationsList();
|
||||
setStations(stationList);
|
||||
} catch (error) {
|
||||
console.error('Failed to load stations:', error);
|
||||
alert(t('relayStation.loadFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载当前配置状态
|
||||
const loadCurrentConfig = async () => {
|
||||
try {
|
||||
setLoadingConfig(true);
|
||||
const config = await api.relayStationGetCurrentConfig();
|
||||
setCurrentConfig(config);
|
||||
} catch (error) {
|
||||
console.error('Failed to load current config:', error);
|
||||
} finally {
|
||||
setLoadingConfig(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 手动同步配置
|
||||
const syncConfig = async () => {
|
||||
try {
|
||||
const result = await api.relayStationSyncConfig();
|
||||
alert(result);
|
||||
loadCurrentConfig();
|
||||
} catch (error) {
|
||||
console.error('Failed to sync config:', error);
|
||||
alert(t('relayStation.syncFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
// 测试连接
|
||||
const testConnection = async (stationId: string) => {
|
||||
try {
|
||||
setTestingConnections(prev => ({ ...prev, [stationId]: true }));
|
||||
const result = await api.relayStationTestConnection(stationId);
|
||||
setConnectionTests(prev => ({ ...prev, [stationId]: result }));
|
||||
|
||||
if (result.success) {
|
||||
alert(t('relayStation.connectionSuccess'));
|
||||
} else {
|
||||
alert(result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Connection test failed:', error);
|
||||
alert(t('relayStation.connectionFailed'));
|
||||
} finally {
|
||||
setTestingConnections(prev => ({ ...prev, [stationId]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
// 删除中转站
|
||||
const deleteStation = async (stationId: string) => {
|
||||
if (!confirm(t('relayStation.deleteConfirm'))) return;
|
||||
|
||||
try {
|
||||
await api.relayStationDelete(stationId);
|
||||
alert(t('relayStation.deleteSuccess'));
|
||||
loadStations();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete station:', error);
|
||||
alert(t('relayStation.deleteFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
// 获取适配器类型显示名称
|
||||
const getAdapterDisplayName = (adapter: RelayStationAdapter): string => {
|
||||
switch (adapter) {
|
||||
case 'newapi': return 'NewAPI';
|
||||
case 'oneapi': return 'OneAPI';
|
||||
case 'yourapi': return 'YourAPI';
|
||||
case 'custom': return t('relayStation.custom');
|
||||
default: return adapter;
|
||||
}
|
||||
};
|
||||
|
||||
// 切换启用状态
|
||||
const toggleEnableStatus = async (stationId: string, currentEnabled: boolean) => {
|
||||
try {
|
||||
setTogglingEnable(prev => ({ ...prev, [stationId]: true }));
|
||||
const newEnabled = !currentEnabled;
|
||||
await api.relayStationToggleEnable(stationId, newEnabled);
|
||||
alert(newEnabled ? t('relayStation.enabledSuccess') : t('relayStation.disabledSuccess'));
|
||||
loadStations();
|
||||
loadCurrentConfig(); // 重新加载配置状态
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle enable status:', error);
|
||||
alert(t('relayStation.toggleEnableFailed'));
|
||||
} finally {
|
||||
setTogglingEnable(prev => ({ ...prev, [stationId]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
// 获取状态样式
|
||||
const getStatusBadge = (station: RelayStation) => {
|
||||
const enabled = station.enabled;
|
||||
const isToggling = togglingEnable[station.id];
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={enabled}
|
||||
disabled={isToggling}
|
||||
onCheckedChange={() => toggleEnableStatus(station.id, enabled)}
|
||||
className="data-[state=checked]:bg-green-500"
|
||||
/>
|
||||
{isToggling ? (
|
||||
<Badge variant="secondary" className="animate-pulse">{t('common.updating')}</Badge>
|
||||
) : enabled ? (
|
||||
<Badge variant="default" className="bg-green-500">{t('status.enabled')}</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary">{t('status.disabled')}</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadStations();
|
||||
loadCurrentConfig();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onBack}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
{t('app.back')}
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">{t('navigation.relayStations')}</h1>
|
||||
<p className="text-muted-foreground">{t('relayStation.description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t('relayStation.create')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<CreateStationDialog
|
||||
onSuccess={() => {
|
||||
setShowCreateDialog(false);
|
||||
loadStations();
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* 当前配置状态 */}
|
||||
<Card className="border-blue-200 dark:border-blue-900 bg-blue-50/50 dark:bg-blue-950/20">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
<CardTitle className="text-lg">{t('relayStation.currentConfig')}</CardTitle>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
loadCurrentConfig();
|
||||
syncConfig();
|
||||
}}
|
||||
disabled={loadingConfig}
|
||||
>
|
||||
{loadingConfig ? (
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-b-2 border-current" />
|
||||
) : (
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
)}
|
||||
<span className="ml-2">{t('relayStation.syncConfig')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="font-medium text-muted-foreground min-w-[100px]">API URL:</span>
|
||||
<span className="font-mono text-xs break-all">
|
||||
{currentConfig.api_url || t('relayStation.notConfigured')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="font-medium text-muted-foreground min-w-[100px]">API Token:</span>
|
||||
<span className="font-mono text-xs">
|
||||
{currentConfig.api_token || t('relayStation.notConfigured')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-3">
|
||||
{t('relayStation.configLocation')}: ~/.claude/settings.json
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 中转站列表 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{loading ? (
|
||||
<div className="col-span-full text-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
|
||||
<p className="mt-2 text-muted-foreground">{t('common.loading')}</p>
|
||||
</div>
|
||||
) : stations.length === 0 ? (
|
||||
<div className="col-span-full text-center py-12">
|
||||
<Server className="mx-auto h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-semibold mb-2">{t('relayStation.noStations')}</h3>
|
||||
<p className="text-muted-foreground mb-4">{t('relayStation.noStationsDesc')}</p>
|
||||
<Button onClick={() => setShowCreateDialog(true)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t('relayStation.createFirst')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
stations.map((station) => (
|
||||
<Card key={station.id} className="relative">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<CardTitle className="text-lg">{station.name}</CardTitle>
|
||||
<CardDescription className="mt-1">
|
||||
{getAdapterDisplayName(station.adapter)}
|
||||
</CardDescription>
|
||||
</div>
|
||||
{getStatusBadge(station)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center text-sm text-muted-foreground">
|
||||
<Globe className="mr-2 h-4 w-4" />
|
||||
{station.api_url}
|
||||
</div>
|
||||
|
||||
{station.description && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{station.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{connectionTests[station.id] && (
|
||||
<div className="flex items-center text-sm">
|
||||
{connectionTests[station.id].success ? (
|
||||
<CheckCircle className="mr-2 h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<XCircle className="mr-2 h-4 w-4 text-red-500" />
|
||||
)}
|
||||
<span>
|
||||
{connectionTests[station.id].message}
|
||||
{connectionTests[station.id].response_time && (
|
||||
<span className="ml-2 text-muted-foreground">
|
||||
({connectionTests[station.id].response_time}ms)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => testConnection(station.id)}
|
||||
disabled={testingConnections[station.id]}
|
||||
>
|
||||
{testingConnections[station.id] ? (
|
||||
<WifiOff className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Wifi className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{t('relayStation.testConnection')}
|
||||
</Button>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSelectedStation(station);
|
||||
setShowEditDialog(true);
|
||||
}}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => deleteStation(station.id)}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 编辑对话框 */}
|
||||
{selectedStation && (
|
||||
<Dialog open={showEditDialog} onOpenChange={setShowEditDialog}>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<EditStationDialog
|
||||
station={selectedStation}
|
||||
onSuccess={() => {
|
||||
setShowEditDialog(false);
|
||||
setSelectedStation(null);
|
||||
loadStations();
|
||||
}}
|
||||
onCancel={() => {
|
||||
setShowEditDialog(false);
|
||||
setSelectedStation(null);
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 创建中转站对话框组件
|
||||
const CreateStationDialog: React.FC<{
|
||||
onSuccess: () => void;
|
||||
}> = ({ onSuccess }) => {
|
||||
const [formData, setFormData] = useState<CreateRelayStationRequest>({
|
||||
name: '',
|
||||
description: '',
|
||||
api_url: '',
|
||||
adapter: 'newapi',
|
||||
auth_method: 'bearer_token',
|
||||
system_token: '',
|
||||
user_id: '',
|
||||
enabled: false, // 默认不启用,需要通过主界面切换
|
||||
});
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
alert(t('relayStation.nameRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.api_url.trim()) {
|
||||
alert(t('relayStation.apiUrlRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.system_token.trim()) {
|
||||
alert(t('relayStation.tokenRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setSubmitting(true);
|
||||
await api.relayStationCreate(formData);
|
||||
alert(t('relayStation.createSuccess'));
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
console.error('Failed to create station:', error);
|
||||
alert(t('relayStation.createFailed'));
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('relayStation.createTitle')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">{t('relayStation.name')} *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||
placeholder={t('relayStation.namePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="adapter">{t('relayStation.adapterType')}</Label>
|
||||
<Select
|
||||
value={formData.adapter}
|
||||
onValueChange={(value: RelayStationAdapter) =>
|
||||
setFormData(prev => ({ ...prev, adapter: value }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="newapi">NewAPI</SelectItem>
|
||||
<SelectItem value="oneapi">OneAPI</SelectItem>
|
||||
<SelectItem value="yourapi">YourAPI</SelectItem>
|
||||
<SelectItem value="custom">{t('relayStation.custom')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">{t('relayStation.description')}</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
||||
placeholder={t('relayStation.descriptionPlaceholder')}
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="api_url">{t('relayStation.apiUrl')} *</Label>
|
||||
<Input
|
||||
id="api_url"
|
||||
type="url"
|
||||
value={formData.api_url}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, api_url: e.target.value }))}
|
||||
placeholder="https://api.example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="auth_method">{t('relayStation.authMethod')}</Label>
|
||||
<Select
|
||||
value={formData.auth_method}
|
||||
onValueChange={(value: AuthMethod) =>
|
||||
setFormData(prev => ({ ...prev, auth_method: value }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="bearer_token">Bearer Token</SelectItem>
|
||||
<SelectItem value="api_key">API Key</SelectItem>
|
||||
<SelectItem value="custom">{t('relayStation.custom')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="system_token">{t('relayStation.systemToken')} *</Label>
|
||||
<Input
|
||||
id="system_token"
|
||||
type="password"
|
||||
value={formData.system_token}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, system_token: e.target.value }))}
|
||||
placeholder={t('relayStation.tokenPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(formData.adapter === 'newapi' || formData.adapter === 'oneapi') && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="user_id">{t('relayStation.userId')}</Label>
|
||||
<Input
|
||||
id="user_id"
|
||||
value={formData.user_id}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, user_id: e.target.value }))}
|
||||
placeholder={t('relayStation.userIdPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="enabled"
|
||||
checked={formData.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
setFormData(prev => ({ ...prev, enabled: checked }))
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="enabled">{t('relayStation.enabled')}</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button type="button" variant="outline" onClick={() => {}}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button type="submit" disabled={submitting}>
|
||||
{submitting && <div className="mr-2 h-4 w-4 animate-spin rounded-full border-b-2 border-white"></div>}
|
||||
{t('common.create')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// 编辑中转站对话框组件
|
||||
const EditStationDialog: React.FC<{
|
||||
station: RelayStation;
|
||||
onSuccess: () => void;
|
||||
onCancel: () => void;
|
||||
}> = ({ station, onSuccess, onCancel }) => {
|
||||
const [formData, setFormData] = useState<UpdateRelayStationRequest>({
|
||||
id: station.id,
|
||||
name: station.name,
|
||||
description: station.description || '',
|
||||
api_url: station.api_url,
|
||||
adapter: station.adapter,
|
||||
auth_method: station.auth_method,
|
||||
system_token: station.system_token,
|
||||
user_id: station.user_id || '',
|
||||
enabled: station.enabled,
|
||||
});
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
alert(t('relayStation.nameRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setSubmitting(true);
|
||||
await api.relayStationUpdate(formData);
|
||||
alert(t('relayStation.updateSuccess'));
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
console.error('Failed to update station:', error);
|
||||
alert(t('relayStation.updateFailed'));
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('relayStation.editTitle')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* 表单内容与创建对话框相同,但使用 formData 和 setFormData */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">{t('relayStation.name')} *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||
placeholder={t('relayStation.namePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="adapter">{t('relayStation.adapterType')}</Label>
|
||||
<Select
|
||||
value={formData.adapter}
|
||||
onValueChange={(value: RelayStationAdapter) =>
|
||||
setFormData(prev => ({ ...prev, adapter: value }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="newapi">NewAPI</SelectItem>
|
||||
<SelectItem value="oneapi">OneAPI</SelectItem>
|
||||
<SelectItem value="yourapi">YourAPI</SelectItem>
|
||||
<SelectItem value="custom">{t('relayStation.custom')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="api_url">{t('relayStation.apiUrl')} *</Label>
|
||||
<Input
|
||||
id="api_url"
|
||||
type="url"
|
||||
value={formData.api_url}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, api_url: e.target.value }))}
|
||||
placeholder="https://api.example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="system_token">{t('relayStation.systemToken')} *</Label>
|
||||
<Input
|
||||
id="system_token"
|
||||
type="password"
|
||||
value={formData.system_token}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, system_token: e.target.value }))}
|
||||
placeholder={t('relayStation.tokenPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button type="submit" disabled={submitting}>
|
||||
{submitting && <div className="mr-2 h-4 w-4 animate-spin rounded-full border-b-2 border-white"></div>}
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RelayStationManager;
|
@@ -1,5 +1,5 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { Bot, FolderCode, BarChart, ServerCog, FileText, Settings } from "lucide-react";
|
||||
import { Bot, FolderCode, BarChart, ServerCog, FileText, Settings, Network } from "lucide-react";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ClaudiaLogoMinimal } from "@/components/ClaudiaLogo";
|
||||
@@ -14,6 +14,15 @@ export function WelcomePage({ onNavigate, onNewSession }: WelcomePageProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const mainFeatures = [
|
||||
{
|
||||
id: "relay-stations",
|
||||
icon: Network,
|
||||
title: t("welcome.relayStationManagement"),
|
||||
subtitle: t("welcome.relayStationManagementDesc"),
|
||||
color: "text-indigo-500",
|
||||
bgColor: "bg-indigo-500/10",
|
||||
view: "relay-stations"
|
||||
},
|
||||
{
|
||||
id: "agents",
|
||||
icon: Bot,
|
||||
@@ -101,7 +110,7 @@ export function WelcomePage({ onNavigate, onNewSession }: WelcomePageProps) {
|
||||
</motion.div>
|
||||
|
||||
{/* Main Feature Cards */}
|
||||
<div className="grid grid-cols-2 gap-8 mb-12">
|
||||
<div className="grid grid-cols-3 gap-8 mb-12">
|
||||
{mainFeatures.map((feature, index) => (
|
||||
<motion.div
|
||||
key={feature.id}
|
||||
|
380
src/lib/api.ts
380
src/lib/api.ts
@@ -440,6 +440,117 @@ export interface ImportServerResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Relay Station Types
|
||||
// ================================
|
||||
|
||||
/** 中转站适配器类型 */
|
||||
export type RelayStationAdapter =
|
||||
| 'newapi' // NewAPI 兼容平台
|
||||
| 'oneapi' // OneAPI 兼容平台
|
||||
| 'yourapi' // YourAPI 特定平台
|
||||
| 'custom'; // 自定义简单配置
|
||||
|
||||
/** 认证方式 */
|
||||
export type AuthMethod =
|
||||
| 'bearer_token' // Bearer Token 认证(推荐)
|
||||
| 'api_key' // API Key 认证
|
||||
| 'custom'; // 自定义认证方式
|
||||
|
||||
/** 中转站配置 */
|
||||
export interface RelayStation {
|
||||
id: string; // 唯一标识符
|
||||
name: string; // 显示名称
|
||||
description?: string; // 描述信息
|
||||
api_url: string; // API 基础 URL
|
||||
adapter: RelayStationAdapter; // 适配器类型
|
||||
auth_method: AuthMethod; // 认证方式
|
||||
system_token: string; // 系统令牌
|
||||
user_id?: string; // 用户 ID(NewAPI 必需)
|
||||
adapter_config?: Record<string, any>; // 适配器特定配置
|
||||
enabled: boolean; // 启用状态
|
||||
created_at: number; // 创建时间
|
||||
updated_at: number; // 更新时间
|
||||
}
|
||||
|
||||
/** 创建中转站请求 */
|
||||
export interface CreateRelayStationRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
api_url: string;
|
||||
adapter: RelayStationAdapter;
|
||||
auth_method: AuthMethod;
|
||||
system_token: string;
|
||||
user_id?: string;
|
||||
adapter_config?: Record<string, any>;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
/** 更新中转站请求 */
|
||||
export interface UpdateRelayStationRequest {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
api_url: string;
|
||||
adapter: RelayStationAdapter;
|
||||
auth_method: AuthMethod;
|
||||
system_token: string;
|
||||
user_id?: string;
|
||||
adapter_config?: Record<string, any>;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
/** 站点信息 */
|
||||
export interface StationInfo {
|
||||
name: string; // 站点名称
|
||||
announcement?: string; // 公告信息
|
||||
api_url: string; // API 地址
|
||||
version?: string; // 版本信息
|
||||
metadata?: Record<string, any>; // 扩展元数据
|
||||
quota_per_unit?: number; // 单位配额(用于价格转换)
|
||||
}
|
||||
|
||||
/** 用户信息 */
|
||||
export interface UserInfo {
|
||||
user_id: string; // 用户 ID
|
||||
username?: string; // 用户名
|
||||
email?: string; // 邮箱
|
||||
balance_remaining?: number; // 剩余余额(美元)
|
||||
amount_used?: number; // 已用金额(美元)
|
||||
request_count?: number; // 请求次数
|
||||
status?: string; // 账户状态
|
||||
metadata?: Record<string, any>; // 原始数据
|
||||
}
|
||||
|
||||
/** 连接测试结果 */
|
||||
export interface ConnectionTestResult {
|
||||
success: boolean; // 连接是否成功
|
||||
response_time?: number; // 响应时间(毫秒)
|
||||
message: string; // 结果消息
|
||||
error?: string; // 错误信息
|
||||
}
|
||||
|
||||
/** Token 信息 */
|
||||
export interface TokenInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
token: string;
|
||||
quota?: number;
|
||||
used_quota?: number;
|
||||
status: string;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
/** Token 分页响应 */
|
||||
export interface TokenPaginationResponse {
|
||||
tokens: TokenInfo[];
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
has_more: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* API client for interacting with the Rust backend
|
||||
*/
|
||||
@@ -1923,5 +2034,274 @@ export const api = {
|
||||
console.error("Failed to get supported languages:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// ================================
|
||||
// Relay Stations
|
||||
// ================================
|
||||
|
||||
/**
|
||||
* Lists all relay stations
|
||||
* @returns Promise resolving to array of relay stations
|
||||
*/
|
||||
async relayStationsList(): Promise<RelayStation[]> {
|
||||
try {
|
||||
return await invoke<RelayStation[]>("relay_stations_list");
|
||||
} catch (error) {
|
||||
console.error("Failed to list relay stations:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a single relay station by ID
|
||||
* @param id - The relay station ID
|
||||
* @returns Promise resolving to the relay station
|
||||
*/
|
||||
async relayStationGet(id: string): Promise<RelayStation> {
|
||||
try {
|
||||
return await invoke<RelayStation>("relay_station_get", { id });
|
||||
} catch (error) {
|
||||
console.error("Failed to get relay station:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new relay station
|
||||
* @param request - The relay station creation request
|
||||
* @returns Promise resolving to the created relay station
|
||||
*/
|
||||
async relayStationCreate(request: CreateRelayStationRequest): Promise<RelayStation> {
|
||||
try {
|
||||
return await invoke<RelayStation>("relay_station_create", { request });
|
||||
} catch (error) {
|
||||
console.error("Failed to create relay station:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates an existing relay station
|
||||
* @param request - The relay station update request
|
||||
* @returns Promise resolving to the updated relay station
|
||||
*/
|
||||
async relayStationUpdate(request: UpdateRelayStationRequest): Promise<RelayStation> {
|
||||
try {
|
||||
return await invoke<RelayStation>("relay_station_update", { request });
|
||||
} catch (error) {
|
||||
console.error("Failed to update relay station:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes a relay station
|
||||
* @param id - The relay station ID
|
||||
* @returns Promise resolving to success message
|
||||
*/
|
||||
async relayStationDelete(id: string): Promise<string> {
|
||||
try {
|
||||
return await invoke<string>("relay_station_delete", { id });
|
||||
} catch (error) {
|
||||
console.error("Failed to delete relay station:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles relay station enable status (ensures only one station is enabled)
|
||||
* @param id - The relay station ID
|
||||
* @param enabled - Whether to enable or disable the station
|
||||
* @returns Promise resolving to success message
|
||||
*/
|
||||
async relayStationToggleEnable(id: string, enabled: boolean): Promise<string> {
|
||||
try {
|
||||
return await invoke<string>("relay_station_toggle_enable", { id, enabled });
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle relay station enable status:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Syncs relay station config to Claude settings.json
|
||||
* @returns Promise resolving to sync result message
|
||||
*/
|
||||
async relayStationSyncConfig(): Promise<string> {
|
||||
try {
|
||||
return await invoke<string>("relay_station_sync_config");
|
||||
} catch (error) {
|
||||
console.error("Failed to sync relay station config:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Restores Claude config from backup
|
||||
* @returns Promise resolving to restore result message
|
||||
*/
|
||||
async relayStationRestoreConfig(): Promise<string> {
|
||||
try {
|
||||
return await invoke<string>("relay_station_restore_config");
|
||||
} catch (error) {
|
||||
console.error("Failed to restore config:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets current API config from Claude settings
|
||||
* @returns Promise resolving to current config info
|
||||
*/
|
||||
async relayStationGetCurrentConfig(): Promise<Record<string, string | null>> {
|
||||
try {
|
||||
return await invoke<Record<string, string | null>>("relay_station_get_current_config");
|
||||
} catch (error) {
|
||||
console.error("Failed to get current config:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets relay station information
|
||||
* @param stationId - The relay station ID
|
||||
* @returns Promise resolving to station information
|
||||
*/
|
||||
async relayStationGetInfo(stationId: string): Promise<StationInfo> {
|
||||
try {
|
||||
return await invoke<StationInfo>("relay_station_get_info", { stationId });
|
||||
} catch (error) {
|
||||
console.error("Failed to get station info:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets user information from relay station
|
||||
* @param stationId - The relay station ID
|
||||
* @param userId - The user ID
|
||||
* @returns Promise resolving to user information
|
||||
*/
|
||||
async relayStationGetUserInfo(stationId: string, userId: string): Promise<UserInfo> {
|
||||
try {
|
||||
return await invoke<UserInfo>("relay_station_get_user_info", { stationId, userId });
|
||||
} catch (error) {
|
||||
console.error("Failed to get user info:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Tests relay station connection
|
||||
* @param stationId - The relay station ID
|
||||
* @returns Promise resolving to connection test result
|
||||
*/
|
||||
async relayStationTestConnection(stationId: string): Promise<ConnectionTestResult> {
|
||||
try {
|
||||
return await invoke<ConnectionTestResult>("relay_station_test_connection", { stationId });
|
||||
} catch (error) {
|
||||
console.error("Failed to test connection:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets usage logs from relay station
|
||||
* @param stationId - The relay station ID
|
||||
* @param userId - The user ID
|
||||
* @param page - Page number (optional)
|
||||
* @param size - Page size (optional)
|
||||
* @returns Promise resolving to usage logs
|
||||
*/
|
||||
async relayStationGetUsageLogs(
|
||||
stationId: string,
|
||||
userId: string,
|
||||
page?: number,
|
||||
size?: number
|
||||
): Promise<any> {
|
||||
try {
|
||||
return await invoke<any>("relay_station_get_usage_logs", { stationId, userId, page, size });
|
||||
} catch (error) {
|
||||
console.error("Failed to get usage logs:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Lists tokens from relay station
|
||||
* @param stationId - The relay station ID
|
||||
* @param page - Page number (optional)
|
||||
* @param size - Page size (optional)
|
||||
* @returns Promise resolving to token pagination response
|
||||
*/
|
||||
async relayStationListTokens(
|
||||
stationId: string,
|
||||
page?: number,
|
||||
size?: number
|
||||
): Promise<TokenPaginationResponse> {
|
||||
try {
|
||||
return await invoke<TokenPaginationResponse>("relay_station_list_tokens", { stationId, page, size });
|
||||
} catch (error) {
|
||||
console.error("Failed to list tokens:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new token on relay station
|
||||
* @param stationId - The relay station ID
|
||||
* @param name - Token name
|
||||
* @param quota - Token quota (optional)
|
||||
* @returns Promise resolving to created token info
|
||||
*/
|
||||
async relayStationCreateToken(
|
||||
stationId: string,
|
||||
name: string,
|
||||
quota?: number
|
||||
): Promise<TokenInfo> {
|
||||
try {
|
||||
return await invoke<TokenInfo>("relay_station_create_token", { stationId, name, quota });
|
||||
} catch (error) {
|
||||
console.error("Failed to create token:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates a token on relay station
|
||||
* @param stationId - The relay station ID
|
||||
* @param tokenId - The token ID
|
||||
* @param name - New token name (optional)
|
||||
* @param quota - New token quota (optional)
|
||||
* @returns Promise resolving to updated token info
|
||||
*/
|
||||
async relayStationUpdateToken(
|
||||
stationId: string,
|
||||
tokenId: string,
|
||||
name?: string,
|
||||
quota?: number
|
||||
): Promise<TokenInfo> {
|
||||
try {
|
||||
return await invoke<TokenInfo>("relay_station_update_token", { stationId, tokenId, name, quota });
|
||||
} catch (error) {
|
||||
console.error("Failed to update token:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes a token from relay station
|
||||
* @param stationId - The relay station ID
|
||||
* @param tokenId - The token ID
|
||||
* @returns Promise resolving to success message
|
||||
*/
|
||||
async relayStationDeleteToken(stationId: string, tokenId: string): Promise<string> {
|
||||
try {
|
||||
return await invoke<string>("relay_station_delete_token", { stationId, tokenId });
|
||||
} catch (error) {
|
||||
console.error("Failed to delete token:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -42,9 +42,12 @@
|
||||
"settings": "Settings",
|
||||
"usage": "Usage Dashboard",
|
||||
"mcp": "MCP Manager",
|
||||
"relayStations": "Relay Stations",
|
||||
"about": "About"
|
||||
},
|
||||
"welcome": {
|
||||
"relayStationManagement": "Relay Stations",
|
||||
"relayStationManagementDesc": "Manage Claude API relay stations",
|
||||
"agentManagement": "Agent Management",
|
||||
"agentManagementDesc": "Create and manage intelligent agents",
|
||||
"projectManagement": "Project Management",
|
||||
@@ -705,5 +708,77 @@
|
||||
"input": {
|
||||
"pressEnterToSend": "Press Enter to send, Shift+Enter for new line",
|
||||
"withFileAndCommandSupport": ", @ to mention files, / for commands, drag & drop or paste images"
|
||||
},
|
||||
"relayStation": {
|
||||
"title": "Relay Stations",
|
||||
"description": "Manage Claude API relay stations for network access",
|
||||
"create": "Add Station",
|
||||
"createTitle": "Create Relay Station",
|
||||
"createFirst": "Create First Station",
|
||||
"edit": "Edit Station",
|
||||
"editTitle": "Edit Relay Station",
|
||||
"delete": "Delete Station",
|
||||
"deleteConfirm": "Are you sure you want to delete this relay station?",
|
||||
"noStations": "No relay stations configured",
|
||||
"noStationsDesc": "Add a relay station to access Claude API through proxy services",
|
||||
"name": "Station Name",
|
||||
"namePlaceholder": "My Relay Station",
|
||||
"nameRequired": "Station name is required",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "Optional description for this station",
|
||||
"apiUrl": "API URL",
|
||||
"apiUrlRequired": "API URL is required",
|
||||
"adapterType": "Adapter Type",
|
||||
"authMethod": "Authentication Method",
|
||||
"systemToken": "System Token",
|
||||
"tokenPlaceholder": "Enter your API token",
|
||||
"tokenRequired": "System token is required",
|
||||
"userId": "User ID",
|
||||
"userIdPlaceholder": "Required for NewAPI/OneAPI",
|
||||
"enabled": "Enabled",
|
||||
"testConnection": "Test Connection",
|
||||
"connectionSuccess": "Connection successful",
|
||||
"connectionFailed": "Connection test failed",
|
||||
"createSuccess": "Relay station created successfully",
|
||||
"createFailed": "Failed to create relay station",
|
||||
"updateSuccess": "Relay station updated successfully",
|
||||
"updateFailed": "Failed to update relay station",
|
||||
"deleteSuccess": "Relay station deleted successfully",
|
||||
"deleteFailed": "Failed to delete relay station",
|
||||
"loadFailed": "Failed to load relay stations",
|
||||
"custom": "Custom",
|
||||
"enabledSuccess": "Relay station enabled successfully",
|
||||
"disabledSuccess": "Relay station disabled successfully",
|
||||
"toggleEnableFailed": "Failed to toggle relay station status",
|
||||
"syncConfig": "Sync Config",
|
||||
"syncFailed": "Failed to sync configuration",
|
||||
"currentConfig": "Current Configuration",
|
||||
"notConfigured": "Not configured",
|
||||
"configLocation": "Config file location"
|
||||
},
|
||||
"status": {
|
||||
"connected": "Connected",
|
||||
"disconnected": "Disconnected",
|
||||
"unknown": "Unknown",
|
||||
"disabled": "Disabled",
|
||||
"enabled": "Enabled"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Loading...",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"create": "Create",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"updating": "Updating..."
|
||||
},
|
||||
"error": {
|
||||
"title": "Error"
|
||||
},
|
||||
"success": {
|
||||
"title": "Success"
|
||||
},
|
||||
"warning": {
|
||||
"title": "Warning"
|
||||
}
|
||||
}
|
@@ -39,9 +39,12 @@
|
||||
"settings": "设置",
|
||||
"usage": "用量仪表板",
|
||||
"mcp": "MCP 管理器",
|
||||
"relayStations": "中转站",
|
||||
"about": "关于"
|
||||
},
|
||||
"welcome": {
|
||||
"relayStationManagement": "中转站管理",
|
||||
"relayStationManagementDesc": "管理 Claude API 中转站",
|
||||
"agentManagement": "Agent 管理",
|
||||
"agentManagementDesc": "创建和管理智能 Agent",
|
||||
"projectManagement": "项目管理",
|
||||
@@ -632,6 +635,78 @@
|
||||
"input": {
|
||||
"pressEnterToSend": "按 Enter 发送,Shift+Enter 换行",
|
||||
"withFileAndCommandSupport": ",@ 提及文件,/ 调用命令,拖拽或粘贴图片"
|
||||
},
|
||||
"relayStation": {
|
||||
"title": "中转站",
|
||||
"description": "管理 Claude API 中转站以实现网络访问",
|
||||
"create": "添加中转站",
|
||||
"createTitle": "创建中转站",
|
||||
"createFirst": "创建第一个中转站",
|
||||
"edit": "编辑中转站",
|
||||
"editTitle": "编辑中转站",
|
||||
"delete": "删除中转站",
|
||||
"deleteConfirm": "确定要删除这个中转站吗?",
|
||||
"noStations": "未配置中转站",
|
||||
"noStationsDesc": "添加中转站以通过代理服务访问 Claude API",
|
||||
"name": "中转站名称",
|
||||
"namePlaceholder": "我的中转站",
|
||||
"nameRequired": "中转站名称必填",
|
||||
"description": "描述",
|
||||
"descriptionPlaceholder": "此中转站的可选描述",
|
||||
"apiUrl": "API 地址",
|
||||
"apiUrlRequired": "API 地址必填",
|
||||
"adapterType": "适配器类型",
|
||||
"authMethod": "认证方式",
|
||||
"systemToken": "系统令牌",
|
||||
"tokenPlaceholder": "输入您的 API 令牌",
|
||||
"tokenRequired": "系统令牌必填",
|
||||
"userId": "用户 ID",
|
||||
"userIdPlaceholder": "NewAPI/OneAPI 必需",
|
||||
"enabled": "启用",
|
||||
"testConnection": "测试连接",
|
||||
"connectionSuccess": "连接成功",
|
||||
"connectionFailed": "连接测试失败",
|
||||
"createSuccess": "中转站创建成功",
|
||||
"createFailed": "创建中转站失败",
|
||||
"updateSuccess": "中转站更新成功",
|
||||
"updateFailed": "更新中转站失败",
|
||||
"deleteSuccess": "中转站删除成功",
|
||||
"deleteFailed": "删除中转站失败",
|
||||
"loadFailed": "加载中转站失败",
|
||||
"custom": "自定义",
|
||||
"enabledSuccess": "中转站启用成功",
|
||||
"disabledSuccess": "中转站禁用成功",
|
||||
"toggleEnableFailed": "切换中转站状态失败",
|
||||
"syncConfig": "同步配置",
|
||||
"syncFailed": "同步配置失败",
|
||||
"currentConfig": "当前配置",
|
||||
"notConfigured": "未配置",
|
||||
"configLocation": "配置文件位置"
|
||||
},
|
||||
"status": {
|
||||
"connected": "已连接",
|
||||
"disconnected": "已断开",
|
||||
"unknown": "未知",
|
||||
"disabled": "已禁用",
|
||||
"enabled": "已启用"
|
||||
},
|
||||
"common": {
|
||||
"loading": "加载中...",
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
"create": "创建",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"updating": "更新中..."
|
||||
},
|
||||
"error": {
|
||||
"title": "错误"
|
||||
},
|
||||
"success": {
|
||||
"title": "成功"
|
||||
},
|
||||
"warning": {
|
||||
"title": "警告"
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user