修改中转站
This commit is contained in:
@@ -15,3 +15,4 @@ pub mod filesystem;
|
||||
pub mod git;
|
||||
pub mod terminal;
|
||||
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,
|
||||
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");
|
||||
|
@@ -64,6 +64,7 @@ const RelayStationManager: React.FC<RelayStationManagerProps> = ({ onBack }) =>
|
||||
const [editingConfig, setEditingConfig] = useState(false);
|
||||
const [configJson, setConfigJson] = useState<string>('');
|
||||
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<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 额度
|
||||
const fetchPackycodeQuota = async (stationId: string) => {
|
||||
@@ -410,14 +425,29 @@ const RelayStationManager: React.FC<RelayStationManagerProps> = ({ onBack }) =>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium">{t('relayStation.configPreview')}</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setJsonConfigView(true)}
|
||||
>
|
||||
<Eye className="h-4 w-4 mr-1" />
|
||||
{t('relayStation.viewJson')}
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setJsonConfigView(true)}
|
||||
>
|
||||
<Eye className="h-4 w-4 mr-1" />
|
||||
{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 className="space-y-2 text-sm">
|
||||
<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 [packycodeService, setPackycodeService] = useState<string>('bus'); // 默认公交车
|
||||
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();
|
||||
|
||||
@@ -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<{
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-3">
|
||||
{packycodeService === 'taxi'
|
||||
? `${t('relayStation.fixedUrl')}: https://share-api.packycode.com`
|
||||
? t('relayStation.taxiServiceNote')
|
||||
: t('relayStation.busServiceNote')
|
||||
}
|
||||
</p>
|
||||
@@ -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<{
|
||||
</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">
|
||||
<Label htmlFor="description">{t('relayStation.description')}</Label>
|
||||
<Textarea
|
||||
@@ -1330,41 +1487,8 @@ const CreateStationDialog: React.FC<{
|
||||
</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 时显示名称输入框 */}
|
||||
{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">
|
||||
<Button type="button" variant="outline" onClick={() => {}}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
@@ -1388,6 +1512,56 @@ const CreateStationDialog: React.FC<{
|
||||
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';
|
||||
});
|
||||
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();
|
||||
|
||||
@@ -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
|
||||
useEffect(() => {
|
||||
if (formData.adapter === 'packycode') {
|
||||
@@ -1467,7 +1722,7 @@ const EditStationDialog: 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') {
|
||||
@@ -1481,7 +1736,7 @@ const EditStationDialog: React.FC<{
|
||||
auth_method: 'bearer_token'
|
||||
}));
|
||||
}
|
||||
}, [formData.adapter, packycodeService, packycodeNode]);
|
||||
}, [formData.adapter, packycodeService, packycodeNode, packycodeTaxiNode]);
|
||||
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
@@ -1783,7 +2038,7 @@ const EditStationDialog: React.FC<{
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-3">
|
||||
{packycodeService === 'taxi'
|
||||
? `${t('relayStation.fixedUrl')}: https://share-api.packycode.com`
|
||||
? t('relayStation.taxiServiceNote')
|
||||
: t('relayStation.busServiceNote')
|
||||
}
|
||||
</p>
|
||||
@@ -1833,18 +2088,18 @@ const EditStationDialog: 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);
|
||||
});
|
||||
}}
|
||||
>
|
||||
自动选择
|
||||
@@ -2005,19 +2260,6 @@ const EditStationDialog: React.FC<{
|
||||
</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">
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
@@ -2043,6 +2285,56 @@ const EditStationDialog: React.FC<{
|
||||
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
|
||||
* @returns Promise resolving to current config info
|
||||
|
@@ -889,6 +889,9 @@
|
||||
"configSaved": "Config saved",
|
||||
"invalidJson": "Invalid JSON format",
|
||||
"saveFailed": "Save failed",
|
||||
"flushDns": "Flush DNS",
|
||||
"flushDnsSuccess": "DNS cache flushed",
|
||||
"flushDnsFailed": "DNS flush failed",
|
||||
"syncFailed": "Failed to sync configuration",
|
||||
"currentConfig": "Current Configuration",
|
||||
"notConfigured": "Not configured",
|
||||
@@ -898,14 +901,17 @@
|
||||
"busService": "Bus",
|
||||
"taxiServiceDesc": "Fast & Stable (share.packycode.com)",
|
||||
"busServiceDesc": "Shared Economy (packycode.com)",
|
||||
"selectService": "Select a service type",
|
||||
"fixedUrl": "Fixed URL",
|
||||
"taxiServiceNote": "Select a node or use auto-selection for optimal performance",
|
||||
"busServiceNote": "Select a node or use auto-selection for optimal performance",
|
||||
"nodeSelection": "Node Selection",
|
||||
"selectNode": "Select a node",
|
||||
"autoSelect": "Auto-select fastest",
|
||||
"autoSelectDesc": "Will automatically test and select the fastest node",
|
||||
"selectedNode": "Selected",
|
||||
"speedTest": "Speed Test",
|
||||
"testingNodes": "Testing node speeds...",
|
||||
"testing": "Testing",
|
||||
"bestNodeSelected": "Best node selected",
|
||||
"testSpeed": "Test Speed",
|
||||
"testResults": "Speed Test Results",
|
||||
"failed": "Failed",
|
||||
|
@@ -816,6 +816,9 @@
|
||||
"configSaved": "配置已保存",
|
||||
"invalidJson": "JSON 格式无效",
|
||||
"saveFailed": "保存失败",
|
||||
"flushDns": "刷新 DNS",
|
||||
"flushDnsSuccess": "DNS 缓存已刷新",
|
||||
"flushDnsFailed": "DNS 刷新失败",
|
||||
"syncFailed": "同步配置失败",
|
||||
"currentConfig": "当前配置",
|
||||
"notConfigured": "未配置",
|
||||
@@ -825,14 +828,17 @@
|
||||
"busService": "公交车",
|
||||
"taxiServiceDesc": "高速稳定 (share.packycode.com)",
|
||||
"busServiceDesc": "共享经济 (packycode.com)",
|
||||
"selectService": "选择服务类型",
|
||||
"fixedUrl": "固定地址",
|
||||
"taxiServiceNote": "选择节点或使用自动选择以获得最佳性能",
|
||||
"busServiceNote": "选择节点或使用自动选择以获得最佳性能",
|
||||
"nodeSelection": "节点选择",
|
||||
"selectNode": "选择节点",
|
||||
"autoSelect": "自动选择最快",
|
||||
"autoSelectDesc": "将自动测试并选择最快的节点",
|
||||
"selectedNode": "已选择",
|
||||
"speedTest": "节点测速",
|
||||
"testingNodes": "正在测试节点速度...",
|
||||
"testing": "测试中",
|
||||
"bestNodeSelected": "已选择最快节点",
|
||||
"testSpeed": "测速",
|
||||
"testResults": "测速结果",
|
||||
"failed": "失败",
|
||||
@@ -870,4 +876,3 @@
|
||||
"title": "警告"
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user