修改中转站

This commit is contained in:
2025-09-06 15:07:09 +08:00
parent e8fe999d16
commit a91b3ebbb2
7 changed files with 473 additions and 90 deletions

View File

@@ -15,3 +15,4 @@ pub mod filesystem;
pub mod git;
pub mod terminal;
pub mod ccr;
pub mod system;

View 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())
}
}

View File

@@ -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");

View File

@@ -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>
</>
);
};

View File

@@ -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

View File

@@ -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",

View File

@@ -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": "警告"
}
}