import React, { useState, useEffect } from 'react'; import { open } from '@tauri-apps/plugin-shell'; import MonacoEditor from '@monaco-editor/react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Progress } from '@/components/ui/progress'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogDescription, DialogFooter } 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 { Toast, ToastContainer } from "@/components/ui/toast"; import { RelayStation, CreateRelayStationRequest, UpdateRelayStationRequest, RelayStationAdapter, AuthMethod, PackycodeUserQuota, ImportResult, api } from '@/lib/api'; import { Plus, Server, ArrowLeft, Settings, RefreshCw, ExternalLink, Eye, Edit3, Save, X, Download, Upload } from 'lucide-react'; import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragEndEvent, } from '@dnd-kit/core'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, } from '@dnd-kit/sortable'; import { SortableStationItem } from './SortableStationItem'; interface RelayStationManagerProps { onBack: () => void; } const RelayStationManager: React.FC = ({ onBack }) => { const [stations, setStations] = useState([]); const [loading, setLoading] = useState(false); const [selectedStation, setSelectedStation] = useState(null); const [showCreateDialog, setShowCreateDialog] = useState(false); const [showEditDialog, setShowEditDialog] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [stationToDelete, setStationToDelete] = useState(null); const [togglingEnable, setTogglingEnable] = useState>({}); const [currentConfig, setCurrentConfig] = useState>({}); const [loadingConfig, setLoadingConfig] = useState(false); const [jsonConfigView, setJsonConfigView] = useState(false); const [editingConfig, setEditingConfig] = useState(false); const [configJson, setConfigJson] = useState(''); const [savingConfig, setSavingConfig] = useState(false); const [flushingDns, setFlushingDns] = useState(false); const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null); // 导入进度相关状态 const [importing, setImporting] = useState(false); const [importProgress, setImportProgress] = useState(0); const [importResult, setImportResult] = useState(null); // PackyCode 额度相关状态 const [quotaData, setQuotaData] = useState>({}); const [loadingQuota, setLoadingQuota] = useState>({}); const { t } = useTranslation(); // 拖拽传感器配置 const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ); // 拖拽结束处理 const handleDragEnd = async (event: DragEndEvent) => { const { active, over } = event; if (over && active.id !== over.id) { const oldIndex = stations.findIndex(station => station.id === active.id); const newIndex = stations.findIndex(station => station.id === over.id); if (oldIndex !== -1 && newIndex !== -1) { const newStations = arrayMove(stations, oldIndex, newIndex); setStations(newStations); try { await api.relayStationUpdateOrder(newStations.map(s => s.id)); showToast('排序已更新', 'success'); } catch (error) { console.error('Failed to update station order:', error); showToast('更新排序失败', 'error'); setStations(stations); } } } }; // Token 脱敏函数 const maskToken = (token: string): string => { if (!token || token.length <= 8) { return '*'.repeat(token?.length || 0); } const start = token.substring(0, 4); const end = token.substring(token.length - 4); const middleLength = Math.max(token.length - 8, 8); return `${start}${'*'.repeat(middleLength)}${end}`; }; // 从中间截断长文本函数 const truncateMiddle = (text: string, maxLength: number = 60): string => { if (!text || text.length <= maxLength) { return text; } const half = Math.floor(maxLength / 2) - 1; const start = text.substring(0, half); const end = text.substring(text.length - half); return `${start}…${end}`; }; // 显示Toast const showToast = (message: string, type: "success" | "error" = "success") => { setToast({ message, type }); }; // 加载中转站列表 const loadStations = async () => { try { setLoading(true); const stationList = await api.relayStationsList(); setStations(stationList); } catch (error) { console.error('Failed to load stations:', error); showToast(t('relayStation.loadFailed'), "error"); } finally { setLoading(false); } }; // 加载当前配置状态 const loadCurrentConfig = async () => { try { setLoadingConfig(true); // 读取完整的 ~/.claude/settings.json 文件 const settings = await api.getClaudeSettings(); // 保存配置用于简单视图显示 setCurrentConfig({ api_url: settings.env?.ANTHROPIC_BASE_URL || '', api_token: settings.env?.ANTHROPIC_AUTH_TOKEN || '' }); // 格式化完整的JSON字符串 setConfigJson(JSON.stringify(settings, null, 2)); } catch (error) { console.error('Failed to load current config:', error); // 如果失败,尝试获取中转站配置 try { const config = await api.relayStationGetCurrentConfig(); setCurrentConfig(config); setConfigJson(JSON.stringify(config, null, 2)); } catch (fallbackError) { console.error('Failed to load fallback config:', fallbackError); } } finally { setLoadingConfig(false); } }; // 手动同步配置 const syncConfig = async () => { try { const result = await api.relayStationSyncConfig(); showToast(result, "success"); loadCurrentConfig(); } catch (error) { console.error('Failed to sync config:', error); showToast(t('relayStation.syncFailed'), "error"); } }; // 保存JSON配置 const saveJsonConfig = async () => { try { setSavingConfig(true); // 验证JSON格式 const parsedConfig = JSON.parse(configJson); // 保存配置到 ~/.claude/settings.json await api.saveClaudeSettings(parsedConfig); showToast(t('relayStation.configSaved'), "success"); setEditingConfig(false); loadCurrentConfig(); } catch (error) { if (error instanceof SyntaxError) { showToast(t('relayStation.invalidJson'), "error"); } else { console.error('Failed to save config:', error); showToast(t('relayStation.saveFailed'), "error"); } } finally { setSavingConfig(false); } }; // 刷新 DNS 缓存 const handleFlushDns = async () => { try { setFlushingDns(true); await api.flushDns(); showToast(t('relayStation.flushDnsSuccess'), 'success'); } catch (error) { console.error('Failed to flush DNS:', error); showToast(t('relayStation.flushDnsFailed'), 'error'); } finally { setFlushingDns(false); } }; // 查询 PackyCode 额度 const fetchPackycodeQuota = async (stationId: string) => { try { setLoadingQuota(prev => ({ ...prev, [stationId]: true })); const quota = await api.getPackycodeUserQuota(stationId); setQuotaData(prev => ({ ...prev, [stationId]: quota })); } catch (error) { console.error('Failed to fetch PackyCode quota:', error); // 不显示错误 Toast,因为可能是出租车服务或 Token 无效 } finally { setLoadingQuota(prev => ({ ...prev, [stationId]: false })); } }; // 导出中转站配置 const handleExportStations = async () => { try { const stations = await api.relayStationsExport(); const jsonData = JSON.stringify(stations, null, 2); // 使用 Tauri 的保存文件对话框 const { save } = await import('@tauri-apps/plugin-dialog'); const filePath = await save({ defaultPath: `relay-stations-${new Date().toISOString().slice(0, 10)}.json`, filters: [{ name: 'JSON', extensions: ['json'] }] }); if (filePath) { // 使用 Tauri 的文件系统 API 写入文件 const { writeTextFile } = await import('@tauri-apps/plugin-fs'); await writeTextFile(filePath, jsonData); showToast(t('relayStation.exportSuccess'), 'success'); } } catch (error) { console.error('Failed to export stations:', error); showToast(t('relayStation.exportFailed'), 'error'); } }; // 导入中转站配置 const handleImportStations = async () => { try { setImporting(true); setImportProgress(0); setImportResult(null); // 使用 Tauri 的文件选择对话框 const { open } = await import('@tauri-apps/plugin-dialog'); const selected = await open({ multiple: false, filters: [{ name: 'JSON', extensions: ['json'] }] }); if (!selected) { setImporting(false); return; } setImportProgress(20); // 使用 Tauri 的文件系统 API 读取文件 const { readTextFile } = await import('@tauri-apps/plugin-fs'); const text = await readTextFile(selected as string); const stations = JSON.parse(text) as RelayStation[]; setImportProgress(40); // 转换为 CreateRelayStationRequest 格式 const importRequests: CreateRelayStationRequest[] = stations.map(station => ({ 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, adapter_config: station.adapter_config, enabled: station.enabled })); setImportProgress(60); // 显示确认对话框 const confirmed = await new Promise((resolve) => { if (window.confirm(t('relayStation.importConfirm', { count: stations.length }))) { resolve(true); } else { resolve(false); } }); if (confirmed) { setImportProgress(80); const result = await api.relayStationsImport(importRequests, false); setImportProgress(100); setImportResult(result); // 显示结果 if (result.imported > 0) { showToast(result.message, 'success'); loadStations(); } else if (result.skipped === result.total) { showToast(t('relayStation.allDuplicate'), 'error'); } else { showToast(result.message, 'success'); } // 3秒后清除结果 setTimeout(() => { setImportResult(null); setImporting(false); setImportProgress(0); }, 3000); } else { setImporting(false); setImportProgress(0); } } catch (error) { console.error('Failed to import stations:', error); showToast(t('relayStation.importFailed'), 'error'); setImporting(false); setImportProgress(0); setImportResult(null); } }; // 删除中转站 const deleteStation = async () => { if (!stationToDelete) return; try { await api.relayStationDelete(stationToDelete.id); loadStations(); setShowDeleteDialog(false); setStationToDelete(null); showToast(t('relayStation.deleteSuccess'), "success"); } catch (error) { console.error('Failed to delete station:', error); showToast(t('relayStation.deleteFailed'), "error"); } }; // 打开删除确认对话框 const openDeleteDialog = (station: RelayStation) => { setStationToDelete(station); setShowDeleteDialog(true); }; // 获取适配器类型显示名称 const getAdapterDisplayName = (adapter: RelayStationAdapter): string => { switch (adapter) { case 'packycode': return 'PackyCode'; case 'deepseek': return 'DeepSeek v3.1'; case 'glm': return '智谱GLM'; case 'qwen': return '千问Qwen'; case 'kimi': return 'Kimi k2'; 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); showToast(newEnabled ? t('relayStation.enabledSuccess') : t('relayStation.disabledSuccess'), "success"); loadStations(); loadCurrentConfig(); // 重新加载配置状态 } catch (error) { console.error('Failed to toggle enable status:', error); showToast(t('relayStation.toggleEnableFailed'), "error"); } finally { setTogglingEnable(prev => ({ ...prev, [stationId]: false })); } }; // 获取状态样式 const getStatusBadge = (station: RelayStation) => { const enabled = station.enabled; const isToggling = togglingEnable[station.id]; return ( toggleEnableStatus(station.id, enabled)} className="data-[state=checked]:bg-green-500" /> ); }; useEffect(() => { loadStations(); loadCurrentConfig(); }, []); // 当中转站加载完成后,自动获取所有 PackyCode 站点的额度 useEffect(() => { stations.forEach(station => { if (station.adapter === 'packycode') { fetchPackycodeQuota(station.id); } }); }, [stations]); return (
{/* 页面标题 */}

{t('navigation.relayStations')}

{t('relayStation.description')}

{ setShowCreateDialog(false); loadStations(); showToast(t('relayStation.createSuccess'), "success"); }} />
{/* 导入进度 */} {importing && (
{t('relayStation.importing')} {importProgress}%
{importResult && (
{importResult.message}
{t('relayStation.importTotal')}: {importResult.total}
{t('relayStation.importSuccess')}: {importResult.imported}
{t('relayStation.importSkipped')}: {importResult.skipped}
{importResult.failed > 0 && (
{t('relayStation.importFailed')}: {importResult.failed}
)}
)}
)} {/* 当前配置状态 */}
{t('relayStation.currentConfig')}
{jsonConfigView ? (
{!editingConfig ? ( ) : ( <> )}
setConfigJson(value || '')} options={{ readOnly: !editingConfig, minimap: { enabled: false }, scrollBeyondLastLine: false, fontSize: 12, wordWrap: 'on', formatOnPaste: true, formatOnType: true, automaticLayout: true, }} />
) : (
{/* 左侧数据展示 */}
{t('relayStation.configPreview')}
API URL: {currentConfig.api_url || t('relayStation.notConfigured')}
API Token: {currentConfig.api_token ? truncateMiddle(maskToken(currentConfig.api_token), 40) : t('relayStation.notConfigured')}
{t('relayStation.configLocation')}: ~/.claude/settings.json
{/* 右侧按钮区域 */}
)}
{/* 中转站列表 */} s.id)} strategy={verticalListSortingStrategy} >
{loading ? (

{t('common.loading')}

) : stations.length === 0 ? (

{t('relayStation.noStations')}

{t('relayStation.noStationsDesc')}

) : ( stations.map((station) => ) )}
{/* 编辑对话框 */} {selectedStation && ( { setShowEditDialog(false); setSelectedStation(null); loadStations(); showToast(t('relayStation.updateSuccess'), "success"); }} onCancel={() => { setShowEditDialog(false); setSelectedStation(null); }} /> )} {/* 删除确认对话框 */} {t('relayStation.confirmDeleteTitle')} {t('relayStation.deleteConfirm')} {stationToDelete && (
{stationToDelete.name}
)}
{/* Toast 容器 */} {toast && ( setToast(null)} /> )}
); }; // 创建中转站对话框组件 const CreateStationDialog: React.FC<{ onSuccess: () => void; }> = ({ onSuccess }) => { const [formData, setFormData] = useState({ name: '', description: '', api_url: '', adapter: 'packycode', // 默认使用 PackyCode auth_method: 'api_key', // PackyCode 默认使用 API Key system_token: '', user_id: '', enabled: false, // 默认不启用,需要通过主界面切换 }); 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(); // 获取API Key获取地址 const getApiKeyUrl = (adapter: string, service?: string): string | null => { switch (adapter) { case 'deepseek': return 'https://platform.deepseek.com/api_keys'; case 'glm': return 'https://bigmodel.cn/usercenter/proj-mgmt/apikeys'; case 'qwen': return 'https://bailian.console.aliyun.com/?tab=model#/api-key'; 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'; default: return null; } }; // 打开外部链接 const openExternalLink = async (url: string) => { try { await open(url); } catch (error) { console.error('Failed to open URL:', error); } }; // 通用测速函数 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') { setFormData(prev => ({ ...prev, auth_method: 'api_key' // PackyCode 固定使用 API Key })); } else if (formData.adapter === 'custom') { setFormData(prev => ({ ...prev, auth_method: 'custom' })); } else { setFormData(prev => ({ ...prev, auth_method: 'bearer_token' })); } }, [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(); if (formData.adapter === 'custom' && !formData.name.trim()) { setFormToast({ message: t('relayStation.nameRequired'), type: "error" }); return; } if (!formData.api_url.trim()) { setFormToast({ message: t('relayStation.apiUrlRequired'), type: "error" }); return; } if (!formData.system_token.trim()) { setFormToast({ message: t('relayStation.tokenRequired'), type: "error" }); return; } try { setSubmitting(true); // 处理自定义JSON配置 let adapterConfig: Record = {}; let shouldUpdateConfig = false; console.log('[DEBUG] Custom JSON Input:', customJson); console.log('[DEBUG] Original Custom JSON:', originalCustomJson); if (customJson.trim()) { // 用户输入了JSON内容 try { const parsed = JSON.parse(customJson); adapterConfig = parsed; shouldUpdateConfig = true; console.log('[DEBUG] Parsed JSON config:', adapterConfig); } catch (error) { setFormToast({ message: t('relayStation.invalidJson'), type: "error" }); return; } } else if (customJson === '' && originalCustomJson !== '') { // 用户清空了输入框(原不为空,现为空) shouldUpdateConfig = true; adapterConfig = {}; console.log('[DEBUG] User cleared custom config'); } else if (customJson === '' && originalCustomJson === '') { // 一直为空(创建新中转站或未修改) shouldUpdateConfig = false; console.log('[DEBUG] No custom config update needed'); } 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; 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 }); } onSuccess(); } catch (error) { console.error('Failed to create station:', error); setFormToast({ message: t('relayStation.createFailed'), type: "error" }); } finally { setSubmitting(false); } }; return ( <> {t('relayStation.createTitle')}
{/* 第一行:主流适配器 */} {/* 第二行:更多适配器 */}
{/* 仅在选择 Custom 时显示名称输入框 */} {formData.adapter === 'custom' && (
setFormData(prev => ({ ...prev, name: e.target.value }))} placeholder={t('relayStation.namePlaceholder')} className="w-full" />
)} {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}

)}