修改中转站
This commit is contained in:
@@ -15,3 +15,4 @@ pub mod filesystem;
|
|||||||
pub mod git;
|
pub mod git;
|
||||||
pub mod terminal;
|
pub mod terminal;
|
||||||
pub mod ccr;
|
pub mod ccr;
|
||||||
|
pub mod system;
|
||||||
|
62
src-tauri/src/commands/system.rs
Normal file
62
src-tauri/src/commands/system.rs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
|
/// Flush system DNS cache across platforms
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn flush_dns() -> Result<String, String> {
|
||||||
|
#[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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@@ -83,6 +83,7 @@ use commands::ccr::{
|
|||||||
check_ccr_installation, get_ccr_version, get_ccr_service_status, start_ccr_service,
|
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,
|
stop_ccr_service, restart_ccr_service, open_ccr_ui, get_ccr_config_path,
|
||||||
};
|
};
|
||||||
|
use commands::system::flush_dns;
|
||||||
use process::ProcessRegistryState;
|
use process::ProcessRegistryState;
|
||||||
use file_watcher::FileWatcherState;
|
use file_watcher::FileWatcherState;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
@@ -431,6 +432,9 @@ fn main() {
|
|||||||
restart_ccr_service,
|
restart_ccr_service,
|
||||||
open_ccr_ui,
|
open_ccr_ui,
|
||||||
get_ccr_config_path,
|
get_ccr_config_path,
|
||||||
|
|
||||||
|
// System utilities
|
||||||
|
flush_dns,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
@@ -64,6 +64,7 @@ const RelayStationManager: React.FC<RelayStationManagerProps> = ({ onBack }) =>
|
|||||||
const [editingConfig, setEditingConfig] = useState(false);
|
const [editingConfig, setEditingConfig] = useState(false);
|
||||||
const [configJson, setConfigJson] = useState<string>('');
|
const [configJson, setConfigJson] = useState<string>('');
|
||||||
const [savingConfig, setSavingConfig] = useState(false);
|
const [savingConfig, setSavingConfig] = useState(false);
|
||||||
|
const [flushingDns, setFlushingDns] = useState(false);
|
||||||
const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null);
|
const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null);
|
||||||
|
|
||||||
// PackyCode 额度相关状态
|
// PackyCode 额度相关状态
|
||||||
@@ -158,6 +159,20 @@ const RelayStationManager: React.FC<RelayStationManagerProps> = ({ 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 额度
|
// 查询 PackyCode 额度
|
||||||
const fetchPackycodeQuota = async (stationId: string) => {
|
const fetchPackycodeQuota = async (stationId: string) => {
|
||||||
@@ -410,14 +425,29 @@ const RelayStationManager: React.FC<RelayStationManagerProps> = ({ onBack }) =>
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between items-center mb-2">
|
<div className="flex justify-between items-center mb-2">
|
||||||
<span className="text-sm font-medium">{t('relayStation.configPreview')}</span>
|
<span className="text-sm font-medium">{t('relayStation.configPreview')}</span>
|
||||||
<Button
|
<div className="flex gap-2">
|
||||||
variant="outline"
|
<Button
|
||||||
size="sm"
|
variant="outline"
|
||||||
onClick={() => setJsonConfigView(true)}
|
size="sm"
|
||||||
>
|
onClick={() => setJsonConfigView(true)}
|
||||||
<Eye className="h-4 w-4 mr-1" />
|
>
|
||||||
{t('relayStation.viewJson')}
|
<Eye className="h-4 w-4 mr-1" />
|
||||||
</Button>
|
{t('relayStation.viewJson')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleFlushDns}
|
||||||
|
disabled={flushingDns}
|
||||||
|
>
|
||||||
|
{flushingDns ? (
|
||||||
|
<div className="h-4 w-4 animate-spin rounded-full border-b-2 border-current mr-1" />
|
||||||
|
) : (
|
||||||
|
<RefreshCw className="h-4 w-4 mr-1" />
|
||||||
|
)}
|
||||||
|
{t('relayStation.flushDns')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2 text-sm">
|
<div className="space-y-2 text-sm">
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
@@ -755,6 +785,12 @@ const CreateStationDialog: React.FC<{
|
|||||||
const [formToast, setFormToast] = useState<{ message: string; type: "success" | "error" } | null>(null);
|
const [formToast, setFormToast] = useState<{ message: string; type: "success" | "error" } | null>(null);
|
||||||
const [packycodeService, setPackycodeService] = useState<string>('bus'); // 默认公交车
|
const [packycodeService, setPackycodeService] = useState<string>('bus'); // 默认公交车
|
||||||
const [packycodeNode, setPackycodeNode] = useState<string>('https://api.packycode.com'); // 默认节点(公交车用)
|
const [packycodeNode, setPackycodeNode] = useState<string>('https://api.packycode.com'); // 默认节点(公交车用)
|
||||||
|
const [packycodeTaxiNode, setPackycodeTaxiNode] = useState<string>('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();
|
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
|
// 当适配器改变时更新认证方式和 URL
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formData.adapter === 'packycode') {
|
if (formData.adapter === 'packycode') {
|
||||||
@@ -795,7 +900,7 @@ const CreateStationDialog: React.FC<{
|
|||||||
...prev,
|
...prev,
|
||||||
auth_method: 'api_key', // PackyCode 固定使用 API Key
|
auth_method: 'api_key', // PackyCode 固定使用 API Key
|
||||||
api_url: packycodeService === 'taxi'
|
api_url: packycodeService === 'taxi'
|
||||||
? 'https://share-api.packycode.com'
|
? packycodeTaxiNode
|
||||||
: packycodeNode
|
: packycodeNode
|
||||||
}));
|
}));
|
||||||
} else if (formData.adapter === 'custom') {
|
} else if (formData.adapter === 'custom') {
|
||||||
@@ -809,7 +914,7 @@ const CreateStationDialog: React.FC<{
|
|||||||
auth_method: 'bearer_token'
|
auth_method: 'bearer_token'
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [formData.adapter, packycodeService, packycodeNode]);
|
}, [formData.adapter, packycodeService, packycodeNode, packycodeTaxiNode]);
|
||||||
|
|
||||||
// 自动填充中转站名称
|
// 自动填充中转站名称
|
||||||
const fillStationName = (serviceType: string) => {
|
const fillStationName = (serviceType: string) => {
|
||||||
@@ -1122,7 +1227,7 @@ const CreateStationDialog: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mt-3">
|
<p className="text-xs text-muted-foreground mt-3">
|
||||||
{packycodeService === 'taxi'
|
{packycodeService === 'taxi'
|
||||||
? `${t('relayStation.fixedUrl')}: https://share-api.packycode.com`
|
? t('relayStation.taxiServiceNote')
|
||||||
: t('relayStation.busServiceNote')
|
: t('relayStation.busServiceNote')
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
@@ -1178,18 +1283,18 @@ const CreateStationDialog: React.FC<{
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setFormToast({ message: "正在测速,请稍候...", type: "success" });
|
const busNodes = [
|
||||||
try {
|
{ url: "https://api.packycode.com", name: "🚌 直连1(默认公交车)" },
|
||||||
const best = await api.autoSelectBestNode();
|
{ url: "https://api-hk-cn2.packycode.com", name: "🇭🇰 直连2 (HK-CN2)" },
|
||||||
setPackycodeNode(best.url);
|
{ url: "https://api-us-cmin2.packycode.com", name: "🇺🇸 直连3 (US-CMIN2)" },
|
||||||
setFormData(prev => ({ ...prev, api_url: best.url }));
|
{ url: "https://api-us-4837.packycode.com", name: "🇺🇸 直连4 (US-4837)" },
|
||||||
setFormToast({
|
{ url: "https://api-us-cn2.packycode.com", name: "🔄 备用1 (US-CN2)" },
|
||||||
message: `已选择最快节点: ${best.name} (延迟: ${best.response_time}ms)`,
|
{ url: "https://api-cf-pro.packycode.com", name: "☁️ 备用2 (CF-Pro)" }
|
||||||
type: "success"
|
];
|
||||||
});
|
|
||||||
} catch (error) {
|
await performSpeedTest(busNodes, (bestNode) => {
|
||||||
setFormToast({ message: "节点测速失败", type: "error" });
|
setPackycodeNode(bestNode.url);
|
||||||
}
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
自动选择
|
自动选择
|
||||||
@@ -1203,6 +1308,58 @@ const CreateStationDialog: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{formData.adapter === 'packycode' && packycodeService === 'taxi' && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>{t('relayStation.nodeSelection')}</Label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Select
|
||||||
|
value={packycodeTaxiNode}
|
||||||
|
onValueChange={(value: string) => setPackycodeTaxiNode(value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={t('relayStation.selectNode')} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="https://share-api.packycode.com">
|
||||||
|
🚗 直连1(默认滴滴车)
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="https://share-api-cf-pro.packycode.com">
|
||||||
|
☁️ 备用1 (CF-Pro)
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="https://share-api-hk-cn2.packycode.com">
|
||||||
|
🇭🇰 备用2 (HK-CN2)
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={async () => {
|
||||||
|
const taxiNodes = [
|
||||||
|
{ url: "https://share-api.packycode.com", name: "🚗 直连1(默认滴滴车)" },
|
||||||
|
{ url: "https://share-api-cf-pro.packycode.com", name: "☁️ 备用1 (CF-Pro)" },
|
||||||
|
{ url: "https://share-api-hk-cn2.packycode.com", name: "🇭🇰 备用2 (HK-CN2)" }
|
||||||
|
];
|
||||||
|
|
||||||
|
await performSpeedTest(taxiNodes, (bestNode) => {
|
||||||
|
setPackycodeTaxiNode(bestNode.url);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
自动选择
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t('relayStation.selectedNode') + ': ' + packycodeTaxiNode}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="description">{t('relayStation.description')}</Label>
|
<Label htmlFor="description">{t('relayStation.description')}</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
@@ -1330,41 +1487,8 @@ const CreateStationDialog: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-between p-3 bg-muted/30 rounded-lg">
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<Switch
|
|
||||||
id="enabled"
|
|
||||||
checked={formData.enabled}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
setFormData(prev => ({ ...prev, enabled: checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="enabled" className="text-sm font-medium cursor-pointer">
|
|
||||||
{t('relayStation.enabled')}
|
|
||||||
</Label>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{t('relayStation.enabledNote')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 仅在选择 Custom 时显示名称输入框 */}
|
<div className="flex justify-end space-x-3 pt-3">
|
||||||
{formData.adapter === 'custom' && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="custom-name">{t('relayStation.name')} *</Label>
|
|
||||||
<Input
|
|
||||||
id="custom-name"
|
|
||||||
value={formData.name}
|
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
|
||||||
placeholder={t('relayStation.namePlaceholder')}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex justify-end space-x-3 pt-3 border-t">
|
|
||||||
<Button type="button" variant="outline" onClick={() => {}}>
|
<Button type="button" variant="outline" onClick={() => {}}>
|
||||||
{t('common.cancel')}
|
{t('common.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1388,6 +1512,56 @@ const CreateStationDialog: React.FC<{
|
|||||||
onDismiss={() => setFormToast(null)}
|
onDismiss={() => setFormToast(null)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 测速弹出框 */}
|
||||||
|
<Dialog open={showSpeedTestModal} onOpenChange={setShowSpeedTestModal}>
|
||||||
|
<DialogContent className="sm:max-w-[500px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t('relayStation.speedTest')}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{speedTestInProgress ? t('relayStation.testingNodes') : t('relayStation.testCompleted')}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{speedTestResults.map((result, index) => (
|
||||||
|
<div key={index} className="flex items-center justify-between p-3 border rounded-lg">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="text-sm font-medium">{result.name}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{result.status === 'testing' && (
|
||||||
|
<>
|
||||||
|
<div className="h-4 w-4 animate-spin rounded-full border-b-2 border-blue-600"></div>
|
||||||
|
<span className="text-sm text-blue-600">{t('relayStation.testing')}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{result.status === 'success' && (
|
||||||
|
<>
|
||||||
|
<div className="h-2 w-2 rounded-full bg-green-500"></div>
|
||||||
|
<span className="text-sm text-green-600">{result.responseTime}ms</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{result.status === 'failed' && (
|
||||||
|
<>
|
||||||
|
<div className="h-2 w-2 rounded-full bg-red-500"></div>
|
||||||
|
<span className="text-sm text-red-600">{t('relayStation.failed')}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{!speedTestInProgress && speedTestResults.length > 0 && (
|
||||||
|
<div className="pt-2 text-center">
|
||||||
|
<div className="text-sm text-green-600">
|
||||||
|
{t('relayStation.bestNodeSelected')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1427,6 +1601,18 @@ const EditStationDialog: React.FC<{
|
|||||||
}
|
}
|
||||||
return 'https://api.packycode.com';
|
return 'https://api.packycode.com';
|
||||||
});
|
});
|
||||||
|
const [packycodeTaxiNode, setPackycodeTaxiNode] = useState<string>(() => {
|
||||||
|
// 如果是PackyCode滴滴车服务,使用当前的API URL
|
||||||
|
if (station.adapter === 'packycode' && station.api_url.includes('share-api')) {
|
||||||
|
return station.api_url;
|
||||||
|
}
|
||||||
|
return '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();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -1460,6 +1646,75 @@ const EditStationDialog: 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
|
// 当适配器改变时更新认证方式和 URL
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formData.adapter === 'packycode') {
|
if (formData.adapter === 'packycode') {
|
||||||
@@ -1467,7 +1722,7 @@ const EditStationDialog: React.FC<{
|
|||||||
...prev,
|
...prev,
|
||||||
auth_method: 'api_key', // PackyCode 固定使用 API Key
|
auth_method: 'api_key', // PackyCode 固定使用 API Key
|
||||||
api_url: packycodeService === 'taxi'
|
api_url: packycodeService === 'taxi'
|
||||||
? 'https://share-api.packycode.com'
|
? packycodeTaxiNode
|
||||||
: packycodeNode
|
: packycodeNode
|
||||||
}));
|
}));
|
||||||
} else if (formData.adapter === 'custom') {
|
} else if (formData.adapter === 'custom') {
|
||||||
@@ -1481,7 +1736,7 @@ const EditStationDialog: React.FC<{
|
|||||||
auth_method: 'bearer_token'
|
auth_method: 'bearer_token'
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [formData.adapter, packycodeService, packycodeNode]);
|
}, [formData.adapter, packycodeService, packycodeNode, packycodeTaxiNode]);
|
||||||
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
@@ -1783,7 +2038,7 @@ const EditStationDialog: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mt-3">
|
<p className="text-xs text-muted-foreground mt-3">
|
||||||
{packycodeService === 'taxi'
|
{packycodeService === 'taxi'
|
||||||
? `${t('relayStation.fixedUrl')}: https://share-api.packycode.com`
|
? t('relayStation.taxiServiceNote')
|
||||||
: t('relayStation.busServiceNote')
|
: t('relayStation.busServiceNote')
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
@@ -1833,18 +2088,18 @@ const EditStationDialog: React.FC<{
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setFormToast({ message: "正在测速,请稍候...", type: "success" });
|
const busNodes = [
|
||||||
try {
|
{ url: "https://api.packycode.com", name: "🚌 直连1(默认公交车)" },
|
||||||
const best = await api.autoSelectBestNode();
|
{ url: "https://api-hk-cn2.packycode.com", name: "🇭🇰 直连2 (HK-CN2)" },
|
||||||
setPackycodeNode(best.url);
|
{ url: "https://api-us-cmin2.packycode.com", name: "🇺🇸 直连3 (US-CMIN2)" },
|
||||||
setFormData(prev => ({ ...prev, api_url: best.url }));
|
{ url: "https://api-us-4837.packycode.com", name: "🇺🇸 直连4 (US-4837)" },
|
||||||
setFormToast({
|
{ url: "https://api-us-cn2.packycode.com", name: "🔄 备用1 (US-CN2)" },
|
||||||
message: `已选择最快节点: ${best.name} (延迟: ${best.response_time}ms)`,
|
{ url: "https://api-cf-pro.packycode.com", name: "☁️ 备用2 (CF-Pro)" }
|
||||||
type: "success"
|
];
|
||||||
});
|
|
||||||
} catch (error) {
|
await performSpeedTest(busNodes, (bestNode) => {
|
||||||
setFormToast({ message: "节点测速失败", type: "error" });
|
setPackycodeNode(bestNode.url);
|
||||||
}
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
自动选择
|
自动选择
|
||||||
@@ -2005,19 +2260,6 @@ const EditStationDialog: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 仅在选择 Custom 时显示名称输入框 */}
|
|
||||||
{formData.adapter === 'custom' && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="custom-name">{t('relayStation.name')} *</Label>
|
|
||||||
<Input
|
|
||||||
id="custom-name"
|
|
||||||
value={formData.name}
|
|
||||||
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
|
||||||
placeholder={t('relayStation.namePlaceholder')}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex justify-end space-x-3 pt-3 border-t">
|
<div className="flex justify-end space-x-3 pt-3 border-t">
|
||||||
<Button type="button" variant="outline" onClick={onCancel}>
|
<Button type="button" variant="outline" onClick={onCancel}>
|
||||||
@@ -2043,6 +2285,56 @@ const EditStationDialog: React.FC<{
|
|||||||
onDismiss={() => setFormToast(null)}
|
onDismiss={() => setFormToast(null)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 测速弹出框 */}
|
||||||
|
<Dialog open={showSpeedTestModal} onOpenChange={setShowSpeedTestModal}>
|
||||||
|
<DialogContent className="sm:max-w-[500px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t('relayStation.speedTest')}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{speedTestInProgress ? t('relayStation.testingNodes') : t('relayStation.testCompleted')}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{speedTestResults.map((result, index) => (
|
||||||
|
<div key={index} className="flex items-center justify-between p-3 border rounded-lg">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="text-sm font-medium">{result.name}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{result.status === 'testing' && (
|
||||||
|
<>
|
||||||
|
<div className="h-4 w-4 animate-spin rounded-full border-b-2 border-blue-600"></div>
|
||||||
|
<span className="text-sm text-blue-600">{t('relayStation.testing')}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{result.status === 'success' && (
|
||||||
|
<>
|
||||||
|
<div className="h-2 w-2 rounded-full bg-green-500"></div>
|
||||||
|
<span className="text-sm text-green-600">{result.responseTime}ms</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{result.status === 'failed' && (
|
||||||
|
<>
|
||||||
|
<div className="h-2 w-2 rounded-full bg-red-500"></div>
|
||||||
|
<span className="text-sm text-red-600">{t('relayStation.failed')}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{!speedTestInProgress && speedTestResults.length > 0 && (
|
||||||
|
<div className="pt-2 text-center">
|
||||||
|
<div className="text-sm text-green-600">
|
||||||
|
{t('relayStation.bestNodeSelected')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -2248,6 +2248,19 @@ export const api = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flush system DNS cache
|
||||||
|
* @returns Promise resolving to success message
|
||||||
|
*/
|
||||||
|
async flushDns(): Promise<string> {
|
||||||
|
try {
|
||||||
|
return await invoke<string>("flush_dns");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to flush DNS:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets current API config from Claude settings
|
* Gets current API config from Claude settings
|
||||||
* @returns Promise resolving to current config info
|
* @returns Promise resolving to current config info
|
||||||
|
@@ -889,6 +889,9 @@
|
|||||||
"configSaved": "Config saved",
|
"configSaved": "Config saved",
|
||||||
"invalidJson": "Invalid JSON format",
|
"invalidJson": "Invalid JSON format",
|
||||||
"saveFailed": "Save failed",
|
"saveFailed": "Save failed",
|
||||||
|
"flushDns": "Flush DNS",
|
||||||
|
"flushDnsSuccess": "DNS cache flushed",
|
||||||
|
"flushDnsFailed": "DNS flush failed",
|
||||||
"syncFailed": "Failed to sync configuration",
|
"syncFailed": "Failed to sync configuration",
|
||||||
"currentConfig": "Current Configuration",
|
"currentConfig": "Current Configuration",
|
||||||
"notConfigured": "Not configured",
|
"notConfigured": "Not configured",
|
||||||
@@ -898,14 +901,17 @@
|
|||||||
"busService": "Bus",
|
"busService": "Bus",
|
||||||
"taxiServiceDesc": "Fast & Stable (share.packycode.com)",
|
"taxiServiceDesc": "Fast & Stable (share.packycode.com)",
|
||||||
"busServiceDesc": "Shared Economy (packycode.com)",
|
"busServiceDesc": "Shared Economy (packycode.com)",
|
||||||
"selectService": "Select a service type",
|
"taxiServiceNote": "Select a node or use auto-selection for optimal performance",
|
||||||
"fixedUrl": "Fixed URL",
|
|
||||||
"busServiceNote": "Select a node or use auto-selection for optimal performance",
|
"busServiceNote": "Select a node or use auto-selection for optimal performance",
|
||||||
"nodeSelection": "Node Selection",
|
"nodeSelection": "Node Selection",
|
||||||
"selectNode": "Select a node",
|
"selectNode": "Select a node",
|
||||||
"autoSelect": "Auto-select fastest",
|
"autoSelect": "Auto-select fastest",
|
||||||
"autoSelectDesc": "Will automatically test and select the fastest node",
|
"autoSelectDesc": "Will automatically test and select the fastest node",
|
||||||
"selectedNode": "Selected",
|
"selectedNode": "Selected",
|
||||||
|
"speedTest": "Speed Test",
|
||||||
|
"testingNodes": "Testing node speeds...",
|
||||||
|
"testing": "Testing",
|
||||||
|
"bestNodeSelected": "Best node selected",
|
||||||
"testSpeed": "Test Speed",
|
"testSpeed": "Test Speed",
|
||||||
"testResults": "Speed Test Results",
|
"testResults": "Speed Test Results",
|
||||||
"failed": "Failed",
|
"failed": "Failed",
|
||||||
|
@@ -816,6 +816,9 @@
|
|||||||
"configSaved": "配置已保存",
|
"configSaved": "配置已保存",
|
||||||
"invalidJson": "JSON 格式无效",
|
"invalidJson": "JSON 格式无效",
|
||||||
"saveFailed": "保存失败",
|
"saveFailed": "保存失败",
|
||||||
|
"flushDns": "刷新 DNS",
|
||||||
|
"flushDnsSuccess": "DNS 缓存已刷新",
|
||||||
|
"flushDnsFailed": "DNS 刷新失败",
|
||||||
"syncFailed": "同步配置失败",
|
"syncFailed": "同步配置失败",
|
||||||
"currentConfig": "当前配置",
|
"currentConfig": "当前配置",
|
||||||
"notConfigured": "未配置",
|
"notConfigured": "未配置",
|
||||||
@@ -825,14 +828,17 @@
|
|||||||
"busService": "公交车",
|
"busService": "公交车",
|
||||||
"taxiServiceDesc": "高速稳定 (share.packycode.com)",
|
"taxiServiceDesc": "高速稳定 (share.packycode.com)",
|
||||||
"busServiceDesc": "共享经济 (packycode.com)",
|
"busServiceDesc": "共享经济 (packycode.com)",
|
||||||
"selectService": "选择服务类型",
|
"taxiServiceNote": "选择节点或使用自动选择以获得最佳性能",
|
||||||
"fixedUrl": "固定地址",
|
|
||||||
"busServiceNote": "选择节点或使用自动选择以获得最佳性能",
|
"busServiceNote": "选择节点或使用自动选择以获得最佳性能",
|
||||||
"nodeSelection": "节点选择",
|
"nodeSelection": "节点选择",
|
||||||
"selectNode": "选择节点",
|
"selectNode": "选择节点",
|
||||||
"autoSelect": "自动选择最快",
|
"autoSelect": "自动选择最快",
|
||||||
"autoSelectDesc": "将自动测试并选择最快的节点",
|
"autoSelectDesc": "将自动测试并选择最快的节点",
|
||||||
"selectedNode": "已选择",
|
"selectedNode": "已选择",
|
||||||
|
"speedTest": "节点测速",
|
||||||
|
"testingNodes": "正在测试节点速度...",
|
||||||
|
"testing": "测试中",
|
||||||
|
"bestNodeSelected": "已选择最快节点",
|
||||||
"testSpeed": "测速",
|
"testSpeed": "测速",
|
||||||
"testResults": "测速结果",
|
"testResults": "测速结果",
|
||||||
"failed": "失败",
|
"failed": "失败",
|
||||||
@@ -870,4 +876,3 @@
|
|||||||
"title": "警告"
|
"title": "警告"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user