修改计算规则
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="dark">
|
||||
<html lang="en" class="theme-gray">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
|
@@ -12,24 +12,20 @@ pub struct ClaudeConfig {
|
||||
pub env: ClaudeEnv,
|
||||
#[serde(default)]
|
||||
pub permissions: Option<ClaudePermissions>,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub model: Option<String>,
|
||||
#[serde(rename = "apiKeyHelper")]
|
||||
#[serde(rename = "apiKeyHelper", skip_serializing_if = "Option::is_none")]
|
||||
pub api_key_helper: Option<String>,
|
||||
#[serde(flatten)]
|
||||
pub other: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ClaudeEnv {
|
||||
#[serde(rename = "ANTHROPIC_AUTH_TOKEN")]
|
||||
#[serde(rename = "ANTHROPIC_AUTH_TOKEN", skip_serializing_if = "Option::is_none")]
|
||||
pub anthropic_auth_token: Option<String>,
|
||||
#[serde(rename = "ANTHROPIC_BASE_URL")]
|
||||
#[serde(rename = "ANTHROPIC_BASE_URL", skip_serializing_if = "Option::is_none")]
|
||||
pub anthropic_base_url: Option<String>,
|
||||
#[serde(rename = "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC")]
|
||||
#[serde(rename = "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", skip_serializing_if = "Option::is_none")]
|
||||
pub disable_nonessential_traffic: Option<String>,
|
||||
#[serde(flatten)]
|
||||
pub other: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
@@ -63,32 +59,63 @@ pub fn read_claude_config() -> Result<ClaudeConfig, String> {
|
||||
permissions: Some(ClaudePermissions::default()),
|
||||
model: None,
|
||||
api_key_helper: None,
|
||||
other: json!({}),
|
||||
});
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&config_path)
|
||||
.map_err(|e| format!("读取配置文件失败: {}", e))?;
|
||||
|
||||
serde_json::from_str(&content)
|
||||
.map_err(|e| format!("解析配置文件失败: {}", e))
|
||||
// 首先尝试解析为 JSON Value,以便处理可能的格式问题
|
||||
let mut json_value: Value = serde_json::from_str(&content)
|
||||
.map_err(|e| format!("解析配置文件失败: {}", e))?;
|
||||
|
||||
// 如果JSON解析成功,再转换为ClaudeConfig
|
||||
if let Some(obj) = json_value.as_object_mut() {
|
||||
// 确保必要的字段存在
|
||||
if !obj.contains_key("env") {
|
||||
obj.insert("env".to_string(), json!({}));
|
||||
}
|
||||
}
|
||||
|
||||
serde_json::from_value(json_value)
|
||||
.map_err(|e| format!("转换配置结构失败: {}", e))
|
||||
}
|
||||
|
||||
/// 写入 Claude 配置文件
|
||||
pub fn write_claude_config(config: &ClaudeConfig) -> Result<(), String> {
|
||||
let config_path = get_claude_config_path()?;
|
||||
|
||||
log::info!("尝试写入配置文件到: {:?}", config_path);
|
||||
|
||||
// 确保目录存在
|
||||
if let Some(parent) = config_path.parent() {
|
||||
log::info!("确保目录存在: {:?}", parent);
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("创建配置目录失败: {}", e))?;
|
||||
.map_err(|e| {
|
||||
let error_msg = format!("创建配置目录失败: {}", e);
|
||||
log::error!("{}", error_msg);
|
||||
error_msg
|
||||
})?;
|
||||
}
|
||||
|
||||
let content = serde_json::to_string_pretty(config)
|
||||
.map_err(|e| format!("序列化配置失败: {}", e))?;
|
||||
.map_err(|e| {
|
||||
let error_msg = format!("序列化配置失败: {}", e);
|
||||
log::error!("{}", error_msg);
|
||||
error_msg
|
||||
})?;
|
||||
|
||||
fs::write(&config_path, content)
|
||||
.map_err(|e| format!("写入配置文件失败: {}", e))
|
||||
log::info!("准备写入内容:\n{}", content);
|
||||
|
||||
fs::write(&config_path, &content)
|
||||
.map_err(|e| {
|
||||
let error_msg = format!("写入配置文件失败: {} (路径: {:?})", e, config_path);
|
||||
log::error!("{}", error_msg);
|
||||
error_msg
|
||||
})?;
|
||||
|
||||
log::info!("配置文件写入成功: {:?}", config_path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 备份当前配置
|
||||
@@ -137,8 +164,11 @@ pub fn apply_relay_station_to_config(station: &RelayStation) -> Result<(), Strin
|
||||
// 格式:echo 'token'
|
||||
config.api_key_helper = Some(format!("echo '{}'", station.system_token));
|
||||
|
||||
// 如果是自定义适配器,可能需要特殊处理
|
||||
// 如果是特定适配器,可能需要特殊处理
|
||||
match station.adapter.as_str() {
|
||||
"packycode" => {
|
||||
// PackyCode 使用原始配置,不做特殊处理
|
||||
}
|
||||
"newapi" | "oneapi" => {
|
||||
// NewAPI 和 OneAPI 兼容 OpenAI 格式,不需要特殊处理
|
||||
}
|
||||
|
@@ -8,3 +8,4 @@ pub mod proxy;
|
||||
pub mod language;
|
||||
pub mod relay_stations;
|
||||
pub mod relay_adapters;
|
||||
pub mod packycode_nodes;
|
||||
|
216
src-tauri/src/commands/packycode_nodes.rs
Normal file
216
src-tauri/src/commands/packycode_nodes.rs
Normal file
@@ -0,0 +1,216 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::{Duration, Instant};
|
||||
use reqwest::Client;
|
||||
use tauri::command;
|
||||
use anyhow::Result;
|
||||
|
||||
/// PackyCode 节点类型
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum NodeType {
|
||||
Direct, // 直连节点
|
||||
Backup, // 备用节点
|
||||
Emergency, // 紧急节点(非紧急情况不要使用)
|
||||
}
|
||||
|
||||
/// PackyCode 节点信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PackycodeNode {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub node_type: NodeType,
|
||||
pub description: String,
|
||||
pub response_time: Option<u64>, // 响应时间(毫秒)
|
||||
pub available: Option<bool>, // 是否可用
|
||||
}
|
||||
|
||||
/// 节点测速结果
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NodeSpeedTestResult {
|
||||
pub node: PackycodeNode,
|
||||
pub response_time: u64,
|
||||
pub success: bool,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// 获取所有 PackyCode 节点
|
||||
pub fn get_all_nodes() -> Vec<PackycodeNode> {
|
||||
vec![
|
||||
// 直连节点
|
||||
PackycodeNode {
|
||||
name: "直连1".to_string(),
|
||||
url: "https://api.packycode.com".to_string(),
|
||||
node_type: NodeType::Direct,
|
||||
description: "默认直连节点".to_string(),
|
||||
response_time: None,
|
||||
available: None,
|
||||
},
|
||||
PackycodeNode {
|
||||
name: "直连2 (HK-CN2)".to_string(),
|
||||
url: "https://api-hk-cn2.packycode.com".to_string(),
|
||||
node_type: NodeType::Direct,
|
||||
description: "香港 CN2 线路".to_string(),
|
||||
response_time: None,
|
||||
available: None,
|
||||
},
|
||||
PackycodeNode {
|
||||
name: "直连3 (US-CMIN2)".to_string(),
|
||||
url: "https://api-us-cmin2.packycode.com".to_string(),
|
||||
node_type: NodeType::Direct,
|
||||
description: "美国 CMIN2 线路".to_string(),
|
||||
response_time: None,
|
||||
available: None,
|
||||
},
|
||||
PackycodeNode {
|
||||
name: "直连4 (US-4837)".to_string(),
|
||||
url: "https://api-us-4837.packycode.com".to_string(),
|
||||
node_type: NodeType::Direct,
|
||||
description: "美国 4837 线路".to_string(),
|
||||
response_time: None,
|
||||
available: None,
|
||||
},
|
||||
// 备用节点
|
||||
PackycodeNode {
|
||||
name: "备用1 (US-CN2)".to_string(),
|
||||
url: "https://api-us-cn2.packycode.com".to_string(),
|
||||
node_type: NodeType::Backup,
|
||||
description: "美国 CN2 备用线路".to_string(),
|
||||
response_time: None,
|
||||
available: None,
|
||||
},
|
||||
PackycodeNode {
|
||||
name: "备用2 (CF-Pro)".to_string(),
|
||||
url: "https://api-cf-pro.packycode.com".to_string(),
|
||||
node_type: NodeType::Backup,
|
||||
description: "CloudFlare Pro 备用线路".to_string(),
|
||||
response_time: None,
|
||||
available: None,
|
||||
},
|
||||
// 紧急节点
|
||||
PackycodeNode {
|
||||
name: "测试节点1".to_string(),
|
||||
url: "https://api-test.packyme.com".to_string(),
|
||||
node_type: NodeType::Emergency,
|
||||
description: "测试节点(非紧急情况勿用)".to_string(),
|
||||
response_time: None,
|
||||
available: None,
|
||||
},
|
||||
PackycodeNode {
|
||||
name: "测试节点2".to_string(),
|
||||
url: "https://api-test-custom.packycode.com".to_string(),
|
||||
node_type: NodeType::Emergency,
|
||||
description: "自定义测试节点(非紧急情况勿用)".to_string(),
|
||||
response_time: None,
|
||||
available: None,
|
||||
},
|
||||
PackycodeNode {
|
||||
name: "测试节点3".to_string(),
|
||||
url: "https://api-tmp-test.dzz.ai".to_string(),
|
||||
node_type: NodeType::Emergency,
|
||||
description: "临时测试节点(非紧急情况勿用)".to_string(),
|
||||
response_time: None,
|
||||
available: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/// 测试单个节点速度(仅测试网络延时,不需要认证)
|
||||
async fn test_node_speed(node: &PackycodeNode, _token: &str) -> NodeSpeedTestResult {
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()
|
||||
.unwrap_or_else(|_| Client::new());
|
||||
|
||||
let start_time = Instant::now();
|
||||
|
||||
// 只需要测试服务器的可达性和延时,使用简单的 HEAD 请求
|
||||
match client
|
||||
.head(&node.url)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(_response) => {
|
||||
let response_time = start_time.elapsed().as_millis() as u64;
|
||||
|
||||
// 只要能连接到服务器就算成功,不管返回什么状态码
|
||||
NodeSpeedTestResult {
|
||||
node: PackycodeNode {
|
||||
response_time: Some(response_time),
|
||||
available: Some(true),
|
||||
..node.clone()
|
||||
},
|
||||
response_time,
|
||||
success: true,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let response_time = start_time.elapsed().as_millis() as u64;
|
||||
NodeSpeedTestResult {
|
||||
node: PackycodeNode {
|
||||
response_time: Some(response_time),
|
||||
available: Some(false),
|
||||
..node.clone()
|
||||
},
|
||||
response_time,
|
||||
success: false,
|
||||
error: Some(format!("连接失败: {}", e.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 测试所有节点速度
|
||||
#[command]
|
||||
pub async fn test_all_packycode_nodes(token: String) -> Result<Vec<NodeSpeedTestResult>, String> {
|
||||
let nodes = get_all_nodes();
|
||||
let mut results = Vec::new();
|
||||
|
||||
for node in nodes {
|
||||
let result = test_node_speed(&node, &token).await;
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
// 按响应时间排序
|
||||
results.sort_by(|a, b| {
|
||||
match (a.success, b.success) {
|
||||
(true, false) => std::cmp::Ordering::Less,
|
||||
(false, true) => std::cmp::Ordering::Greater,
|
||||
_ => a.response_time.cmp(&b.response_time),
|
||||
}
|
||||
});
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// 自动选择最快的节点(仅从直连和备用中选择)
|
||||
#[command]
|
||||
pub async fn auto_select_best_node(token: String) -> Result<PackycodeNode, String> {
|
||||
let nodes = get_all_nodes();
|
||||
let mut best_node: Option<(PackycodeNode, u64)> = None;
|
||||
|
||||
// 只测试直连和备用节点
|
||||
for node in nodes.iter().filter(|n| matches!(n.node_type, NodeType::Direct | NodeType::Backup)) {
|
||||
let result = test_node_speed(node, &token).await;
|
||||
|
||||
if result.success {
|
||||
match &best_node {
|
||||
None => best_node = Some((result.node, result.response_time)),
|
||||
Some((_, best_time)) if result.response_time < *best_time => {
|
||||
best_node = Some((result.node, result.response_time));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
best_node
|
||||
.map(|(node, _)| node)
|
||||
.ok_or_else(|| "No available nodes found".to_string())
|
||||
}
|
||||
|
||||
/// 获取节点列表(不测速)
|
||||
#[command]
|
||||
pub fn get_packycode_nodes() -> Vec<PackycodeNode> {
|
||||
get_all_nodes()
|
||||
}
|
@@ -51,6 +51,237 @@ pub trait StationAdapter: Send + Sync {
|
||||
async fn delete_token(&self, station: &RelayStation, token_id: &str) -> Result<String>;
|
||||
}
|
||||
|
||||
/// PackyCode 适配器(默认使用 API Key 认证)
|
||||
pub struct PackycodeAdapter;
|
||||
|
||||
#[async_trait]
|
||||
impl StationAdapter for PackycodeAdapter {
|
||||
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!("sk-{}", 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("PackyCode")
|
||||
.to_string(),
|
||||
announcement: data.get("announcement")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string()),
|
||||
api_url: station.api_url.clone(),
|
||||
version: data.get("version")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string()),
|
||||
metadata: Some({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("adapter_type".to_string(), json!("packycode"));
|
||||
map.insert("service_type".to_string(), json!(
|
||||
if station.api_url.contains("share.packycode.com") {
|
||||
"bus" // 公交车
|
||||
} else {
|
||||
"taxi" // 滴滴车
|
||||
}
|
||||
));
|
||||
if let Some(quota_per_unit) = data.get("quota_per_unit").and_then(|v| v.as_i64()) {
|
||||
map.insert("quota_per_unit".to_string(), json!(quota_per_unit));
|
||||
}
|
||||
map
|
||||
}),
|
||||
quota_per_unit: data.get("quota_per_unit").and_then(|v| v.as_i64()),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_user_info(&self, station: &RelayStation, user_id: &str) -> Result<UserInfo> {
|
||||
let url = format!("{}/api/user/self", station.api_url.trim_end_matches('/'));
|
||||
|
||||
let response = HTTP_CLIENT
|
||||
.get(&url)
|
||||
.header("Authorization", format!("sk-{}", station.system_token))
|
||||
.header("X-User-Id", user_id)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let data: Value = response.json().await?;
|
||||
|
||||
if !data.get("success").and_then(|v| v.as_bool()).unwrap_or(false) {
|
||||
return Err(anyhow::anyhow!("API Error: {}",
|
||||
data.get("message").and_then(|v| v.as_str()).unwrap_or("Unknown error")));
|
||||
}
|
||||
|
||||
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!("sk-{}", 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!("sk-{}", station.system_token))
|
||||
.header("X-User-Id", user_id)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let data: Value = response.json().await?;
|
||||
|
||||
if !data.get("success").and_then(|v| v.as_bool()).unwrap_or(false) {
|
||||
return Err(anyhow::anyhow!("API Error: {}",
|
||||
data.get("message").and_then(|v| v.as_str()).unwrap_or("Unknown error")));
|
||||
}
|
||||
|
||||
Ok(data.get("data").cloned().unwrap_or(json!([])))
|
||||
}
|
||||
|
||||
async fn list_tokens(&self, station: &RelayStation, page: Option<usize>, size: Option<usize>) -> Result<TokenPaginationResponse> {
|
||||
// PackyCode 使用简化的 Token 管理
|
||||
let page = page.unwrap_or(1);
|
||||
let size = size.unwrap_or(10);
|
||||
|
||||
// 返回当前使用的 API Key 作为唯一 Token
|
||||
let token = TokenInfo {
|
||||
id: "1".to_string(),
|
||||
name: "API Key".to_string(),
|
||||
token: format!("sk-{}...", &station.system_token[..8]),
|
||||
quota: None,
|
||||
used_quota: None,
|
||||
status: "active".to_string(),
|
||||
created_at: station.created_at,
|
||||
updated_at: station.updated_at,
|
||||
};
|
||||
|
||||
Ok(TokenPaginationResponse {
|
||||
tokens: vec![token],
|
||||
total: 1,
|
||||
page,
|
||||
size,
|
||||
has_more: false,
|
||||
})
|
||||
}
|
||||
|
||||
async fn create_token(&self, _station: &RelayStation, _name: &str, _quota: Option<i64>) -> Result<TokenInfo> {
|
||||
Err(anyhow::anyhow!(i18n::t("relay_adapter.packycode_single_token")))
|
||||
}
|
||||
|
||||
async fn update_token(&self, _station: &RelayStation, _token_id: &str, _name: Option<&str>, _quota: Option<i64>) -> Result<TokenInfo> {
|
||||
Err(anyhow::anyhow!(i18n::t("relay_adapter.packycode_single_token")))
|
||||
}
|
||||
|
||||
async fn delete_token(&self, _station: &RelayStation, _token_id: &str) -> Result<String> {
|
||||
Err(anyhow::anyhow!(i18n::t("relay_adapter.packycode_single_token")))
|
||||
}
|
||||
}
|
||||
|
||||
/// NewAPI 适配器(支持 NewAPI 和 OneAPI)
|
||||
pub struct NewApiAdapter;
|
||||
|
||||
@@ -575,7 +806,7 @@ impl StationAdapter for CustomAdapter {
|
||||
// Custom 适配器跳过连接测试,直接返回成功
|
||||
Ok(ConnectionTestResult {
|
||||
success: true,
|
||||
response_time: Some(0),
|
||||
response_time: None, // 不显示响应时间
|
||||
message: i18n::t("relay_adapter.custom_no_test"),
|
||||
error: None,
|
||||
})
|
||||
@@ -605,6 +836,7 @@ impl StationAdapter for CustomAdapter {
|
||||
/// 适配器工厂函数
|
||||
pub fn create_adapter(adapter_type: &RelayStationAdapter) -> Box<dyn StationAdapter> {
|
||||
match adapter_type {
|
||||
RelayStationAdapter::Packycode => Box::new(PackycodeAdapter),
|
||||
RelayStationAdapter::Newapi => Box::new(NewApiAdapter),
|
||||
RelayStationAdapter::Oneapi => Box::new(NewApiAdapter), // OneAPI 兼容 NewAPI
|
||||
RelayStationAdapter::Yourapi => Box::new(YourApiAdapter::new()),
|
||||
|
@@ -14,6 +14,7 @@ use crate::claude_config;
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RelayStationAdapter {
|
||||
Packycode, // PackyCode 平台(放在第一位)
|
||||
Newapi, // NewAPI 兼容平台
|
||||
Oneapi, // OneAPI 兼容平台
|
||||
Yourapi, // YourAPI 特定平台
|
||||
@@ -23,6 +24,7 @@ pub enum RelayStationAdapter {
|
||||
impl RelayStationAdapter {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
RelayStationAdapter::Packycode => "packycode",
|
||||
RelayStationAdapter::Newapi => "newapi",
|
||||
RelayStationAdapter::Oneapi => "oneapi",
|
||||
RelayStationAdapter::Yourapi => "yourapi",
|
||||
@@ -520,12 +522,10 @@ pub async fn relay_station_toggle_enable(
|
||||
let station = relay_station_get_internal(&conn, &id)?;
|
||||
|
||||
// 将中转站配置应用到 Claude 配置文件
|
||||
if let Err(e) = claude_config::apply_relay_station_to_config(&station) {
|
||||
claude_config::apply_relay_station_to_config(&station).map_err(|e| {
|
||||
log::error!("Failed to apply relay station config: {}", e);
|
||||
// 不中断流程,但记录错误
|
||||
} else {
|
||||
log::info!("Applied relay station config to Claude settings");
|
||||
}
|
||||
format!("配置文件写入失败: {}", e)
|
||||
})?;
|
||||
} else {
|
||||
// 如果禁用中转站,清除 Claude 配置中的相关设置
|
||||
if let Err(e) = claude_config::clear_relay_station_from_config() {
|
||||
|
@@ -45,6 +45,7 @@ impl SimpleI18n {
|
||||
|
||||
// Relay Station English translations
|
||||
("en-US", "relay_adapter.custom_no_test") => "Custom configuration, connection test skipped".to_string(),
|
||||
("en-US", "relay_adapter.packycode_single_token") => "PackyCode only supports single API key".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(),
|
||||
@@ -65,6 +66,7 @@ impl SimpleI18n {
|
||||
|
||||
// Relay Station Chinese translations
|
||||
("zh-CN", "relay_adapter.custom_no_test") => "自定义配置,跳过连接测试".to_string(),
|
||||
("zh-CN", "relay_adapter.packycode_single_token") => "PackyCode 仅支持单个 API 密钥".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(),
|
||||
|
@@ -56,6 +56,9 @@ use commands::relay_adapters::{
|
||||
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 commands::packycode_nodes::{
|
||||
test_all_packycode_nodes, auto_select_best_node, get_packycode_nodes,
|
||||
};
|
||||
use process::ProcessRegistryState;
|
||||
use std::sync::Mutex;
|
||||
use tauri::Manager;
|
||||
@@ -286,6 +289,11 @@ fn main() {
|
||||
relay_station_create_token,
|
||||
relay_station_update_token,
|
||||
relay_station_delete_token,
|
||||
|
||||
// PackyCode Nodes
|
||||
test_all_packycode_nodes,
|
||||
auto_select_best_node,
|
||||
get_packycode_nodes,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -58,7 +58,7 @@ const DEFAULT_CUSTOM_COLORS: CustomThemeColors = {
|
||||
};
|
||||
|
||||
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [theme, setThemeState] = useState<ThemeMode>('dark');
|
||||
const [theme, setThemeState] = useState<ThemeMode>('gray');
|
||||
const [customColors, setCustomColorsState] = useState<CustomThemeColors>(DEFAULT_CUSTOM_COLORS);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
@@ -73,6 +73,9 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre
|
||||
const themeMode = savedTheme as ThemeMode;
|
||||
setThemeState(themeMode);
|
||||
applyTheme(themeMode, customColors);
|
||||
} else {
|
||||
// Apply default theme if no saved preference
|
||||
applyTheme('gray', customColors);
|
||||
}
|
||||
|
||||
// Load custom colors
|
||||
|
@@ -446,6 +446,7 @@ export interface ImportServerResult {
|
||||
|
||||
/** 中转站适配器类型 */
|
||||
export type RelayStationAdapter =
|
||||
| 'packycode' // PackyCode 平台(默认)
|
||||
| 'newapi' // NewAPI 兼容平台
|
||||
| 'oneapi' // OneAPI 兼容平台
|
||||
| 'yourapi' // YourAPI 特定平台
|
||||
@@ -548,7 +549,32 @@ export interface TokenPaginationResponse {
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
has_more: boolean;
|
||||
}
|
||||
|
||||
// ============= PackyCode Nodes =============
|
||||
|
||||
/** PackyCode 节点类型 */
|
||||
export type NodeType =
|
||||
| 'direct' // 直连节点
|
||||
| 'backup' // 备用节点
|
||||
| 'emergency'; // 紧急节点
|
||||
|
||||
/** PackyCode 节点信息 */
|
||||
export interface PackycodeNode {
|
||||
name: string; // 节点名称
|
||||
url: string; // 节点 URL
|
||||
node_type: NodeType; // 节点类型
|
||||
description: string; // 节点描述
|
||||
response_time?: number; // 响应时间(毫秒)
|
||||
available?: boolean; // 是否可用
|
||||
}
|
||||
|
||||
/** 节点测速结果 */
|
||||
export interface NodeSpeedTestResult {
|
||||
node: PackycodeNode; // 节点信息
|
||||
response_time: number; // 响应时间
|
||||
success: boolean; // 是否成功
|
||||
error?: string; // 错误信息
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2303,5 +2329,48 @@ export const api = {
|
||||
console.error("Failed to delete token:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// ============= PackyCode Nodes =============
|
||||
|
||||
/**
|
||||
* Tests all PackyCode nodes and returns speed test results
|
||||
* @param token - API token for authentication
|
||||
* @returns Promise resolving to array of node speed test results
|
||||
*/
|
||||
async testAllPackycodeNodes(token: string): Promise<NodeSpeedTestResult[]> {
|
||||
try {
|
||||
return await invoke<NodeSpeedTestResult[]>("test_all_packycode_nodes", { token });
|
||||
} catch (error) {
|
||||
console.error("Failed to test PackyCode nodes:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Automatically selects the best PackyCode node based on speed
|
||||
* @param token - API token for authentication
|
||||
* @returns Promise resolving to the best node
|
||||
*/
|
||||
async autoSelectBestNode(token: string): Promise<PackycodeNode> {
|
||||
try {
|
||||
return await invoke<PackycodeNode>("auto_select_best_node", { token });
|
||||
} catch (error) {
|
||||
console.error("Failed to auto-select best node:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets all available PackyCode nodes
|
||||
* @returns Promise resolving to array of PackyCode nodes
|
||||
*/
|
||||
async getPackycodeNodes(): Promise<PackycodeNode[]> {
|
||||
try {
|
||||
return await invoke<PackycodeNode[]>("get_packycode_nodes");
|
||||
} catch (error) {
|
||||
console.error("Failed to get PackyCode nodes:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -719,6 +719,10 @@
|
||||
"editTitle": "Edit Relay Station",
|
||||
"delete": "Delete Station",
|
||||
"deleteConfirm": "Are you sure you want to delete this relay station?",
|
||||
"confirmDeleteTitle": "Confirm Delete",
|
||||
"deleteSuccess": "Successfully deleted",
|
||||
"createSuccess": "Successfully created",
|
||||
"updateSuccess": "Successfully updated",
|
||||
"noStations": "No relay stations configured",
|
||||
"noStationsDesc": "Add a relay station to access Claude API through proxy services",
|
||||
"name": "Station Name",
|
||||
@@ -754,7 +758,30 @@
|
||||
"syncFailed": "Failed to sync configuration",
|
||||
"currentConfig": "Current Configuration",
|
||||
"notConfigured": "Not configured",
|
||||
"configLocation": "Config file location"
|
||||
"configLocation": "Config file location",
|
||||
"serviceType": "Service Type",
|
||||
"taxiService": "Taxi",
|
||||
"busService": "Bus",
|
||||
"taxiServiceDesc": "Fast & Stable (share-api.packycode.com)",
|
||||
"busServiceDesc": "Shared Economy (api.packycode.com)",
|
||||
"selectService": "Select a service type",
|
||||
"fixedUrl": "Fixed URL",
|
||||
"busServiceNote": "Select a node or use auto-selection for optimal performance",
|
||||
"nodeSelection": "Node Selection",
|
||||
"selectNode": "Select a node",
|
||||
"autoSelect": "Auto-select fastest",
|
||||
"autoSelectDesc": "Will automatically test and select the fastest node",
|
||||
"selectedNode": "Selected",
|
||||
"testSpeed": "Test Speed",
|
||||
"testResults": "Speed Test Results",
|
||||
"failed": "Failed",
|
||||
"testCompleted": "Speed test completed",
|
||||
"testFailed": "Speed test failed",
|
||||
"autoSelectedNode": "Auto-selected node",
|
||||
"autoSelectFailed": "Failed to auto-select node",
|
||||
"selectingBestNode": "Testing nodes to find the fastest...",
|
||||
"packycodeTokenNote": "PackyCode uses API Key authentication only",
|
||||
"enabledNote": "Enable this station to make it available for use"
|
||||
},
|
||||
"status": {
|
||||
"connected": "Connected",
|
||||
|
@@ -646,6 +646,10 @@
|
||||
"editTitle": "编辑中转站",
|
||||
"delete": "删除中转站",
|
||||
"deleteConfirm": "确定要删除这个中转站吗?",
|
||||
"confirmDeleteTitle": "确认删除",
|
||||
"deleteSuccess": "删除成功",
|
||||
"createSuccess": "创建成功",
|
||||
"updateSuccess": "更新成功",
|
||||
"noStations": "未配置中转站",
|
||||
"noStationsDesc": "添加中转站以通过代理服务访问 Claude API",
|
||||
"name": "中转站名称",
|
||||
@@ -681,7 +685,30 @@
|
||||
"syncFailed": "同步配置失败",
|
||||
"currentConfig": "当前配置",
|
||||
"notConfigured": "未配置",
|
||||
"configLocation": "配置文件位置"
|
||||
"configLocation": "配置文件位置",
|
||||
"serviceType": "服务类型",
|
||||
"taxiService": "滴滴车",
|
||||
"busService": "公交车",
|
||||
"taxiServiceDesc": "高速稳定 (share-api.packycode.com)",
|
||||
"busServiceDesc": "共享经济 (api.packycode.com)",
|
||||
"selectService": "选择服务类型",
|
||||
"fixedUrl": "固定地址",
|
||||
"busServiceNote": "选择节点或使用自动选择以获得最佳性能",
|
||||
"nodeSelection": "节点选择",
|
||||
"selectNode": "选择节点",
|
||||
"autoSelect": "自动选择最快",
|
||||
"autoSelectDesc": "将自动测试并选择最快的节点",
|
||||
"selectedNode": "已选择",
|
||||
"testSpeed": "测速",
|
||||
"testResults": "测速结果",
|
||||
"failed": "失败",
|
||||
"testCompleted": "测速完成",
|
||||
"testFailed": "测速失败",
|
||||
"autoSelectedNode": "自动选择节点",
|
||||
"autoSelectFailed": "自动选择节点失败",
|
||||
"selectingBestNode": "正在测试节点以寻找最快的...",
|
||||
"packycodeTokenNote": "PackyCode 仅支持 API Key 认证方式",
|
||||
"enabledNote": "启用此中转站以使其可用"
|
||||
},
|
||||
"status": {
|
||||
"connected": "已连接",
|
||||
|
Reference in New Issue
Block a user