diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index db0f061..3d47866 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -15,3 +15,4 @@ pub mod filesystem; pub mod git; pub mod terminal; pub mod ccr; +pub mod system; diff --git a/src-tauri/src/commands/system.rs b/src-tauri/src/commands/system.rs new file mode 100644 index 0000000..4636f61 --- /dev/null +++ b/src-tauri/src/commands/system.rs @@ -0,0 +1,62 @@ +use std::process::{Command, Stdio}; + +/// Flush system DNS cache across platforms +#[tauri::command] +pub async fn flush_dns() -> Result { + #[cfg(target_os = "windows")] + { + let output = Command::new("ipconfig") + .arg("/flushdns") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .map_err(|e| format!("Failed to execute ipconfig: {}", e))?; + if output.status.success() { + return Ok("DNS cache flushed".into()); + } else { + let err = String::from_utf8_lossy(&output.stderr).to_string(); + return Err(if err.is_empty() { "ipconfig /flushdns failed".into() } else { err }); + } + } + + #[cfg(target_os = "macos")] + { + let output = Command::new("dscacheutil") + .arg("-flushcache") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .map_err(|e| format!("Failed to execute dscacheutil: {}", e))?; + if output.status.success() { + return Ok("DNS cache flushed".into()); + } else { + let err = String::from_utf8_lossy(&output.stderr).to_string(); + return Err(if err.is_empty() { "dscacheutil -flushcache failed".into() } else { err }); + } + } + + #[cfg(target_os = "linux")] + { + // Try common Linux methods in order + let attempts: Vec<(&str, Vec<&str>)> = vec![ + ("resolvectl", vec!["flush-caches"]), + ("systemd-resolve", vec!["--flush-caches"]), + ("sh", vec!["-c", "service nscd restart || service dnsmasq restart || rc-service nscd restart"]), + ]; + + for (cmd, args) in attempts { + if let Ok(output) = Command::new(cmd) + .args(&args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + { + if output.status.success() { + return Ok("DNS cache flushed".into()); + } + } + } + Err("No supported DNS flush method succeeded on this Linux system".into()) + } +} + diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index a0d24e6..ff11068 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -83,6 +83,7 @@ use commands::ccr::{ check_ccr_installation, get_ccr_version, get_ccr_service_status, start_ccr_service, stop_ccr_service, restart_ccr_service, open_ccr_ui, get_ccr_config_path, }; +use commands::system::flush_dns; use process::ProcessRegistryState; use file_watcher::FileWatcherState; use std::sync::Mutex; @@ -431,6 +432,9 @@ fn main() { restart_ccr_service, open_ccr_ui, get_ccr_config_path, + + // System utilities + flush_dns, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/components/RelayStationManager.tsx b/src/components/RelayStationManager.tsx index 1512785..75c326b 100644 --- a/src/components/RelayStationManager.tsx +++ b/src/components/RelayStationManager.tsx @@ -64,6 +64,7 @@ const RelayStationManager: React.FC = ({ onBack }) => 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); // PackyCode 额度相关状态 @@ -158,6 +159,20 @@ const RelayStationManager: React.FC = ({ onBack }) => } }; + // 刷新 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) => { @@ -410,14 +425,29 @@ const RelayStationManager: React.FC = ({ onBack }) =>
{t('relayStation.configPreview')} - +
+ + +
@@ -755,6 +785,12 @@ const CreateStationDialog: React.FC<{ 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 [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(); @@ -788,6 +824,75 @@ 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', + timeout: 5000, + 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') { @@ -795,7 +900,7 @@ const CreateStationDialog: React.FC<{ ...prev, auth_method: 'api_key', // PackyCode 固定使用 API Key api_url: packycodeService === 'taxi' - ? 'https://share-api.packycode.com' + ? packycodeTaxiNode : packycodeNode })); } else if (formData.adapter === 'custom') { @@ -809,7 +914,7 @@ const CreateStationDialog: React.FC<{ auth_method: 'bearer_token' })); } - }, [formData.adapter, packycodeService, packycodeNode]); + }, [formData.adapter, packycodeService, packycodeNode, packycodeTaxiNode]); // 自动填充中转站名称 const fillStationName = (serviceType: string) => { @@ -1122,7 +1227,7 @@ const CreateStationDialog: React.FC<{

{packycodeService === 'taxi' - ? `${t('relayStation.fixedUrl')}: https://share-api.packycode.com` + ? t('relayStation.taxiServiceNote') : t('relayStation.busServiceNote') }

@@ -1178,18 +1283,18 @@ const CreateStationDialog: React.FC<{ type="button" variant="outline" onClick={async () => { - setFormToast({ message: "正在测速,请稍候...", type: "success" }); - try { - const best = await api.autoSelectBestNode(); - setPackycodeNode(best.url); - setFormData(prev => ({ ...prev, api_url: best.url })); - setFormToast({ - message: `已选择最快节点: ${best.name} (延迟: ${best.response_time}ms)`, - type: "success" - }); - } catch (error) { - setFormToast({ message: "节点测速失败", type: "error" }); - } + const busNodes = [ + { url: "https://api.packycode.com", name: "🚌 直连1(默认公交车)" }, + { url: "https://api-hk-cn2.packycode.com", name: "🇭🇰 直连2 (HK-CN2)" }, + { url: "https://api-us-cmin2.packycode.com", name: "🇺🇸 直连3 (US-CMIN2)" }, + { url: "https://api-us-4837.packycode.com", name: "🇺🇸 直连4 (US-4837)" }, + { url: "https://api-us-cn2.packycode.com", name: "🔄 备用1 (US-CN2)" }, + { url: "https://api-cf-pro.packycode.com", name: "☁️ 备用2 (CF-Pro)" } + ]; + + await performSpeedTest(busNodes, (bestNode) => { + setPackycodeNode(bestNode.url); + }); }} > 自动选择 @@ -1203,6 +1308,58 @@ const CreateStationDialog: React.FC<{
)} + {formData.adapter === 'packycode' && packycodeService === 'taxi' && ( +
+ +
+
+
+ +
+ +
+ +

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

+
+
+ )} +