diff --git a/src-tauri/src/commands/api_nodes.rs b/src-tauri/src/commands/api_nodes.rs new file mode 100644 index 0000000..8c120d5 --- /dev/null +++ b/src-tauri/src/commands/api_nodes.rs @@ -0,0 +1,401 @@ +use anyhow::{Context, Result}; +use rusqlite::{params, Connection}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use uuid::Uuid; + +/// API 节点数据结构 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ApiNode { + pub id: String, + pub name: String, + pub url: String, + pub adapter: String, + pub description: Option, + pub enabled: bool, + pub is_default: bool, + pub created_at: String, + pub updated_at: String, +} + +/// 创建节点请求 +#[derive(Debug, Deserialize)] +pub struct CreateApiNodeRequest { + pub name: String, + pub url: String, + pub adapter: String, + pub description: Option, +} + +/// 更新节点请求 +#[derive(Debug, Deserialize)] +pub struct UpdateApiNodeRequest { + pub name: Option, + pub url: Option, + pub description: Option, + pub enabled: Option, +} + +/// 节点测试结果 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NodeTestResult { + pub node_id: String, + pub url: String, + pub name: String, + pub response_time: Option, + pub status: String, + pub error: Option, +} + +/// 获取数据库连接 +fn get_connection() -> Result { + let db_path = get_nodes_db_path()?; + let conn = Connection::open(&db_path) + .context(format!("Failed to open database at {:?}", db_path))?; + Ok(conn) +} + +/// 获取节点数据库路径 +fn get_nodes_db_path() -> Result { + let home = dirs::home_dir().context("Could not find home directory")?; + let db_dir = home.join(".claudia"); + std::fs::create_dir_all(&db_dir).context("Failed to create database directory")?; + Ok(db_dir.join("api_nodes.db")) +} + +/// 初始化数据库表 +pub fn init_nodes_db() -> Result<()> { + let conn = get_connection()?; + + conn.execute( + "CREATE TABLE IF NOT EXISTS api_nodes ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + url TEXT NOT NULL UNIQUE, + adapter TEXT NOT NULL, + description TEXT, + enabled INTEGER DEFAULT 1, + is_default INTEGER DEFAULT 0, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + )", + [], + )?; + + // 创建索引 + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_api_nodes_adapter ON api_nodes(adapter)", + [], + )?; + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_api_nodes_enabled ON api_nodes(enabled)", + [], + )?; + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_api_nodes_is_default ON api_nodes(is_default)", + [], + )?; + + Ok(()) +} + +/// 预设节点配置 +const DEFAULT_NODES: &[(&str, &str, &str, &str)] = &[ + // PackyCode + ("🚌 默认节点", "https://www.packyapi.com", "packycode", "PackyCode 默认节点"), + ("⚖️ 负载均衡", "https://api-slb.packyapi.com", "packycode", "PackyCode 负载均衡节点"), + + // DeepSeek + ("默认节点", "https://api.deepseek.com/anthropic", "deepseek", "DeepSeek 官方节点"), + + // GLM + ("默认节点", "https://open.bigmodel.cn/api/anthropic", "glm", "智谱 GLM 官方节点"), + + // Qwen + ("默认节点", "https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy", "qwen", "通义千问官方节点"), + + // Kimi + ("默认节点", "https://api.moonshot.cn/anthropic", "kimi", "Moonshot Kimi 官方节点"), + + // MiniMax + ("默认节点", "https://api.minimaxi.com/anthropic", "minimax", "MiniMax 官方节点"), + ("备用节点", "https://api.minimaxi.io/anthropic", "minimax", "MiniMax 备用节点"), +]; + +/// 初始化预设节点 +#[tauri::command] +pub async fn init_default_nodes() -> Result<(), String> { + let conn = get_connection().map_err(|e| e.to_string())?; + let now = chrono::Utc::now().to_rfc3339(); + + for (name, url, adapter, description) in DEFAULT_NODES { + // 检查是否已存在 + let exists: bool = conn + .query_row( + "SELECT COUNT(*) > 0 FROM api_nodes WHERE url = ?1", + params![url], + |row| row.get(0), + ) + .map_err(|e| e.to_string())?; + + if !exists { + let id = Uuid::new_v4().to_string(); + conn.execute( + "INSERT INTO api_nodes (id, name, url, adapter, description, enabled, is_default, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, 1, 1, ?6, ?7)", + params![id, name, url, adapter, description, now, now], + ) + .map_err(|e| e.to_string())?; + } + } + + Ok(()) +} + +/// 获取节点列表 +#[tauri::command] +pub async fn list_api_nodes( + adapter: Option, + enabled_only: Option, +) -> Result, String> { + let conn = get_connection().map_err(|e| e.to_string())?; + + let mut sql = "SELECT id, name, url, adapter, description, enabled, is_default, created_at, updated_at FROM api_nodes WHERE 1=1".to_string(); + + if let Some(adapter_filter) = &adapter { + sql.push_str(&format!(" AND adapter = '{}'", adapter_filter)); + } + + if enabled_only.unwrap_or(false) { + sql.push_str(" AND enabled = 1"); + } + + sql.push_str(" ORDER BY is_default DESC, created_at ASC"); + + let mut stmt = conn.prepare(&sql).map_err(|e| e.to_string())?; + let nodes = stmt + .query_map([], |row| { + Ok(ApiNode { + id: row.get(0)?, + name: row.get(1)?, + url: row.get(2)?, + adapter: row.get(3)?, + description: row.get(4)?, + enabled: row.get::<_, i32>(5)? != 0, + is_default: row.get::<_, i32>(6)? != 0, + created_at: row.get(7)?, + updated_at: row.get(8)?, + }) + }) + .map_err(|e| e.to_string())? + .collect::>>() + .map_err(|e| e.to_string())?; + + Ok(nodes) +} + +/// 创建节点 +#[tauri::command] +pub async fn create_api_node(request: CreateApiNodeRequest) -> Result { + let conn = get_connection().map_err(|e| e.to_string())?; + let id = Uuid::new_v4().to_string(); + let now = chrono::Utc::now().to_rfc3339(); + + // 检查 URL 是否已存在 + let exists: bool = conn + .query_row( + "SELECT COUNT(*) > 0 FROM api_nodes WHERE url = ?1", + params![&request.url], + |row| row.get(0), + ) + .map_err(|e| e.to_string())?; + + if exists { + return Err("节点 URL 已存在".to_string()); + } + + conn.execute( + "INSERT INTO api_nodes (id, name, url, adapter, description, enabled, is_default, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, 1, 0, ?6, ?7)", + params![ + &id, + &request.name, + &request.url, + &request.adapter, + &request.description, + &now, + &now + ], + ) + .map_err(|e| e.to_string())?; + + Ok(ApiNode { + id, + name: request.name, + url: request.url, + adapter: request.adapter, + description: request.description, + enabled: true, + is_default: false, + created_at: now.clone(), + updated_at: now, + }) +} + +/// 更新节点 +#[tauri::command] +pub async fn update_api_node(id: String, request: UpdateApiNodeRequest) -> Result { + let conn = get_connection().map_err(|e| e.to_string())?; + let now = chrono::Utc::now().to_rfc3339(); + + // 检查节点是否存在 + let exists: bool = conn + .query_row( + "SELECT COUNT(*) > 0 FROM api_nodes WHERE id = ?1", + params![&id], + |row| row.get(0), + ) + .map_err(|e| e.to_string())?; + + if !exists { + return Err("节点不存在".to_string()); + } + + // 构建动态 SQL + let mut updates = Vec::new(); + let mut params_vec: Vec> = Vec::new(); + + if let Some(name) = &request.name { + updates.push("name = ?"); + params_vec.push(Box::new(name.clone())); + } + if let Some(url) = &request.url { + updates.push("url = ?"); + params_vec.push(Box::new(url.clone())); + } + if let Some(description) = &request.description { + updates.push("description = ?"); + params_vec.push(Box::new(description.clone())); + } + if let Some(enabled) = request.enabled { + updates.push("enabled = ?"); + params_vec.push(Box::new(if enabled { 1 } else { 0 })); + } + + updates.push("updated_at = ?"); + params_vec.push(Box::new(now.clone())); + params_vec.push(Box::new(id.clone())); + + let sql = format!( + "UPDATE api_nodes SET {} WHERE id = ?", + updates.join(", ") + ); + + let params_refs: Vec<&dyn rusqlite::ToSql> = params_vec.iter().map(|p| p.as_ref()).collect(); + conn.execute(&sql, params_refs.as_slice()) + .map_err(|e| e.to_string())?; + + // 获取更新后的节点 + let node = conn + .query_row( + "SELECT id, name, url, adapter, description, enabled, is_default, created_at, updated_at FROM api_nodes WHERE id = ?1", + params![&id], + |row| { + Ok(ApiNode { + id: row.get(0)?, + name: row.get(1)?, + url: row.get(2)?, + adapter: row.get(3)?, + description: row.get(4)?, + enabled: row.get::<_, i32>(5)? != 0, + is_default: row.get::<_, i32>(6)? != 0, + created_at: row.get(7)?, + updated_at: row.get(8)?, + }) + }, + ) + .map_err(|e| e.to_string())?; + + Ok(node) +} + +/// 删除节点 +#[tauri::command] +pub async fn delete_api_node(id: String) -> Result<(), String> { + let conn = get_connection().map_err(|e| e.to_string())?; + + conn.execute("DELETE FROM api_nodes WHERE id = ?1", params![&id]) + .map_err(|e| e.to_string())?; + + Ok(()) +} + +/// 测试单个节点 +#[tauri::command] +pub async fn test_api_node(url: String, timeout_ms: Option) -> Result { + let timeout = std::time::Duration::from_millis(timeout_ms.unwrap_or(5000)); + let start = std::time::Instant::now(); + + let client = reqwest::Client::builder() + .timeout(timeout) + .build() + .map_err(|e| e.to_string())?; + + // 使用 HEAD 请求测试连通性,更轻量且不会触发 API 调用 + match client.head(&url).send().await { + Ok(response) => { + let response_time = start.elapsed().as_millis() as u64; + // 允许 2xx, 3xx, 4xx 状态码,说明服务器在线(5xx 视为失败) + let status_code = response.status(); + let status = if status_code.is_success() + || status_code.is_redirection() + || status_code.is_client_error() { + "success" + } else { + "failed" + }; + + Ok(NodeTestResult { + node_id: String::new(), + url: url.clone(), + name: String::new(), + response_time: Some(response_time), + status: status.to_string(), + error: if status == "failed" { + Some(format!("HTTP {}", status_code)) + } else { + None + }, + }) + } + Err(e) => Ok(NodeTestResult { + node_id: String::new(), + url: url.clone(), + name: String::new(), + response_time: None, + status: "failed".to_string(), + error: Some(e.to_string()), + }), + } +} + +/// 批量测试节点 +#[tauri::command] +pub async fn test_all_api_nodes( + adapter: Option, + timeout_ms: Option, +) -> Result, String> { + let nodes = list_api_nodes(adapter, Some(true)).await?; + let mut results = Vec::new(); + + for node in nodes { + let result = test_api_node(node.url.clone(), timeout_ms).await?; + results.push(NodeTestResult { + node_id: node.id.clone(), + name: node.name.clone(), + ..result + }); + } + + Ok(results) +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 2763bae..c6f80df 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,4 +1,5 @@ pub mod agents; +pub mod api_nodes; pub mod ccr; pub mod claude; pub mod filesystem; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 1e5db27..6ef0725 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -224,6 +224,10 @@ fn main() { // Initialize agents database let conn = init_database(&app.handle()).expect("Failed to initialize agents database"); + // Initialize API nodes database + commands::api_nodes::init_nodes_db() + .expect("Failed to initialize API nodes database"); + // Load and apply proxy settings from the database { let db = AgentDb(Mutex::new(conn)); @@ -500,6 +504,14 @@ fn main() { test_all_packycode_nodes, auto_select_best_node, get_packycode_nodes, + // API Nodes Management + commands::api_nodes::init_default_nodes, + commands::api_nodes::list_api_nodes, + commands::api_nodes::create_api_node, + commands::api_nodes::update_api_node, + commands::api_nodes::delete_api_node, + commands::api_nodes::test_api_node, + commands::api_nodes::test_all_api_nodes, // File System read_directory_tree, search_files_by_name, diff --git a/src/components/NodeManager/index.tsx b/src/components/NodeManager/index.tsx new file mode 100644 index 0000000..620d024 --- /dev/null +++ b/src/components/NodeManager/index.tsx @@ -0,0 +1,625 @@ +import React, { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Badge } from '@/components/ui/badge'; +import { Switch } from '@/components/ui/switch'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { + AlertCircle, + CheckCircle2, + Settings, + Plus, + Edit, + Trash2, + Zap, + Loader2, +} from 'lucide-react'; +import * as api from '@/lib/api'; +import type { ApiNode, CreateApiNodeRequest, NodeTestResult } from '@/lib/api'; + +interface NodeSelectorProps { + adapter: api.RelayStationAdapter; + value?: string; + onChange: (url: string, node?: ApiNode) => void; + allowManualInput?: boolean; + showToast?: (message: string, type: 'success' | 'error') => void; +} + +/** + * 节点选择器组件 + * 用于中转站表单中选择API节点 + */ +export const NodeSelector: React.FC = ({ + adapter, + value = '', + onChange, + allowManualInput = true, + showToast = (msg, _type) => alert(msg), // 默认使用 alert +}) => { + const [showDialog, setShowDialog] = useState(false); + const [nodes, setNodes] = useState([]); + const [currentNode, setCurrentNode] = useState(null); + + useEffect(() => { + loadNodes(); + }, [adapter]); + + useEffect(() => { + if (value && nodes.length > 0) { + const node = nodes.find(n => n.url === value); + setCurrentNode(node || null); + } + }, [value, nodes]); + + const loadNodes = async () => { + try { + const allNodes = await api.listApiNodes(adapter, true); + setNodes(allNodes); + } catch (error) { + console.error('Failed to load nodes:', error); + } + }; + + const handleSelectNode = (node: ApiNode) => { + onChange(node.url, node); + setShowDialog(false); + }; + + const handleSaveCustomNode = async () => { + if (!value.trim() || value.startsWith('http') === false) { + showToast('请输入有效的 URL', 'error'); + return; + } + + // 检查是否已存在 + const existingNode = nodes.find(n => n.url === value); + if (existingNode) { + showToast('该节点已存在', 'error'); + return; + } + + try { + await api.createApiNode({ + name: `自定义节点 - ${new URL(value).hostname}`, + url: value, + adapter: adapter, + description: '用户手动添加的节点', + }); + showToast('节点保存成功', 'success'); + loadNodes(); + } catch (error) { + showToast('保存失败', 'error'); + console.error(error); + } + }; + + return ( +
+ +
+ allowManualInput && onChange(e.target.value)} + placeholder="https://api.example.com" + readOnly={!allowManualInput} + className="flex-1" + /> + + {allowManualInput && value && !currentNode && value.startsWith('http') && ( + + )} +
+ {currentNode && adapter !== 'custom' && ( +
+ 📍 当前节点:{currentNode.name} + {currentNode.is_default && ( + 预设 + )} +
+ )} + + +
+ ); +}; + +interface NodeManagerDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + adapter?: api.RelayStationAdapter; + onSelectNode?: (node: ApiNode) => void; + currentUrl?: string; + showToast?: (message: string, type: 'success' | 'error') => void; +} + +/** + * 从中间截断 URL,保留开头和结尾 + */ +const truncateUrl = (url: string, maxLength: number = 50): string => { + if (url.length <= maxLength) return url; + + const start = Math.floor(maxLength * 0.6); + const end = Math.floor(maxLength * 0.4); + + return url.substring(0, start) + '...' + url.substring(url.length - end); +}; + +/** + * 节点管理弹窗 + * 支持增删改查和测速功能 + */ +const NodeManagerDialog: React.FC = ({ + open, + onOpenChange, + adapter: filterAdapter, + onSelectNode, + currentUrl, + showToast = (msg) => alert(msg), +}) => { + const [nodes, setNodes] = useState([]); + const [loading, setLoading] = useState(false); + const [testing, setTesting] = useState(false); + const [testResults, setTestResults] = useState>({}); + const [editingNode, setEditingNode] = useState(null); + const [showForm, setShowForm] = useState(false); + const [enabledOnly, setEnabledOnly] = useState(false); + + useEffect(() => { + if (open) { + loadNodes(); + // 首次打开时初始化预设节点 + api.initDefaultNodes().catch(console.error); + } + }, [open, filterAdapter, enabledOnly]); + + const loadNodes = async () => { + setLoading(true); + try { + const allNodes = await api.listApiNodes(filterAdapter, enabledOnly); + setNodes(allNodes); + } catch (error) { + showToast('加载节点失败', 'error'); + console.error(error); + } finally { + setLoading(false); + } + }; + + const handleTestAll = async () => { + setTesting(true); + setTestResults({}); + try { + const results = await api.testAllApiNodes(filterAdapter, 5000); + const resultsMap: Record = {}; + results.forEach(r => { + resultsMap[r.node_id] = r; + }); + setTestResults(resultsMap); + } catch (error) { + showToast('测速失败', 'error'); + console.error(error); + } finally { + setTesting(false); + } + }; + + const handleTestOne = async (node: ApiNode) => { + setTestResults(prev => ({ + ...prev, + [node.id]: { ...testResults[node.id], status: 'testing' } as NodeTestResult, + })); + try { + const result = await api.testApiNode(node.url, 5000); + setTestResults(prev => ({ + ...prev, + [node.id]: { ...result, node_id: node.id, name: node.name }, + })); + } catch (error) { + setTestResults(prev => ({ + ...prev, + [node.id]: { + node_id: node.id, + url: node.url, + name: node.name, + response_time: null, + status: 'failed', + error: String(error), + }, + })); + } + }; + + const handleDelete = async (node: ApiNode) => { + if (!confirm(`确定要删除节点 "${node.name}" 吗?`)) return; + + try { + await api.deleteApiNode(node.id); + showToast('删除成功', 'success'); + loadNodes(); + } catch (error) { + showToast('删除失败', 'error'); + console.error(error); + } + }; + + const handleToggleEnable = async (node: ApiNode) => { + try { + await api.updateApiNode(node.id, { enabled: !node.enabled }); + loadNodes(); + } catch (error) { + showToast('更新失败', 'error'); + console.error(error); + } + }; + + const getStatusBadge = (node: ApiNode) => { + const result = testResults[node.id]; + if (!result) { + return 未测试; + } + if (result.status === 'testing') { + return ( + + + 测试中 + + ); + } + if (result.status === 'success') { + return ( + + + {result.response_time}ms + + ); + } + return ( + + + 失败 + + ); + }; + + return ( + + + +
+
+ 节点管理 + 管理 API 节点,支持增删改查和测速 +
+
+ +
+
+
+ +
+ {/* 工具栏 */} +
+ +
+ + +
+
+ + {/* 节点列表 */} + {loading ? ( +
+ +
+ ) : nodes.length === 0 ? ( +
+ 暂无节点 +
+ ) : ( +
+ {nodes.map((node) => ( +
{ + // 如果点击的是操作按钮区域,不触发选择 + if ((e.target as HTMLElement).closest('.action-buttons')) { + return; + } + if (onSelectNode) { + onSelectNode(node); + } + }} + > +
+
e.stopPropagation()}> + handleToggleEnable(node)} + /> +
+
+
+ {node.name} + {node.is_default && ( + 预设 + )} + {getStatusBadge(node)} +
+
+ {truncateUrl(node.url, 60)} +
+ {node.description && ( +
{node.description}
+ )} +
+
+
e.stopPropagation()}> + + + +
+
+ ))} +
+ )} +
+ + {/* 添加/编辑表单对话框 */} + { + setShowForm(false); + setEditingNode(null); + loadNodes(); + }} + showToast={showToast} + /> +
+
+ ); +}; + +interface NodeFormDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + node?: ApiNode | null; + defaultAdapter?: api.RelayStationAdapter; + onSuccess: () => void; + showToast?: (message: string, type: 'success' | 'error') => void; +} + +/** + * 节点添加/编辑表单 + */ +const NodeFormDialog: React.FC = ({ + open, + onOpenChange, + node, + defaultAdapter, + onSuccess, + showToast = (msg) => alert(msg), +}) => { + const [submitting, setSubmitting] = useState(false); + const [formData, setFormData] = useState({ + name: '', + url: '', + adapter: defaultAdapter || 'packycode', + description: '', + }); + + useEffect(() => { + if (node) { + setFormData({ + name: node.name, + url: node.url, + adapter: node.adapter, + description: node.description || '', + }); + } else { + setFormData({ + name: '', + url: '', + adapter: defaultAdapter || 'packycode', + description: '', + }); + } + }, [node, defaultAdapter, open]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setSubmitting(true); + + try { + if (node) { + await api.updateApiNode(node.id, { + name: formData.name, + url: formData.url, + description: formData.description, + }); + showToast('更新成功', 'success'); + } else { + await api.createApiNode(formData); + showToast('创建成功', 'success'); + } + onSuccess(); + } catch (error) { + showToast(node ? '更新失败' : '创建失败', 'error'); + console.error(error); + } finally { + setSubmitting(false); + } + }; + + return ( + + + + {node ? '编辑节点' : '添加节点'} + +
+
+ + setFormData(prev => ({ ...prev, name: e.target.value }))} + placeholder="例如:🚀 我的自定义节点" + required + /> +
+ +
+ + setFormData(prev => ({ ...prev, url: e.target.value }))} + placeholder="https://api.example.com" + required + /> +
+ + {!node && ( +
+ + +
+ )} + +
+ + setFormData(prev => ({ ...prev, description: e.target.value }))} + placeholder="节点描述信息" + /> +
+ +
+ + +
+
+
+
+ ); +}; + +export default NodeSelector; diff --git a/src/components/RelayStationManager.tsx b/src/components/RelayStationManager.tsx index 33eec7c..f0cde51 100644 --- a/src/components/RelayStationManager.tsx +++ b/src/components/RelayStationManager.tsx @@ -68,6 +68,7 @@ import { verticalListSortingStrategy, } from '@dnd-kit/sortable'; import { SortableStationItem } from './SortableStationItem'; +import { NodeSelector } from '@/components/NodeManager'; interface RelayStationManagerProps { onBack: () => void; @@ -500,6 +501,7 @@ const RelayStationManager: React.FC = ({ onBack }) => case 'glm': return '智谱GLM'; case 'qwen': return '千问Qwen'; case 'kimi': return 'Kimi k2'; + case 'minimax': return 'MiniMax M2'; case 'custom': return t('relayStation.custom'); default: return adapter; } @@ -1055,21 +1057,21 @@ const CreateStationDialog: React.FC<{ }); const [submitting, setSubmitting] = useState(false); const [formToast, setFormToast] = useState<{ message: string; type: "success" | "error" } | null>(null); - const [packycodeService, setPackycodeService] = useState('bus'); // 默认公交车 - const [packycodeNode, setPackycodeNode] = useState('https://api.packycode.com'); // 默认节点(公交车用) - const [packycodeTaxiNode, setPackycodeTaxiNode] = useState('https://share-api.packycode.com'); // 滴滴车节点 const [customJson, setCustomJson] = useState(''); // 自定义JSON配置 const [originalCustomJson] = useState(''); // 原始JSON配置(用于比较是否修改) - // 测速弹出框状态 - const [showSpeedTestModal, setShowSpeedTestModal] = useState(false); - const [speedTestResults, setSpeedTestResults] = useState<{ url: string; name: string; responseTime: number | null; status: 'testing' | 'success' | 'failed' }[]>([]); - const [speedTestInProgress, setSpeedTestInProgress] = useState(false); - const { t } = useTranslation(); + // Toast 显示函数 + const showToast = (message: string, type: "success" | "error" = "success") => { + setFormToast({ message, type }); + setTimeout(() => { + setFormToast(null); + }, 3000); + }; + // 获取API Key获取地址 - const getApiKeyUrl = (adapter: string, service?: string): string | null => { + const getApiKeyUrl = (adapter: string): string | null => { switch (adapter) { case 'deepseek': return 'https://platform.deepseek.com/api_keys'; @@ -1080,10 +1082,9 @@ const CreateStationDialog: React.FC<{ case 'kimi': return 'https://platform.moonshot.cn/console/api-keys'; case 'packycode': - if (service === 'taxi') { - return 'https://share.packycode.com/api-management'; - } return 'https://www.packycode.com/api-management'; + case 'minimax': + return 'https://platform.minimaxi.com/user-center/basic-information/interface-key'; default: return null; } @@ -1098,74 +1099,6 @@ const CreateStationDialog: React.FC<{ } }; - // 通用测速函数 - const performSpeedTest = async (nodes: { url: string; name: string }[], onComplete: (bestNode: { url: string; name: string }) => void) => { - setShowSpeedTestModal(true); - setSpeedTestInProgress(true); - - // 初始化测速结果 - const initialResults = nodes.map(node => ({ - url: node.url, - name: node.name, - responseTime: null, - status: 'testing' as const - })); - setSpeedTestResults(initialResults); - - let bestNode = nodes[0]; - let minTime = Infinity; - - // 并行测试所有节点 - const testPromises = nodes.map(async (node, index) => { - try { - const startTime = Date.now(); - await fetch(node.url, { - method: 'HEAD', - mode: 'no-cors' - }); - const responseTime = Date.now() - startTime; - - // 更新单个节点的测试结果 - setSpeedTestResults(prev => prev.map((result, i) => - i === index ? { ...result, responseTime, status: 'success' } : result - )); - - if (responseTime < minTime) { - minTime = responseTime; - bestNode = node; - } - - return { node, responseTime }; - } catch (error) { - console.log(`Node ${node.url} failed:`, error); - // 标记节点为失败 - setSpeedTestResults(prev => prev.map((result, i) => - i === index ? { ...result, responseTime: null, status: 'failed' } : result - )); - return { node, responseTime: null }; - } - }); - - try { - await Promise.all(testPromises); - // 测试完成后等待2秒让用户看到结果 - setTimeout(() => { - setSpeedTestInProgress(false); - onComplete(bestNode); - // 再等1秒后关闭弹框 - setTimeout(() => { - setShowSpeedTestModal(false); - }, 1000); - }, 2000); - } catch (error) { - console.error('Speed test failed:', error); - setSpeedTestInProgress(false); - setTimeout(() => { - setShowSpeedTestModal(false); - }, 1000); - } - }; - // 当适配器改变时更新认证方式和 URL useEffect(() => { if (formData.adapter === 'packycode') { @@ -1186,18 +1119,6 @@ const CreateStationDialog: React.FC<{ } }, [formData.adapter]); - // 自动填充中转站名称 - const fillStationName = (serviceType: string) => { - const serviceName = serviceType === 'taxi' ? t('relayStation.taxiService') : t('relayStation.busService'); - const newName = `PackyCode ${serviceName}`; - - // 当选择PackyCode服务类型时,始终更新名称 - setFormData(prev => ({ - ...prev, - name: newName - })); - }; - const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -1252,65 +1173,22 @@ const CreateStationDialog: React.FC<{ console.log('[DEBUG] Should update config:', shouldUpdateConfig); console.log('[DEBUG] Adapter config to send:', shouldUpdateConfig ? adapterConfig : 'undefined'); - // PackyCode 保存时自动选择最佳节点 - if (formData.adapter === 'packycode') { - let finalApiUrl = formData.api_url; + // 统一处理所有适配器的创建逻辑 + let finalConfig = shouldUpdateConfig ? adapterConfig : undefined; - if (packycodeService === 'bus') { - // 公交车自动选择 - const busNodes = [ - { url: "https://api.packycode.com", name: "🚌 公交车默认节点" }, - { url: "https://api-hk-cn2.packycode.com", name: "🇭🇰 公交车 HK-CN2" }, - { url: "https://api-hk-g.packycode.com", name: "🇭🇰 公交车 HK-G" }, - { url: "https://api-cf-pro.packycode.com", name: "☁️ 公交车 CF-Pro" }, - { url: "https://api-us-cn2.packycode.com", name: "🇺🇸 公交车 US-CN2" } - ]; - - await performSpeedTest(busNodes, (bestNode) => { - finalApiUrl = bestNode.url; - setPackycodeNode(bestNode.url); - }); - } else if (packycodeService === 'taxi') { - // 滴滴车自动选择 - const taxiNodes = [ - { url: "https://share-api.packycode.com", name: "🚗 滴滴车默认节点" }, - { url: "https://share-api-hk-cn2.packycode.com", name: "🇭🇰 滴滴车 HK-CN2" }, - { url: "https://share-api-hk-g.packycode.com", name: "🇭🇰 滴滴车 HK-G" }, - { url: "https://share-api-cf-pro.packycode.com", name: "☁️ 滴滴车 CF-Pro" }, - { url: "https://share-api-us-cn2.packycode.com", name: "🇺🇸 滴滴车 US-CN2" } - ]; - - await performSpeedTest(taxiNodes, (bestNode) => { - finalApiUrl = bestNode.url; - setPackycodeTaxiNode(bestNode.url); - }); - } - - const finalConfig = shouldUpdateConfig ? { - service_type: packycodeService, - ...adapterConfig - } : undefined; - - console.log('[DEBUG] Final adapter_config for PackyCode:', finalConfig); - - // 使用选择的最佳节点创建中转站 - await api.relayStationCreate({ - ...formData, - api_url: finalApiUrl, - adapter_config: finalConfig - }); - } else { - const finalConfig = shouldUpdateConfig ? adapterConfig : undefined; - - console.log('[DEBUG] Final adapter_config for non-PackyCode:', finalConfig); - - // 非 PackyCode 适配器直接创建 - await api.relayStationCreate({ - ...formData, - adapter_config: finalConfig - }); + // MiniMax 适配器默认模型配置 + if (formData.adapter === 'minimax' && !shouldUpdateConfig) { + finalConfig = { model: "MiniMax-M2-Preview" }; } + console.log('[DEBUG] Final adapter_config:', finalConfig); + + // 创建中转站 + await api.relayStationCreate({ + ...formData, + adapter_config: finalConfig + }); + onSuccess(); } catch (error) { console.error('Failed to create station:', error); @@ -1322,9 +1200,6 @@ const CreateStationDialog: React.FC<{ return ( <> - - {t('relayStation.createTitle')} -
@@ -1344,13 +1219,12 @@ const CreateStationDialog: React.FC<{ ...prev, adapter: 'packycode', name: 'PackyCode', - api_url: 'https://api.packycode.com' + api_url: 'https://www.packyapi.com' }))} >
📦
PackyCode
-
推荐使用
@@ -1372,7 +1246,7 @@ const CreateStationDialog: React.FC<{
🚀
DeepSeek
-
v3.1
+
深度求索
@@ -1393,7 +1267,7 @@ const CreateStationDialog: React.FC<{ >
🤖
-
智谱GLM
+
GLM
清华智谱
@@ -1416,7 +1290,7 @@ const CreateStationDialog: React.FC<{ >
🎯
-
千问Qwen
+
Qwen
阿里通义
@@ -1438,11 +1312,33 @@ const CreateStationDialog: React.FC<{ >
🌙
-
Kimi k2
+
Kimi
月之暗面
+ +
@@ -1478,186 +1378,16 @@ const CreateStationDialog: React.FC<{ )} - {formData.adapter === 'packycode' && ( -
-
- -
- - - -
-

- {packycodeService === 'taxi' - ? t('relayStation.taxiServiceNote') - : t('relayStation.busServiceNote') - } -

-
-
- )} - - {formData.adapter === 'packycode' && packycodeService === 'bus' && ( -
- -
-
-
- -
- -
- -

- {t('relayStation.selectedNode') + ': ' + packycodeNode} -

-
-
- )} - - {formData.adapter === 'packycode' && packycodeService === 'taxi' && ( -
- -
-
-
- -
- -
- -

- {t('relayStation.selectedNode') + ': ' + packycodeTaxiNode} -

-
-
- )} + {/* 节点地址选择 - 使用通用 NodeSelector */} +
+ setFormData(prev => ({ ...prev, api_url: url }))} + showToast={showToast} + allowManualInput={true} + /> +
@@ -1671,75 +1401,9 @@ const CreateStationDialog: React.FC<{ />
- {formData.adapter !== 'packycode' && ( -
- - setFormData(prev => ({ ...prev, api_url: e.target.value }))} - placeholder="https://api.example.com" - className="w-full" - /> -
- )} -
- {formData.adapter === 'packycode' ? ( - // PackyCode 固定使用 API Key,不显示选择器 -
-
- - {getApiKeyUrl(formData.adapter, packycodeService) && ( - - )} -
- setFormData(prev => ({ ...prev, system_token: e.target.value }))} - placeholder="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - className="w-full font-mono text-sm" - /> -

- {t('relayStation.packycodeTokenNote')} -

- - {/* 自定义JSON配置 */} -
-
- - {t('relayStation.customJsonOptional')} -
-