修改计算规则

This commit is contained in:
2025-08-08 12:27:56 +08:00
parent f3324b1107
commit 5016c1d9d6
13 changed files with 1462 additions and 132 deletions

View File

@@ -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" />

View File

@@ -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 格式,不需要特殊处理
}

View File

@@ -8,3 +8,4 @@ pub mod proxy;
pub mod language;
pub mod relay_stations;
pub mod relay_adapters;
pub mod packycode_nodes;

View 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()
}

View File

@@ -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()),

View File

@@ -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() {

View File

@@ -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(),

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}
}
};

View File

@@ -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",

View File

@@ -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": "已连接",