Compare commits
11 Commits
dev
...
59aa67d456
| Author | SHA1 | Date | |
|---|---|---|---|
| 59aa67d456 | |||
| 2a1ee3018d | |||
| 76e83e0135 | |||
| 89d6ec3c76 | |||
| 670eb7ebc9 | |||
| 46295f4543 | |||
| a47174df56 | |||
| 748ec6bee9 | |||
| a884e6c838 | |||
| 920b86e56a | |||
| a78b40c339 |
+3
-2
@@ -1,3 +1,4 @@
|
||||
[submodule "libcore"]
|
||||
path = libcore
|
||||
url = https://github.com/hiddify/hiddify-next-core
|
||||
path = libcore
|
||||
url = https://github.com/hiddify/hiddify-next-core
|
||||
branch = main
|
||||
|
||||
@@ -57,7 +57,7 @@ android {
|
||||
defaultConfig {
|
||||
applicationId "app.hifastvpn.com"
|
||||
minSdkVersion flutter.minSdkVersion
|
||||
targetSdkVersion 36
|
||||
targetSdkVersion 34
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
multiDexEnabled true
|
||||
@@ -105,8 +105,8 @@ android {
|
||||
debugSymbolLevel 'FULL'
|
||||
}
|
||||
// 禁用代码混淆和资源压缩,解决VPN连接问题
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
minifyEnabled false
|
||||
shrinkResources false
|
||||
// proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,11 @@
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<!-- 如果 targetSdkVersion >= 33 -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<application
|
||||
android:name=".Application"
|
||||
android:banner="@mipmap/ic_banner"
|
||||
|
||||
@@ -193,7 +193,7 @@
|
||||
"rewardDetails": "Reward Details >",
|
||||
"steps": "Invitation Steps",
|
||||
"inviteFriend": "Invite Friends",
|
||||
"acceptInvite": "Friends accept invitation\nPlace order and register",
|
||||
"acceptInvite": "Friends accept invitationPlace order and register",
|
||||
"getReward": "Get Reward",
|
||||
"shareLink": "Share Link",
|
||||
"shareQR": "Share QR Code",
|
||||
@@ -398,7 +398,7 @@
|
||||
"content": "Would you like to update now?",
|
||||
"updateNow": "Update Now",
|
||||
"updateLater": "Later",
|
||||
"defaultContent": "1. Optimize app performance\n2. Fix known issues\n3. Improve user experience"
|
||||
"defaultContent": "1. Optimize app performance2. Fix known issues3. Improve user experience"
|
||||
},
|
||||
"country": {
|
||||
"cn": "China",
|
||||
|
||||
@@ -256,7 +256,7 @@
|
||||
"rewardDetails": "Detalles de recompensa >",
|
||||
"steps": "Pasos de invitación",
|
||||
"inviteFriend": "Invitar amigo",
|
||||
"acceptInvite": "El amigo acepta la invitación\ny se registra",
|
||||
"acceptInvite": "El amigo acepta la invitacióny se registra",
|
||||
"getReward": "Obtener recompensa",
|
||||
"shareLink": "Compartir por enlace",
|
||||
"shareQR": "Compartir por código QR",
|
||||
@@ -404,7 +404,7 @@
|
||||
"content": "¿Actualizar ahora?",
|
||||
"updateNow": "Actualizar ahora",
|
||||
"updateLater": "Más tarde",
|
||||
"defaultContent": "1. Optimización del rendimiento de la aplicación\n2. Corrección de problemas conocidos\n3. Mejora de la experiencia del usuario"
|
||||
"defaultContent": "1. Optimización del rendimiento de la aplicación2. Corrección de problemas conocidos3. Mejora de la experiencia del usuario"
|
||||
},
|
||||
"kr_invite": {
|
||||
"close": "Cerrar",
|
||||
|
||||
@@ -193,7 +193,7 @@
|
||||
"rewardDetails": "Tasu üksikasjad >",
|
||||
"steps": "Kutse Sammud",
|
||||
"inviteFriend": "Kutsu Sõbrad",
|
||||
"acceptInvite": "Sõbrad aktsepteerivad kutset\nTee tellimus ja registreeru",
|
||||
"acceptInvite": "Sõbrad aktsepteerivad kutsetTee tellimus ja registreeru",
|
||||
"getReward": "Saada Tasu",
|
||||
"shareLink": "Jaga Linki",
|
||||
"shareQR": "Jaga QR-koodi",
|
||||
@@ -395,7 +395,7 @@
|
||||
"content": "Uuendada nüüd?",
|
||||
"updateNow": "Uuenda nüüd",
|
||||
"updateLater": "Hiljem",
|
||||
"defaultContent": "1. Rakenduse jõudluse optimeerimine\n2. Teadaolevate probleemide parandamine\n3. Kasutajamugavuse parandamine"
|
||||
"defaultContent": "1. Rakenduse jõudluse optimeerimine2. Teadaolevate probleemide parandamine3. Kasutajamugavuse parandamine"
|
||||
},
|
||||
"country": {
|
||||
"cn": "Hiina",
|
||||
|
||||
@@ -209,7 +209,7 @@
|
||||
"rewardDetails": "報酬の詳細 >",
|
||||
"steps": "招待の手順",
|
||||
"inviteFriend": "友達を招待",
|
||||
"acceptInvite": "友達が招待を受け入れ\n注文して登録",
|
||||
"acceptInvite": "友達が招待を受け入れ注文して登録",
|
||||
"getReward": "報酬を獲得",
|
||||
"shareLink": "リンクを共有",
|
||||
"shareQR": "QRコードを共有",
|
||||
@@ -412,7 +412,7 @@
|
||||
"content": "今すぐアップデートしますか?",
|
||||
"updateNow": "今すぐアップデート",
|
||||
"updateLater": "後で",
|
||||
"defaultContent": "1. アプリのパフォーマンス最適化\n2. 既知の問題の修正\n3. ユーザー体験の向上"
|
||||
"defaultContent": "1. アプリのパフォーマンス最適化2. 既知の問題の修正3. ユーザー体験の向上"
|
||||
},
|
||||
"country": {
|
||||
"cn": "中国",
|
||||
|
||||
@@ -387,7 +387,7 @@
|
||||
"content": "Хотите обновить сейчас?",
|
||||
"updateNow": "Обновить сейчас",
|
||||
"later": "Позже",
|
||||
"defaultContent": "1. Оптимизация производительности приложения\n2. Исправление известных проблем\n3. Улучшение пользовательского опыта"
|
||||
"defaultContent": "1. Оптимизация производительности приложения2. Исправление известных проблем3. Улучшение пользовательского опыта"
|
||||
},
|
||||
"country": {
|
||||
"cn": "Китай",
|
||||
|
||||
@@ -232,13 +232,13 @@
|
||||
"longestConnection": "最长连接时间",
|
||||
"days": "{days}天",
|
||||
"daysOfWeek": {
|
||||
"monday": "周\n一",
|
||||
"tuesday": "周\n二",
|
||||
"wednesday": "周\n三",
|
||||
"thursday": "周\n四",
|
||||
"friday": "周\n五",
|
||||
"saturday": "周\n六",
|
||||
"sunday": "周\n日"
|
||||
"monday": "周一",
|
||||
"tuesday": "周二",
|
||||
"wednesday": "周三",
|
||||
"thursday": "周四",
|
||||
"friday": "周五",
|
||||
"saturday": "周六",
|
||||
"sunday": "周日"
|
||||
},
|
||||
"processTrafficFailed": "处理流量日志数据失败"
|
||||
},
|
||||
@@ -256,7 +256,7 @@
|
||||
"rewardDetails": "奖励明细 >",
|
||||
"steps": "邀请步骤",
|
||||
"inviteFriend": "邀请好友",
|
||||
"acceptInvite": "好友接受邀请\n下单并注册",
|
||||
"acceptInvite": "好友接受邀请下单并注册",
|
||||
"getReward": "获得奖励",
|
||||
"shareLink": "分享链接",
|
||||
"shareQR": "分享二维码",
|
||||
@@ -415,7 +415,7 @@
|
||||
"content": "是否立即更新?",
|
||||
"updateNow": "立即更新",
|
||||
"updateLater": "稍后再说",
|
||||
"defaultContent": "1. 优化应用性能\n2. 修复已知问题\n3. 提升用户体验"
|
||||
"defaultContent": "1. 优化应用性能2. 修复已知问题3. 提升用户体验"
|
||||
},
|
||||
"country": {
|
||||
"cn": "中国",
|
||||
|
||||
@@ -170,13 +170,13 @@
|
||||
"longestConnection": "最長連接時間",
|
||||
"days": "{days}天",
|
||||
"daysOfWeek": {
|
||||
"monday": "週\n一",
|
||||
"tuesday": "週\n二",
|
||||
"wednesday": "週\n三",
|
||||
"thursday": "週\n四",
|
||||
"friday": "週\n五",
|
||||
"saturday": "週\n六",
|
||||
"sunday": "週\n日"
|
||||
"monday": "週一",
|
||||
"tuesday": "週二",
|
||||
"wednesday": "週三",
|
||||
"thursday": "週四",
|
||||
"friday": "週五",
|
||||
"saturday": "週六",
|
||||
"sunday": "週日"
|
||||
},
|
||||
"processTrafficFailed": "處理流量日誌資料失敗"
|
||||
},
|
||||
@@ -194,7 +194,7 @@
|
||||
"rewardDetails": "獎勵明細 >",
|
||||
"steps": "邀請步驟",
|
||||
"inviteFriend": "邀請好友",
|
||||
"acceptInvite": "好友接受邀請\n下單並註冊",
|
||||
"acceptInvite": "好友接受邀請下單並註冊",
|
||||
"getReward": "獲得獎勵",
|
||||
"shareLink": "分享連結",
|
||||
"shareQR": "分享二維碼",
|
||||
@@ -332,7 +332,7 @@
|
||||
"content": "是否立即更新?",
|
||||
"updateNow": "立即更新",
|
||||
"updateLater": "稍後",
|
||||
"defaultContent": "1. 優化應用性能\n2. 修復已知問題\n3. 改進用戶體驗"
|
||||
"defaultContent": "1. 優化應用性能2. 修復已知問題3. 改進用戶體驗"
|
||||
},
|
||||
"orderStatus": {
|
||||
"title": "訂單狀態",
|
||||
|
||||
+6
-1
@@ -1,4 +1,5 @@
|
||||
name: BearVPN
|
||||
app_name: BearVPN
|
||||
version: 1.0.0
|
||||
build_number: 1
|
||||
targets:
|
||||
@@ -10,4 +11,8 @@ targets:
|
||||
pkg:
|
||||
enable: true
|
||||
# 不签名
|
||||
sign: false
|
||||
sign: false
|
||||
windows:
|
||||
exe:
|
||||
enable: true
|
||||
icon: assets/images/tray_icon.ico
|
||||
@@ -28,14 +28,66 @@ class KROutboundItem {
|
||||
|
||||
/// URL
|
||||
String url = "";
|
||||
@override
|
||||
String toString() {
|
||||
return 'KROutboundItem(tag: $tag, country: $country, delay: ${urlTestDelay.value}ms)';
|
||||
}
|
||||
// ✅ 1. 将传入的 nodeListItem 保存为类的 final 成员变量
|
||||
final KrNodeListItem nodeListItem;
|
||||
/// 服务器类型
|
||||
|
||||
/// 构造函数,接受 KrNodeListItem 对象并初始化 KROutboundItem
|
||||
KROutboundItem(KrNodeListItem nodeListItem) {
|
||||
/// 构造函数,接受 KrItem 对象并初始化 KROutboundItem
|
||||
KROutboundItem(this.nodeListItem) {
|
||||
_initFromNodeListItem();
|
||||
}
|
||||
|
||||
/// 静态工厂:构造虚拟 urltest 节点(用于 ${country}-auto)
|
||||
factory KROutboundItem.fromVirtual(String tag, String country, Map<String, dynamic> config) {
|
||||
// 构造一个虚拟 KrNodeListItem,仅填充必要字段
|
||||
final virtualNode = KrNodeListItem(
|
||||
id: 0,
|
||||
name: tag,
|
||||
protocol: 'urltest',
|
||||
serverAddr: '',
|
||||
port: 0,
|
||||
uuid: '',
|
||||
config: jsonEncode(config),
|
||||
city: '',
|
||||
country: country,
|
||||
tags: [],
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
latitudeCountry: 0,
|
||||
longitudeCountry: 0,
|
||||
relayNode: '',
|
||||
relayMode: 'none',
|
||||
protocols: '',
|
||||
method: '',
|
||||
speedLimit: 0,
|
||||
traffic: 0,
|
||||
trafficRatio: 0,
|
||||
upload: 0,
|
||||
download: 0,
|
||||
startTime: '',
|
||||
expireTime: '',
|
||||
);
|
||||
return KROutboundItem._virtual(virtualNode);
|
||||
}
|
||||
|
||||
/// 私有构造:用于虚拟节点,避免重复解析
|
||||
KROutboundItem._virtual(this.nodeListItem) {
|
||||
// 直接填充虚拟节点所需字段
|
||||
id = nodeListItem.id.toString();
|
||||
protocol = nodeListItem.protocol;
|
||||
tag = nodeListItem.name;
|
||||
serverAddr = nodeListItem.serverAddr;
|
||||
city = nodeListItem.city;
|
||||
country = nodeListItem.country;
|
||||
latitude = nodeListItem.latitude;
|
||||
latitudeCountry = nodeListItem.latitudeCountry;
|
||||
longitude = nodeListItem.longitude;
|
||||
longitudeCountry = nodeListItem.longitudeCountry;
|
||||
config = jsonDecode(nodeListItem.config); // 已知 config 有效
|
||||
}
|
||||
|
||||
/// 初始化逻辑提取,供主构造调用
|
||||
void _initFromNodeListItem() {
|
||||
id = nodeListItem.id.toString();
|
||||
protocol = nodeListItem.protocol;
|
||||
latitude = nodeListItem.latitude;
|
||||
@@ -58,160 +110,163 @@ class KROutboundItem {
|
||||
return;
|
||||
}
|
||||
|
||||
// 兜底:尝试解析 config 字段(旧API格式)
|
||||
if (nodeListItem.config.isEmpty) {
|
||||
if (kDebugMode) {
|
||||
print('❌ 节点 ${nodeListItem.name} 缺少配置信息(无port或config)');
|
||||
}
|
||||
config = {};
|
||||
return;
|
||||
}
|
||||
|
||||
late Map<String, dynamic> json;
|
||||
try {
|
||||
json = jsonDecode(nodeListItem.config) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('❌ 节点 ${nodeListItem.name} 的 config 解析失败: $e,尝试使用直接字段');
|
||||
}
|
||||
if (kDebugMode) {
|
||||
print('📄 Config 内容: ${nodeListItem.config}');
|
||||
}
|
||||
_buildConfigFromFields(nodeListItem);
|
||||
return;
|
||||
}
|
||||
switch (nodeListItem.protocol) {
|
||||
case "vless":
|
||||
final securityConfig =
|
||||
json["security_config"] as Map<String, dynamic>? ?? {};
|
||||
|
||||
// 智能设置 server_name
|
||||
String serverName = securityConfig["sni"] ?? "";
|
||||
if (serverName.isEmpty) {
|
||||
serverName = nodeListItem.serverAddr;
|
||||
// 兜底:尝试从 config 字段解析(旧API格式)
|
||||
if (nodeListItem.config.isNotEmpty) {
|
||||
try {
|
||||
final json = jsonDecode(nodeListItem.config) as Map<String, dynamic>;
|
||||
if (kDebugMode) {
|
||||
print('📄 解析到 config JSON: $json');
|
||||
}
|
||||
|
||||
config = {
|
||||
"type": "vless",
|
||||
"tag": nodeListItem.name,
|
||||
"server": nodeListItem.serverAddr,
|
||||
"server_port": json["port"],
|
||||
"uuid": nodeListItem.uuid,
|
||||
if (json["flow"] != null && json["flow"] != "none")
|
||||
"flow": json["flow"],
|
||||
if (json["transport"] != null && json["transport"] != "tcp")
|
||||
"transport": _buildTransport(json),
|
||||
"tls": {
|
||||
"enabled": json["security"] == "tls",
|
||||
"server_name": serverName,
|
||||
"insecure": securityConfig["allow_insecure"] ?? true,
|
||||
"utls": {
|
||||
"enabled": true,
|
||||
"fingerprint": securityConfig["fingerprint"] ?? "chrome"
|
||||
|
||||
// 提取 transport 配置
|
||||
Map<String, dynamic>? transportConfig;
|
||||
if (json['transport'] != null && json['transport'] != 'tcp') {
|
||||
transportConfig = _buildTransport(json);
|
||||
if (kDebugMode) {
|
||||
print('✅ 找到 transport 配置: $transportConfig');
|
||||
}
|
||||
}
|
||||
|
||||
// 提取 security_config
|
||||
Map<String, dynamic>? securityConfig;
|
||||
if (json['security_config'] != null) {
|
||||
securityConfig = json['security_config'] as Map<String, dynamic>;
|
||||
if (kDebugMode) {
|
||||
print('✅ 找到 security_config: $securityConfig');
|
||||
}
|
||||
}
|
||||
|
||||
// 根据协议类型构建配置
|
||||
switch (nodeListItem.protocol) {
|
||||
case "shadowsocks":
|
||||
config = {
|
||||
"type": "shadowsocks",
|
||||
"tag": nodeListItem.name,
|
||||
"server": nodeListItem.serverAddr,
|
||||
"server_port": json["port"],
|
||||
"method": json["method"],
|
||||
"password": nodeListItem.uuid
|
||||
};
|
||||
break;
|
||||
case "vless":
|
||||
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||
.hasMatch(nodeListItem.serverAddr);
|
||||
|
||||
config = {
|
||||
"type": "vless",
|
||||
"tag": nodeListItem.name,
|
||||
"server": nodeListItem.serverAddr,
|
||||
"server_port": json["port"],
|
||||
"uuid": nodeListItem.uuid,
|
||||
if (transportConfig != null) "transport": transportConfig,
|
||||
if (json["security"] == "tls") "tls": {
|
||||
"enabled": true,
|
||||
if (isDomain) "server_name": securityConfig?["sni"] ?? nodeListItem.serverAddr,
|
||||
"insecure": securityConfig?["allow_insecure"] ?? true,
|
||||
"utls": {
|
||||
"enabled": true,
|
||||
"fingerprint": securityConfig?["fingerprint"] ?? "chrome"
|
||||
}
|
||||
}
|
||||
};
|
||||
break;
|
||||
case "vmess":
|
||||
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||
.hasMatch(nodeListItem.serverAddr);
|
||||
|
||||
config = {
|
||||
"type": "vmess",
|
||||
"tag": nodeListItem.name,
|
||||
"server": nodeListItem.serverAddr,
|
||||
"server_port": json["port"],
|
||||
"uuid": nodeListItem.uuid,
|
||||
"alter_id": 0,
|
||||
"security": "auto",
|
||||
if (transportConfig != null) "transport": transportConfig,
|
||||
if (json["security"] == "tls") "tls": {
|
||||
"enabled": true,
|
||||
if (isDomain) "server_name": securityConfig?["sni"] ?? nodeListItem.serverAddr,
|
||||
"insecure": securityConfig?["allow_insecure"] ?? true,
|
||||
"utls": {
|
||||
"enabled": true,
|
||||
"fingerprint": securityConfig?["fingerprint"] ?? "chrome"
|
||||
}
|
||||
}
|
||||
};
|
||||
break;
|
||||
case "trojan":
|
||||
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||
.hasMatch(nodeListItem.serverAddr);
|
||||
|
||||
config = {
|
||||
"type": "trojan",
|
||||
"tag": nodeListItem.name,
|
||||
"server": nodeListItem.serverAddr,
|
||||
"server_port": json["port"],
|
||||
"password": nodeListItem.uuid,
|
||||
if (transportConfig != null) "transport": transportConfig,
|
||||
"tls": {
|
||||
"enabled": json["security"] == "tls",
|
||||
if (isDomain) "server_name": securityConfig?["sni"] ?? nodeListItem.serverAddr,
|
||||
"insecure": securityConfig?["allow_insecure"] ?? true,
|
||||
"utls": {
|
||||
"enabled": true,
|
||||
"fingerprint": securityConfig?["fingerprint"] ?? "chrome"
|
||||
}
|
||||
}
|
||||
};
|
||||
break;
|
||||
case "hysteria":
|
||||
case "hysteria2":
|
||||
final securityConfig = json["security_config"] as Map<String, dynamic>? ?? {};
|
||||
config = {
|
||||
"type": "hysteria2",
|
||||
"tag": nodeListItem.name,
|
||||
"server": nodeListItem.serverAddr,
|
||||
"server_port": json["port"],
|
||||
"password": nodeListItem.uuid,
|
||||
"up_mbps": 100,
|
||||
"down_mbps": 100,
|
||||
"obfs": {
|
||||
"type": "salamander",
|
||||
"password": json["obfs_password"] ?? nodeListItem.uuid
|
||||
},
|
||||
"tls": {
|
||||
"enabled": true,
|
||||
"server_name": securityConfig["sni"] ?? "",
|
||||
"insecure": securityConfig["allow_insecure"] ?? true
|
||||
}
|
||||
};
|
||||
break;
|
||||
default:
|
||||
if (kDebugMode) {
|
||||
print('⚠️ 不支持的协议类型: ${nodeListItem.protocol}');
|
||||
}
|
||||
}
|
||||
};
|
||||
break;
|
||||
case "vmess":
|
||||
final securityConfig =
|
||||
json["security_config"] as Map<String, dynamic>? ?? {};
|
||||
|
||||
// 智能设置 server_name
|
||||
String serverName = securityConfig["sni"] ?? "";
|
||||
if (serverName.isEmpty) {
|
||||
serverName = nodeListItem.serverAddr;
|
||||
config = {};
|
||||
}
|
||||
|
||||
config = {
|
||||
"type": "vmess",
|
||||
"tag": nodeListItem.name,
|
||||
"server": nodeListItem.serverAddr,
|
||||
"server_port": json["port"],
|
||||
"uuid": nodeListItem.uuid,
|
||||
"alter_id": 0,
|
||||
"security": "auto",
|
||||
if (json["transport"] != null && json["transport"] != "tcp")
|
||||
"transport": _buildTransport(json),
|
||||
"tls": {
|
||||
"enabled": json["security"] == "tls",
|
||||
"server_name": serverName,
|
||||
"insecure": securityConfig["allow_insecure"] ?? true,
|
||||
"utls": {"enabled": true, "fingerprint": "chrome"}
|
||||
}
|
||||
};
|
||||
break;
|
||||
case "shadowsocks":
|
||||
config = {
|
||||
"type": "shadowsocks",
|
||||
"tag": nodeListItem.name,
|
||||
"server": nodeListItem.serverAddr,
|
||||
"server_port": json["port"],
|
||||
"method": json["method"],
|
||||
"password": nodeListItem.uuid
|
||||
};
|
||||
break;
|
||||
case "hysteria":
|
||||
case "hysteria2":
|
||||
// 后端的 "hysteria" 实际上是 Hysteria2 协议
|
||||
final securityConfig =
|
||||
json["security_config"] as Map<String, dynamic>? ?? {};
|
||||
config = {
|
||||
"type": "hysteria2",
|
||||
"tag": nodeListItem.name,
|
||||
"server": nodeListItem.serverAddr,
|
||||
"server_port": json["port"],
|
||||
"password": nodeListItem.uuid,
|
||||
"up_mbps": 100,
|
||||
"down_mbps": 100,
|
||||
"obfs": {
|
||||
"type": "salamander",
|
||||
"password": json["obfs_password"] ?? nodeListItem.uuid
|
||||
},
|
||||
"tls": {
|
||||
"enabled": true,
|
||||
"server_name": securityConfig["sni"] ?? "",
|
||||
"insecure": securityConfig["allow_insecure"] ?? true
|
||||
}
|
||||
};
|
||||
break;
|
||||
case "trojan":
|
||||
final securityConfig =
|
||||
json["security_config"] as Map<String, dynamic>? ?? {};
|
||||
|
||||
// 智能设置 server_name
|
||||
String serverName = securityConfig["sni"] ?? "";
|
||||
if (serverName.isEmpty) {
|
||||
// 如果没有配置 SNI,使用服务器地址
|
||||
serverName = nodeListItem.serverAddr;
|
||||
}
|
||||
|
||||
config = {
|
||||
"type": "trojan",
|
||||
"tag": nodeListItem.name,
|
||||
"server": nodeListItem.serverAddr,
|
||||
"server_port": json["port"],
|
||||
"password": nodeListItem.uuid,
|
||||
"tls": {
|
||||
"enabled": json["security"] == "tls",
|
||||
"server_name": serverName,
|
||||
"insecure": securityConfig["allow_insecure"] ?? true,
|
||||
"utls": {"enabled": true, "fingerprint": "chrome"}
|
||||
}
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
// 检查 relayNode 是否为 JSON 字符串并解析
|
||||
if (nodeListItem.relayNode.isNotEmpty && nodeListItem.relayMode != "none") {
|
||||
final relayNodeJson = jsonDecode(nodeListItem.relayNode);
|
||||
if (relayNodeJson is List && nodeListItem.relayMode != "none") {
|
||||
// 随机选择一个元素
|
||||
final randomNode = (relayNodeJson..shuffle()).first;
|
||||
config["server"] = randomNode["host"]; // 提取 host
|
||||
config["server_port"] = randomNode["port"]; // 提取 port
|
||||
// 检查 relayNode 是否为 JSON 字符串并解析
|
||||
if (nodeListItem.relayNode.isNotEmpty && nodeListItem.relayMode != "none") {
|
||||
final relayNodeJson = jsonDecode(nodeListItem.relayNode);
|
||||
if (relayNodeJson is List && nodeListItem.relayMode != "none") {
|
||||
// 随机选择一个元素
|
||||
final randomNode = (relayNodeJson..shuffle()).first;
|
||||
config["server"] = randomNode["host"]; // 提取 host
|
||||
config["server_port"] = randomNode["port"]; // 提取 port
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('⚠️ 解析 config 字段失败: $e');
|
||||
}
|
||||
config = {};
|
||||
}
|
||||
}
|
||||
// 解析配置
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'KROutboundItem(name: ${nodeListItem.name}, protocol: ${nodeListItem.protocol}, server: ${nodeListItem.serverAddr}, port: ${nodeListItem.port})';
|
||||
}
|
||||
|
||||
/// 构建传输配置
|
||||
@@ -249,31 +304,11 @@ class KROutboundItem {
|
||||
if (kDebugMode) {
|
||||
print('🔧 开始构建节点配置 - 协议: ${nodeListItem.protocol}, 名称: ${nodeListItem.name}');
|
||||
}
|
||||
if (kDebugMode) {
|
||||
print('📋 节点详细信息:');
|
||||
}
|
||||
if (kDebugMode) {
|
||||
print(' - serverAddr: ${nodeListItem.serverAddr}');
|
||||
}
|
||||
if (kDebugMode) {
|
||||
print(' - port: ${nodeListItem.port}');
|
||||
}
|
||||
if (kDebugMode) {
|
||||
print(' - uuid: ${nodeListItem.uuid}');
|
||||
}
|
||||
if (kDebugMode) {
|
||||
print(' - method: ${nodeListItem.method}');
|
||||
}
|
||||
if (kDebugMode) {
|
||||
print(' - config: ${nodeListItem.config}');
|
||||
}
|
||||
if (kDebugMode) {
|
||||
print(' - protocols: ${nodeListItem.protocols}');
|
||||
}
|
||||
|
||||
// 🔧 尝试从 config 字段解析 transport 配置
|
||||
Map<String, dynamic>? transportConfig;
|
||||
Map<String, dynamic>? securityConfig;
|
||||
int actualPort = nodeListItem.port; // 🔧 使用局部变量存储实际端口
|
||||
|
||||
// 🔧 关键修复:优先从 protocols 字段解析配置
|
||||
if (nodeListItem.protocols.isNotEmpty) {
|
||||
@@ -307,6 +342,28 @@ class KROutboundItem {
|
||||
}
|
||||
|
||||
if (matchedProtocol != null) {
|
||||
// 🔧 关键修复:只在顶层端口为0时,才使用 protocols 中的端口
|
||||
// 这样可以保留顶层的正确端口(如 53441),不被 protocols 数组中的端口(如 287)覆盖
|
||||
if (actualPort == 0 && matchedProtocol['port'] != null) {
|
||||
// 安全解析端口号
|
||||
int protocolPort = 0;
|
||||
final portValue = matchedProtocol['port'];
|
||||
if (portValue is int) {
|
||||
protocolPort = portValue;
|
||||
} else if (portValue is String) {
|
||||
protocolPort = int.tryParse(portValue) ?? 0;
|
||||
}
|
||||
|
||||
if (protocolPort > 0) {
|
||||
actualPort = protocolPort;
|
||||
if (kDebugMode) {
|
||||
print(' ✅ 从 protocols 使用端口: $protocolPort (顶层端口为0)');
|
||||
}
|
||||
}
|
||||
} else if (kDebugMode && matchedProtocol['port'] != null) {
|
||||
print(' ✅ 保留顶层端口: $actualPort (protocols中的端口: ${matchedProtocol['port']})');
|
||||
}
|
||||
|
||||
// 提取 transport 配置
|
||||
if (matchedProtocol['network'] != null || matchedProtocol['transport'] != null) {
|
||||
final network = matchedProtocol['network'] ?? matchedProtocol['transport'];
|
||||
@@ -402,7 +459,7 @@ class KROutboundItem {
|
||||
|
||||
switch (nodeListItem.protocol) {
|
||||
case "shadowsocks":
|
||||
// 优先使用 protocols 解析出来的 cipher,其次是 method 字段,最后才是默认值
|
||||
// 优先使用 protocols 解析出来的 cipher,其次是 method 字段,最后才是默认值
|
||||
String finalMethod = nodeListItem.method.isNotEmpty
|
||||
? nodeListItem.method
|
||||
: "2022-blake3-aes-256-gcm";
|
||||
@@ -411,22 +468,21 @@ class KROutboundItem {
|
||||
"type": "shadowsocks",
|
||||
"tag": nodeListItem.name,
|
||||
"server": nodeListItem.serverAddr,
|
||||
"server_port": nodeListItem.port,
|
||||
"server_port": actualPort,
|
||||
"method": finalMethod,
|
||||
"password": nodeListItem.uuid
|
||||
};
|
||||
if (kDebugMode) {
|
||||
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
print('✅ Shadowsocks 节点配置构建成功: ${nodeListItem.name}');
|
||||
}
|
||||
if (kDebugMode) {
|
||||
print('📄 使用加密方法: $finalMethod');
|
||||
}
|
||||
if (kDebugMode) {
|
||||
print('📄 完整配置: $config');
|
||||
print('📄 完整配置 JSON:');
|
||||
print(jsonEncode(config));
|
||||
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
}
|
||||
break;
|
||||
case "vless":
|
||||
// 判断是否为域名(非IP地址)
|
||||
// 判断是否为域名(非IP地址)
|
||||
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||
.hasMatch(nodeListItem.serverAddr);
|
||||
|
||||
@@ -436,17 +492,25 @@ class KROutboundItem {
|
||||
serverName = securityConfig['sni'].toString();
|
||||
}
|
||||
|
||||
// 🔧 关键修复:根据 security_config 判断是否启用 TLS
|
||||
final bool vlessTlsEnabled = securityConfig?['tls_enabled'] ?? false;
|
||||
// 🔧 关键修复:智能判断是否启用 TLS
|
||||
// 1. 优先使用 securityConfig['tls_enabled']
|
||||
// 2. 如果没有明确配置,根据端口和域名智能判断
|
||||
bool vlessTlsEnabled = securityConfig?['tls_enabled'] ?? true; // 默认启用 TLS
|
||||
|
||||
// 如果端口是标准非TLS端口(80, 8080等),则禁用 TLS
|
||||
if (nodeListItem.port == 80 || nodeListItem.port == 8080) {
|
||||
vlessTlsEnabled = false;
|
||||
}
|
||||
|
||||
if (kDebugMode) {
|
||||
print('🔐 VLESS TLS 状态: enabled=$vlessTlsEnabled');
|
||||
print('🔐 VLESS TLS 状态: enabled=$vlessTlsEnabled (port=${nodeListItem.port}, isDomain=$isDomain)');
|
||||
}
|
||||
|
||||
config = {
|
||||
"type": "vless",
|
||||
"tag": nodeListItem.name,
|
||||
"server": nodeListItem.serverAddr,
|
||||
"server_port": nodeListItem.port,
|
||||
"server_port": actualPort,
|
||||
"uuid": nodeListItem.uuid,
|
||||
if (transportConfig != null) "transport": transportConfig,
|
||||
if (vlessTlsEnabled) "tls": {
|
||||
@@ -460,14 +524,27 @@ class KROutboundItem {
|
||||
}
|
||||
};
|
||||
if (kDebugMode) {
|
||||
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
print('✅ VLESS 节点配置构建成功: ${nodeListItem.name}');
|
||||
}
|
||||
if (kDebugMode) {
|
||||
print('📄 完整配置: $config');
|
||||
print('📋 原始节点信息:');
|
||||
print(' - serverAddr: ${nodeListItem.serverAddr}');
|
||||
print(' - port: ${nodeListItem.port}');
|
||||
print(' - uuid: ${nodeListItem.uuid}');
|
||||
print(' - protocols: ${nodeListItem.protocols}');
|
||||
print('🔐 安全配置:');
|
||||
print(' - TLS 启用: $vlessTlsEnabled');
|
||||
print(' - 是域名: $isDomain');
|
||||
print(' - server_name: $serverName');
|
||||
print(' - securityConfig: $securityConfig');
|
||||
print('📡 传输配置:');
|
||||
print(' - transportConfig: $transportConfig');
|
||||
print('📄 最终生成的完整配置 JSON:');
|
||||
print(jsonEncode(config));
|
||||
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
}
|
||||
break;
|
||||
case "vmess":
|
||||
// 判断是否为域名(非IP地址)
|
||||
// 判断是否为域名(非IP地址)
|
||||
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||
.hasMatch(nodeListItem.serverAddr);
|
||||
|
||||
@@ -477,17 +554,25 @@ class KROutboundItem {
|
||||
serverName = securityConfig['sni'].toString();
|
||||
}
|
||||
|
||||
// 🔧 关键修复:根据 security_config 判断是否启用 TLS
|
||||
final bool tlsEnabled = securityConfig?['tls_enabled'] ?? false;
|
||||
// 🔧 关键修复:智能判断是否启用 TLS
|
||||
// 1. 优先使用 securityConfig['tls_enabled']
|
||||
// 2. 如果没有明确配置,根据端口和域名智能判断
|
||||
bool tlsEnabled = securityConfig?['tls_enabled'] ?? true; // 默认启用 TLS
|
||||
|
||||
// 如果端口是标准非TLS端口(80, 8080等),则禁用 TLS
|
||||
if (nodeListItem.port == 80 || nodeListItem.port == 8080) {
|
||||
tlsEnabled = false;
|
||||
}
|
||||
|
||||
if (kDebugMode) {
|
||||
print('🔐 TLS 状态: enabled=$tlsEnabled');
|
||||
print('🔐 VMess TLS 状态: enabled=$tlsEnabled (port=${nodeListItem.port}, isDomain=$isDomain)');
|
||||
}
|
||||
|
||||
config = {
|
||||
"type": "vmess",
|
||||
"tag": nodeListItem.name,
|
||||
"server": nodeListItem.serverAddr,
|
||||
"server_port": nodeListItem.port,
|
||||
"server_port": actualPort,
|
||||
"uuid": nodeListItem.uuid,
|
||||
"alter_id": 0,
|
||||
"security": "auto",
|
||||
@@ -503,14 +588,27 @@ class KROutboundItem {
|
||||
}
|
||||
};
|
||||
if (kDebugMode) {
|
||||
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
print('✅ VMess 节点配置构建成功: ${nodeListItem.name}');
|
||||
}
|
||||
if (kDebugMode) {
|
||||
print('📄 完整配置: $config');
|
||||
print('📋 原始节点信息:');
|
||||
print(' - serverAddr: ${nodeListItem.serverAddr}');
|
||||
print(' - port: ${nodeListItem.port}');
|
||||
print(' - uuid: ${nodeListItem.uuid}');
|
||||
print(' - protocols: ${nodeListItem.protocols}');
|
||||
print('🔐 安全配置:');
|
||||
print(' - TLS 启用: $tlsEnabled');
|
||||
print(' - 是域名: $isDomain');
|
||||
print(' - server_name: $serverName');
|
||||
print(' - securityConfig: $securityConfig');
|
||||
print('📡 传输配置:');
|
||||
print(' - transportConfig: $transportConfig');
|
||||
print('📄 最终生成的完整配置 JSON:');
|
||||
print(jsonEncode(config));
|
||||
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
}
|
||||
break;
|
||||
case "trojan":
|
||||
// 判断是否为域名(非IP地址)
|
||||
// 判断是否为域名(非IP地址)
|
||||
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||
.hasMatch(nodeListItem.serverAddr);
|
||||
|
||||
@@ -524,7 +622,7 @@ class KROutboundItem {
|
||||
"type": "trojan",
|
||||
"tag": nodeListItem.name,
|
||||
"server": nodeListItem.serverAddr,
|
||||
"server_port": nodeListItem.port,
|
||||
"server_port": actualPort,
|
||||
"password": nodeListItem.uuid,
|
||||
if (transportConfig != null) "transport": transportConfig,
|
||||
"tls": {
|
||||
@@ -546,7 +644,7 @@ class KROutboundItem {
|
||||
break;
|
||||
case "hysteria":
|
||||
case "hysteria2":
|
||||
// 后端的 "hysteria" 实际上是 Hysteria2 协议
|
||||
// 后端的 "hysteria" 实际上是 Hysteria2 协议
|
||||
if (kDebugMode) {
|
||||
print('🔍 构建 Hysteria2 节点: ${nodeListItem.name}');
|
||||
}
|
||||
|
||||
@@ -88,6 +88,9 @@ class KrOutboundsList {
|
||||
}
|
||||
}
|
||||
|
||||
// 生成国家自动选择虚拟节点
|
||||
_generateCountryAutoNodes(countryGroups);
|
||||
|
||||
// 将标签分组转换为 KRGroupOutboundList 并添加到 groupOutboundList
|
||||
for (var tag in tagGroups.keys) {
|
||||
final item = KRGroupOutboundList(
|
||||
@@ -108,7 +111,40 @@ class KrOutboundsList {
|
||||
country: country,
|
||||
outboundList: countryGroups[country]!)); // 添加国家分组到列表
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成国家自动选择虚拟节点
|
||||
void _generateCountryAutoNodes(Map<String, List<KROutboundItem>> countryGroups) {
|
||||
for (var entry in countryGroups.entries) {
|
||||
final country = entry.key;
|
||||
final nodes = entry.value;
|
||||
final autoTag = '${country}-auto';
|
||||
|
||||
if (kDebugMode) {
|
||||
print('🤖 生成国家自动选择节点: $autoTag, 包含 ${nodes.length} 个节点');
|
||||
}
|
||||
|
||||
// 构建 urltest 配置
|
||||
final urltestConfig = {
|
||||
'type': 'urltest',
|
||||
'tag': autoTag,
|
||||
'outbounds': nodes.map((node) => node.tag).toList(),
|
||||
'url': 'https://www.google.com/generate_204',
|
||||
'interval': '10m',
|
||||
'tolerance': 50,
|
||||
};
|
||||
|
||||
// 创建虚拟节点
|
||||
final virtualNode = KROutboundItem.fromVirtual(autoTag, country, urltestConfig);
|
||||
|
||||
// 添加到各个列表
|
||||
allList.add(virtualNode);
|
||||
keyList[autoTag] = virtualNode;
|
||||
configJsonList.add(urltestConfig);
|
||||
|
||||
if (kDebugMode) {
|
||||
print('✅ 生成虚拟节点: $autoTag, 配置: ${urltestConfig.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'dart:convert';
|
||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||
|
||||
class KRNodeList {
|
||||
final List<KrNodeListItem> list;
|
||||
final List<KrNodeListItem> list;
|
||||
final String subscribeId;
|
||||
final String startTime;
|
||||
final String expireTime;
|
||||
@@ -35,8 +35,8 @@ class KRNodeList {
|
||||
// 查找 id 匹配的订阅项
|
||||
try {
|
||||
subscribeData = listData.firstWhere(
|
||||
(item) => (item as Map<String, dynamic>)['id']?.toString() == requestSubscribeId,
|
||||
orElse: () => listData[0] as Map<String, dynamic>
|
||||
(item) => (item as Map<String, dynamic>)['id']?.toString() == requestSubscribeId,
|
||||
orElse: () => listData[0] as Map<String, dynamic>
|
||||
) as Map<String, dynamic>;
|
||||
KRLogUtil.kr_i('✅ 找到匹配的订阅项: id=$requestSubscribeId', tag: 'NodeList');
|
||||
} catch (e) {
|
||||
@@ -70,7 +70,7 @@ class KRNodeList {
|
||||
|
||||
class KrNodeListItem {
|
||||
final int id;
|
||||
String name;
|
||||
String name;
|
||||
final String uuid;
|
||||
final String protocol;
|
||||
final String relayMode;
|
||||
@@ -136,25 +136,68 @@ class KrNodeListItem {
|
||||
String method = json['method']?.toString() ?? ''; // 加密方法(Shadowsocks等)
|
||||
final protocols = json['protocols']?.toString() ?? ''; // 协议配置JSON
|
||||
|
||||
// 🔧 如果有 protocols 字段,从中解析 port 和 cipher
|
||||
// 🔧 打印原始节点 JSON(用于调试)
|
||||
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'NodeList');
|
||||
KRLogUtil.kr_i('📥 收到节点 API 原始数据:', tag: 'NodeList');
|
||||
KRLogUtil.kr_i('节点名称: ${json['name']}', tag: 'NodeList');
|
||||
KRLogUtil.kr_i('协议类型: ${json['protocol']}', tag: 'NodeList');
|
||||
KRLogUtil.kr_i('完整 JSON:', tag: 'NodeList');
|
||||
KRLogUtil.kr_i(jsonEncode(json), tag: 'NodeList');
|
||||
|
||||
// 🔧 如果有 protocols 字段,从中解析 cipher(但不覆盖顶层的 port)
|
||||
if (protocols.isNotEmpty) {
|
||||
try {
|
||||
final protocolsList = jsonDecode(protocols) as List;
|
||||
final currentProtocol = json['protocol']?.toString().toLowerCase() ?? '';
|
||||
|
||||
KRLogUtil.kr_i('📋 protocols 字段内容 (${protocolsList.length} 个协议):', tag: 'NodeList');
|
||||
for (var i = 0; i < protocolsList.length; i++) {
|
||||
KRLogUtil.kr_i(' 协议 $i: ${jsonEncode(protocolsList[i])}', tag: 'NodeList');
|
||||
}
|
||||
|
||||
if (protocolsList.isNotEmpty) {
|
||||
final firstProtocol = protocolsList[0] as Map<String, dynamic>;
|
||||
// 优先使用 protocols 中的配置
|
||||
if (firstProtocol['port'] != null) {
|
||||
port = _parseIntSafely(firstProtocol['port']);
|
||||
// 🔧 修复:查找与当前协议类型匹配的配置,而不是直接使用第一个
|
||||
Map<String, dynamic>? matchedProtocolConfig;
|
||||
|
||||
// 尝试找到协议类型匹配的配置
|
||||
for (var protocolConfig in protocolsList) {
|
||||
final configMap = protocolConfig as Map<String, dynamic>;
|
||||
// 🔧 修复:API 使用的字段名是 'type',不是 'protocol'
|
||||
final protocolType = (configMap['type'] ?? configMap['protocol'])?.toString().toLowerCase() ?? '';
|
||||
|
||||
// 检查是否匹配(支持 shadowsocks/ss, vmess, vless, trojan 等)
|
||||
if (protocolType == currentProtocol ||
|
||||
(currentProtocol == 'shadowsocks' && protocolType == 'ss') ||
|
||||
(currentProtocol == 'ss' && protocolType == 'shadowsocks')) {
|
||||
matchedProtocolConfig = configMap;
|
||||
KRLogUtil.kr_i('🎯 找到匹配的协议配置: $protocolType', tag: 'NodeList');
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (firstProtocol['cipher'] != null && firstProtocol['cipher'].toString().isNotEmpty) {
|
||||
method = firstProtocol['cipher'].toString();
|
||||
|
||||
// 如果没找到匹配的,使用第一个配置(兼容旧API)
|
||||
final targetProtocol = matchedProtocolConfig ?? (protocolsList[0] as Map<String, dynamic>);
|
||||
|
||||
// 🔧 关键修复:只在顶层没有 port 字段时,才使用 protocols 中的端口
|
||||
// 这样可以保留顶层的正确端口(如 53441),不被 protocols 数组中的端口(如 287)覆盖
|
||||
if (port == 0 && targetProtocol['port'] != null) {
|
||||
port = _parseIntSafely(targetProtocol['port']);
|
||||
KRLogUtil.kr_i('✅ 从 protocols 解析端口: $port', tag: 'NodeList');
|
||||
} else {
|
||||
KRLogUtil.kr_i('✅ 保留顶层端口: $port (protocols中的端口: ${targetProtocol['port']})', tag: 'NodeList');
|
||||
}
|
||||
|
||||
// 提取 cipher(加密方法)
|
||||
if (targetProtocol['cipher'] != null && targetProtocol['cipher'].toString().isNotEmpty) {
|
||||
method = targetProtocol['cipher'].toString();
|
||||
KRLogUtil.kr_i('✅ 从 protocols 解析 cipher: $method', tag: 'NodeList');
|
||||
}
|
||||
KRLogUtil.kr_i('从 protocols 解析: port=$port, cipher=$method', tag: 'NodeList');
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('解析 protocols 字段失败: $e', tag: 'NodeList');
|
||||
KRLogUtil.kr_w('⚠️ 解析 protocols 字段失败: $e', tag: 'NodeList');
|
||||
}
|
||||
}
|
||||
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'NodeList');
|
||||
|
||||
return KrNodeListItem(
|
||||
id: _parseIntSafely(json['id']),
|
||||
|
||||
@@ -12,6 +12,15 @@ class HINodeListController extends GetxController {
|
||||
/// 首页服务
|
||||
final KRHomeController homeController = Get.find<KRHomeController>();
|
||||
|
||||
/// 调试模式状态
|
||||
final RxBool isDebugMode = false.obs;
|
||||
|
||||
/// 模式按钮点击计数器
|
||||
int modeButtonClickCount = 0;
|
||||
|
||||
/// 最后一次点击时间
|
||||
DateTime? lastModeButtonClickTime;
|
||||
|
||||
/// 获取连接类型字符串
|
||||
String kr_getConnectionTypeString() {
|
||||
final connectionType = KRSingBoxImp.instance.kr_connectionType.value;
|
||||
@@ -41,4 +50,48 @@ class HINodeListController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理模式按钮点击(用于激活调试模式)
|
||||
void kr_handleModeButtonClick() {
|
||||
final now = DateTime.now();
|
||||
|
||||
// 检查是否在2秒内连续点击
|
||||
if (lastModeButtonClickTime != null &&
|
||||
now.difference(lastModeButtonClickTime!).inSeconds > 2) {
|
||||
// 超过2秒,重置计数器
|
||||
modeButtonClickCount = 0;
|
||||
}
|
||||
|
||||
modeButtonClickCount++;
|
||||
lastModeButtonClickTime = now;
|
||||
|
||||
KRLogUtil.kr_i('模式按钮点击次数: $modeButtonClickCount', tag: 'HINodeListController');
|
||||
|
||||
if (modeButtonClickCount >= 5) {
|
||||
// 激活调试模式
|
||||
isDebugMode.value = true;
|
||||
modeButtonClickCount = 0; // 重置计数器
|
||||
KRLogUtil.kr_i('🐛 调试模式已激活!', tag: 'HINodeListController');
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取要显示的节点列表(根据调试模式过滤)
|
||||
List<dynamic> kr_getFilteredNodeList() {
|
||||
if (isDebugMode.value) {
|
||||
// 调试模式:显示所有节点
|
||||
return kr_subscribeService.allList;
|
||||
} else {
|
||||
// 正常模式:只显示 country-auto 节点
|
||||
return kr_subscribeService.allList.where((node) => node.tag.endsWith('-auto')).toList();
|
||||
}
|
||||
}
|
||||
|
||||
/// 重置调试模式
|
||||
void kr_resetDebugMode() {
|
||||
isDebugMode.value = false;
|
||||
modeButtonClickCount = 0;
|
||||
lastModeButtonClickTime = null;
|
||||
KRLogUtil.kr_i('调试模式已重置', tag: 'HINodeListController');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// hi_node_list_view.dart
|
||||
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
@@ -48,17 +47,9 @@ class HINodeListView extends GetView<HINodeListController> {
|
||||
) {
|
||||
if (outboundList.isEmpty) return 0;
|
||||
|
||||
// 过滤掉不可用节点(延迟标记为 65535 或负数)
|
||||
final validNodes = outboundList.where((node) {
|
||||
final delay = node.urlTestDelay.value;
|
||||
return delay > 0 && delay < 65535;
|
||||
}).toList();
|
||||
|
||||
if (validNodes.isEmpty) return 0;
|
||||
|
||||
// 返回最小延迟值
|
||||
validNodes.sort((a, b) => a.urlTestDelay.value.compareTo(b.urlTestDelay.value));
|
||||
return validNodes.first.urlTestDelay.value;
|
||||
outboundList.sort((a, b) => a.urlTestDelay.value.compareTo(b.urlTestDelay.value));
|
||||
return outboundList.first.urlTestDelay.value;
|
||||
}
|
||||
|
||||
/// 找出延迟最低(最快)的节点对象
|
||||
@@ -88,8 +79,8 @@ class HINodeListView extends GetView<HINodeListController> {
|
||||
// 并设置透明背景,让父组件的背景可以透出来
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: _buildSubscribeList(context)
|
||||
// child: _kr_buildRegionList(context)
|
||||
child: _buildSubscribeList(context)
|
||||
// child: _kr_buildRegionList(context)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -199,9 +190,20 @@ class HINodeListView extends GetView<HINodeListController> {
|
||||
),
|
||||
...controller.kr_subscribeService.countryOutboundList.map((country) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
// 自动选择这个国家下的节点延迟中最快的
|
||||
controller.homeController.onCountrySelected(country.country);
|
||||
onTap: () async {
|
||||
try {
|
||||
|
||||
final success =
|
||||
await controller.homeController.kr_performNodeSwitch('${country.country}-auto');
|
||||
print('node 点击 ${country.country} 节点数量${country.outboundList.length} 节点详情 ${country.outboundList}');
|
||||
if (success) {
|
||||
controller.homeController.kr_currentListStatus.value =
|
||||
KRHomeViewsListStatus.kr_none;
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('Auto选项切换异常: $e',
|
||||
tag: 'NodeListView');
|
||||
}
|
||||
},
|
||||
child: _kr_buildCountryListItem(context, country: country),
|
||||
);
|
||||
@@ -229,14 +231,24 @@ class HINodeListView extends GetView<HINodeListController> {
|
||||
padding: EdgeInsets.symmetric(vertical: 8.w),
|
||||
// 2. 使用 children 属性,并一次性构建所有列表项
|
||||
children: [
|
||||
if (controller.kr_subscribeService.allList.isEmpty)
|
||||
_buildEmptyListPlaceholder(context, AppTranslations.kr_home.noNodes)
|
||||
if (controller.kr_getFilteredNodeList().isEmpty)
|
||||
_buildEmptyListPlaceholder(context,
|
||||
controller.isDebugMode.value ? '调试模式:无节点数据' : AppTranslations.kr_home.noNodes)
|
||||
else ...[
|
||||
InkWell(
|
||||
onTap: () {
|
||||
controller.homeController.kr_selectNode('auto');
|
||||
controller.homeController.kr_currentListStatus.value =
|
||||
KRHomeViewsListStatus.kr_none;
|
||||
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
|
||||
onTap: () async {
|
||||
try {
|
||||
final success =
|
||||
await controller.homeController.kr_performNodeSwitch('auto');
|
||||
if (success) {
|
||||
controller.homeController.kr_currentListStatus.value =
|
||||
KRHomeViewsListStatus.kr_none;
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('Auto选项切换异常: $e',
|
||||
tag: 'NodeListView');
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
@@ -281,13 +293,28 @@ class HINodeListView extends GetView<HINodeListController> {
|
||||
),
|
||||
),
|
||||
// 2. 第二个 Text: "根据网络IP自动匹配最快线路"
|
||||
Text(
|
||||
'根据网络IP自动匹配最快线路', // 您指定的文本
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Obx(() {
|
||||
// 当选择全局 auto 时,显示当前选中的节点信息
|
||||
if (controller.homeController.kr_cutTag.value == 'auto') {
|
||||
final autoNodeInfo = controller.homeController.kr_getGlobalAutoSelectedNode();
|
||||
if (autoNodeInfo != null) {
|
||||
return Text(
|
||||
'当前: ${autoNodeInfo['tag']} (${autoNodeInfo['delay']}ms)',
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return Text(
|
||||
'根据网络IP自动匹配最快线路', // 默认文本
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -308,9 +335,25 @@ class HINodeListView extends GetView<HINodeListController> {
|
||||
),
|
||||
),
|
||||
),
|
||||
...controller.kr_subscribeService.allList.map((item) {
|
||||
...controller.kr_getFilteredNodeList().map((item) {
|
||||
return InkWell(
|
||||
onTap: () => _onNodeSelected(item),
|
||||
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
|
||||
onTap: () async {
|
||||
try {
|
||||
KRLogUtil.kr_i(
|
||||
'🔄 用户点击节点: ${item.tag}');
|
||||
final success = await controller.homeController
|
||||
.kr_performNodeSwitch(item.tag);
|
||||
if (success) {
|
||||
controller.homeController.kr_currentListStatus.value =
|
||||
KRHomeViewsListStatus.kr_none;
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e(
|
||||
'节点切换异常: $e',
|
||||
tag: 'NodeListView');
|
||||
}
|
||||
},
|
||||
child: _kr_buildNodeListItem(context, item: item),
|
||||
);
|
||||
}).toList(),
|
||||
@@ -344,15 +387,6 @@ class HINodeListView extends GetView<HINodeListController> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 节点选中时的通用处理逻辑
|
||||
void _onNodeSelected(KROutboundItem item) {
|
||||
KRLogUtil.kr_i('Node selected: ${item.tag}');
|
||||
KRSingBoxImp.instance.kr_selectOutbound(item.tag);
|
||||
controller.homeController.kr_selectNode(item.tag);
|
||||
// 切换回主页的仪表盘视图
|
||||
controller.homeController.kr_currentListStatus.value = KRHomeViewsListStatus.kr_none;
|
||||
}
|
||||
|
||||
/// 构建单个节点列表项的UI
|
||||
Widget _kr_buildNodeListItem(BuildContext context, {required KROutboundItem item}) {
|
||||
return Container(
|
||||
@@ -372,55 +406,75 @@ class HINodeListView extends GetView<HINodeListController> {
|
||||
KRCountryFlag(countryCode: item.country, width: 30.w, height: 20.w, isCircle: false, maintainSize: false),
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
controller.homeController.kr_getCountryFullName(item.country),
|
||||
style: KrAppTextStyle(fontSize: 14, color: Colors.white),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
Obx(() => controller.homeController.kr_cutTag.value == item.tag
|
||||
? Container(
|
||||
margin: EdgeInsets.only(left: 4.w),
|
||||
padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.w),
|
||||
decoration: BoxDecoration(
|
||||
color: krModernGreenLight.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4.w),
|
||||
),
|
||||
child: Text(
|
||||
AppTranslations.kr_home.selected,
|
||||
style: KrAppTextStyle(fontSize: 10, color: krModernGreen, fontWeight: FontWeight.w500),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink()),
|
||||
],
|
||||
child: Obx(() {
|
||||
final isDebug = controller.isDebugMode.value;
|
||||
|
||||
final text = isDebug
|
||||
? '${controller.homeController.kr_getCountryFullName(item.country)} - ${item.tag}'
|
||||
: '${controller.homeController.kr_getCountryFullName(item.country)}';
|
||||
|
||||
return Text(
|
||||
text,
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
SizedBox(height: 2.w),
|
||||
Obx(() {
|
||||
// 2. 获取用于显示的延迟值
|
||||
final int delay = _getDisplayDelay(controller, item);
|
||||
return Text(
|
||||
'${delay}ms',
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 10,
|
||||
color: krModernGreen,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
);
|
||||
}),
|
||||
Text(
|
||||
item.city,
|
||||
style: KrAppTextStyle(fontSize: 12, color: Theme.of(context).textTheme.bodySmall?.color),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
Obx(() {
|
||||
// 1. 获取延迟值和测速状态
|
||||
int displayDelay;
|
||||
bool isTesting = controller.homeController.kr_isLatency.value;
|
||||
|
||||
// 极简逻辑:只根据节点类型决定显示方式
|
||||
print('🔄 _kr_buildNodeListItem节点: item.tag=${item.tag}, country=${item.country}');
|
||||
|
||||
// 如果节点本身是auto组节点,显示组内最快节点的速度
|
||||
if (item.tag.endsWith('-auto')) {
|
||||
print('🎯 检测到auto组节点,获取最快节点信息');
|
||||
final autoNodeInfo = controller.homeController.kr_getCountryAutoSelectedNode(item.country);
|
||||
print('📊 _kr_buildNodeListItem获取auto最快节点: $autoNodeInfo');
|
||||
displayDelay = autoNodeInfo?['delay'] ?? 0;
|
||||
print('✅ _kr_buildNodeListItem使用auto最快节点延迟: $displayDelay');
|
||||
} else {
|
||||
// 普通节点,直接显示节点自身的速度
|
||||
displayDelay = item.urlTestDelay.value;
|
||||
print('🔄 _kr_buildNodeListItem使用普通节点延迟: $displayDelay');
|
||||
}
|
||||
|
||||
// 2. 声明文本和颜色变量
|
||||
String delayText;
|
||||
Color delayColor;
|
||||
|
||||
// 3. 根据状态设置文本和颜色
|
||||
if (isTesting && displayDelay == 0) {
|
||||
delayText = '测速中...';
|
||||
delayColor = Colors.grey; // 测速时使用灰色
|
||||
} else if (displayDelay == 0) {
|
||||
delayText = '- ms'; // 未测速或初始状态
|
||||
delayColor = Colors.grey;
|
||||
} else if (displayDelay >= 3000) {
|
||||
delayText = AppTranslations.kr_home.timeout; // "超时"
|
||||
delayColor = Colors.red; // 超时状态使用红色
|
||||
} else {
|
||||
delayText = '${displayDelay}ms';
|
||||
// 正常延迟,根据快慢设置不同颜色
|
||||
delayColor = (displayDelay < 500) ? krModernGreen : Colors.orange;
|
||||
}
|
||||
|
||||
// 4. 返回最终的 Text 组件
|
||||
return Text(
|
||||
delayText,
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 10,
|
||||
color: delayColor, // 使用动态计算出的颜色
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(width: 12.w),
|
||||
Obx(() => controller.homeController.kr_cutTag.value == item.tag
|
||||
? KrLocalImage(
|
||||
imageName: 'radio-active-icon',
|
||||
@@ -441,18 +495,7 @@ class HINodeListView extends GetView<HINodeListController> {
|
||||
|
||||
/// 构建国家列表项的UI
|
||||
Widget _kr_buildCountryListItem(BuildContext context, {required country}) {
|
||||
// 获取延迟颜色
|
||||
Color getLatencyColor(int delay) {
|
||||
if (delay == 0) {
|
||||
return Colors.transparent;
|
||||
} else if (delay < 500) {
|
||||
return krModernGreen;
|
||||
} else if (delay < 3000) {
|
||||
return Color(0xFFFFB700); // 使用更容易看清的黄色
|
||||
} else {
|
||||
return Colors.red;
|
||||
}
|
||||
}
|
||||
|
||||
return Container(
|
||||
key: ValueKey(country),
|
||||
decoration: BoxDecoration(
|
||||
@@ -476,22 +519,42 @@ class HINodeListView extends GetView<HINodeListController> {
|
||||
),
|
||||
),
|
||||
Obx(() {
|
||||
final int delay = getFastestNodeDelay(controller, country.outboundList);
|
||||
// 1. 获取延迟值和测速状态
|
||||
int displayDelay = getFastestNodeDelay(controller, country.outboundList);
|
||||
bool isTesting = controller.homeController.kr_isLatency.value;
|
||||
|
||||
// 2. 声明文本和颜色变量
|
||||
String delayText;
|
||||
Color delayColor;
|
||||
|
||||
// 3. 根据状态设置文本和颜色
|
||||
if (isTesting && displayDelay == 0) {
|
||||
delayText = '测速中...';
|
||||
delayColor = Colors.grey; // 测速时使用灰色
|
||||
} else if (displayDelay == 0) {
|
||||
delayText = '- ms'; // 未测速或初始状态
|
||||
delayColor = Colors.grey;
|
||||
} else if (displayDelay >= 3000) {
|
||||
delayText = AppTranslations.kr_home.timeout; // "超时"
|
||||
delayColor = Colors.red; // 超时状态使用红色
|
||||
} else {
|
||||
delayText = '${displayDelay}ms';
|
||||
// 正常延迟,根据快慢设置不同颜色
|
||||
delayColor = (displayDelay < 500) ? krModernGreen : Colors.orange;
|
||||
}
|
||||
|
||||
// 4. 返回最终的 Text 组件
|
||||
return Text(
|
||||
delay == 0
|
||||
? ''
|
||||
: delay >= 3000
|
||||
? AppTranslations.kr_home.timeout
|
||||
: '${delay}ms',
|
||||
delayText,
|
||||
style: KrAppTextStyle(
|
||||
fontSize: 10,
|
||||
color: getLatencyColor(delay),
|
||||
color: delayColor, // 使用动态计算出的颜色
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(width: 12.w),
|
||||
Obx(() => controller.homeController.kr_coutryText.value == country.country
|
||||
Obx(() => controller.homeController.kr_cutTag.value == '${country.country}-auto'
|
||||
? KrLocalImage(
|
||||
imageName: 'radio-active-icon',
|
||||
imageType: ImageType.svg,
|
||||
|
||||
@@ -23,7 +23,49 @@ class HINodePageView extends GetView<HINodeListController> {
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 模式切换器
|
||||
Positioned(
|
||||
left: 0, // ⭐ 所有内容整体贴到左边
|
||||
child: Obx(() => controller.isDebugMode.value
|
||||
? GestureDetector(
|
||||
onTap: () {
|
||||
controller.kr_resetDebugMode();
|
||||
Get.snackbar(
|
||||
'调试模式',
|
||||
'已关闭调试模式',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withOpacity(0.8),
|
||||
borderRadius: BorderRadius.circular(12.w),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.bug_report,
|
||||
size: 12.w,
|
||||
color: Colors.white,
|
||||
),
|
||||
SizedBox(width: 3.w),
|
||||
Text(
|
||||
'关闭调试',
|
||||
style: TextStyle(
|
||||
fontSize: 10.sp,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink()),
|
||||
),
|
||||
// 模式切换器
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 100.w, right: 60.w),
|
||||
child: Row(
|
||||
@@ -159,7 +201,12 @@ class HINodePageView extends GetView<HINodeListController> {
|
||||
}) {
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
onTap: () {
|
||||
// 处理模式按钮点击(用于调试模式激活)
|
||||
controller.kr_handleModeButtonClick();
|
||||
// 执行原有的点击逻辑
|
||||
onTap();
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: EdgeInsets.symmetric(vertical: 4.w),
|
||||
|
||||
@@ -69,8 +69,12 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
||||
children: [
|
||||
Obx(() {
|
||||
final account = KRAppRunData.getInstance().kr_account.value;
|
||||
final isDeviceLogin = account != null && account.startsWith('9000');
|
||||
|
||||
if (isDeviceLogin) return const SizedBox();
|
||||
|
||||
return Text(
|
||||
(account != null && account.isNotEmpty) ? account : '未绑定',
|
||||
account ?? '',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20.sp,
|
||||
@@ -433,7 +437,7 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
'ID: ${identifier.substring(0, identifier.length > 4 ? 4 : identifier.length)}$id',
|
||||
'SN: ${identifier.substring(0, identifier.length > 4 ? 4 : identifier.length)}$id',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 10.sp,
|
||||
|
||||
@@ -18,6 +18,7 @@ import '../../../common/app_config.dart';
|
||||
import '../../../localization/app_translations.dart';
|
||||
import '../../../localization/kr_language_utils.dart';
|
||||
import '../../../model/business/kr_group_outbound_list.dart';
|
||||
import '../../../model/business/kr_outbound_item.dart';
|
||||
import '../../../services/kr_announcement_service.dart';
|
||||
import '../../../utils/kr_event_bus.dart';
|
||||
import '../../../utils/kr_update_util.dart';
|
||||
@@ -133,11 +134,21 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
if (value != null) {
|
||||
isQuickConnectEnabled.value = value;
|
||||
// 保存闪连状态到本地存储
|
||||
await _storage.kr_saveBool(key: _quickConnectKey, value: value);
|
||||
await _saveQuickConnectStatus(value);
|
||||
KRLogUtil.kr_i('闪连状态已更新: $value', tag: 'QuickConnect');
|
||||
}
|
||||
}
|
||||
|
||||
// 保存闪连状态到本地存储
|
||||
Future<void> _saveQuickConnectStatus(bool enabled) async {
|
||||
try {
|
||||
await _storage.kr_saveBool(key: _quickConnectKey, value: enabled);
|
||||
KRLogUtil.kr_i('闪连状态已保存到本地存储: $enabled', tag: 'QuickConnect');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('保存闪连状态失败: $e', tag: 'QuickConnect');
|
||||
}
|
||||
}
|
||||
|
||||
// 从本地存储加载闪连状态
|
||||
Future<void> _loadQuickConnectStatus() async {
|
||||
try {
|
||||
@@ -839,6 +850,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
if (kDebugMode) {
|
||||
}
|
||||
await KRSingBoxImp.instance.kr_start();
|
||||
|
||||
KRLogUtil.kr_i('✅ 连接命令已发送', tag: 'HomeController');
|
||||
if (kDebugMode) {
|
||||
}
|
||||
@@ -1265,10 +1277,6 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onCountrySelected(String countryTag) async {
|
||||
await KRSingBoxImp.instance.kr_selectCountry(countryTag);
|
||||
KRCommonUtil.kr_showToast('已切换到 $countryTag 节点组');
|
||||
}
|
||||
|
||||
/// 🔧 修复:简化的 kr_selectNode 方法
|
||||
/// 现在只是委托给新的 kr_performNodeSwitch 方法
|
||||
@@ -1414,8 +1422,8 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
|
||||
/// 获取真实连接的节点信息(auto 模式下获取实际连接的节点)
|
||||
Map<String, dynamic> kr_getRealConnectedNodeInfo() {
|
||||
// 如果不是 auto 模式,直接返回当前选中的节点信息
|
||||
if (kr_cutTag.value != 'auto') {
|
||||
// 如果不是 auto 模式,也不是 country-auto 模式,直接返回当前选中的节点信息
|
||||
if (kr_cutTag.value != 'auto' && !kr_cutTag.value.endsWith('-auto')) {
|
||||
final node = kr_subscribeService.keyList[kr_cutSeletedTag.value];
|
||||
return {
|
||||
'nodeName': kr_cutSeletedTag.value,
|
||||
@@ -1424,11 +1432,13 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
};
|
||||
}
|
||||
|
||||
// auto 模式下,获取 urltest 组的实际连接节点
|
||||
// 处理 auto 模式(包括全局 auto 和 country-auto)
|
||||
print('当前活动组----${KRSingBoxImp.instance.kr_activeGroups.length}');
|
||||
for (var group in KRSingBoxImp.instance.kr_activeGroups) {
|
||||
print('当前活动组----$group}');
|
||||
if (group.type == ProxyType.urltest) {
|
||||
|
||||
// 处理全局 auto 模式
|
||||
if (kr_cutTag.value == 'auto' && group.type == ProxyType.urltest && group.tag == 'auto') {
|
||||
final selectedNode = group.selected;
|
||||
final node = kr_subscribeService.keyList[selectedNode];
|
||||
return {
|
||||
@@ -1437,12 +1447,39 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
'country': node?.country ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
// 处理 country-auto 模式
|
||||
if (kr_cutTag.value.endsWith('-auto') && group.type == ProxyType.urltest) {
|
||||
if (group.tag == kr_cutTag.value) {
|
||||
final selectedNode = group.selected;
|
||||
final node = kr_subscribeService.keyList[selectedNode];
|
||||
return {
|
||||
'nodeName': selectedNode,
|
||||
'delay': node?.urlTestDelay.value ?? -2,
|
||||
'country': node?.country ?? '',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
print('hhhhhh${kr_subscribeService.keyList}', );
|
||||
|
||||
// 处理 country-auto 模式的备用方案(当 SingBox 组数据不可用时)
|
||||
if (kr_cutTag.value.endsWith('-auto')) {
|
||||
final countryCode = kr_cutTag.value.replaceAll('-auto', '');
|
||||
KRLogUtil.kr_i('🔄 kr_getRealConnectedNodeInfo 使用备用方案获取 country-auto 信息: $countryCode', tag: 'HomeController');
|
||||
final autoNodeInfo = kr_getCountryAutoSelectedNode(countryCode);
|
||||
if (autoNodeInfo != null) {
|
||||
return {
|
||||
'nodeName': autoNodeInfo['tag'],
|
||||
'delay': autoNodeInfo['delay'] ?? -2,
|
||||
'country': autoNodeInfo['country'] ?? countryCode,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到 urltest 组,返回默认值
|
||||
return {
|
||||
'nodeName': 'auto',
|
||||
'nodeName': kr_cutTag.value,
|
||||
'delay': -2,
|
||||
'country': '',
|
||||
};
|
||||
@@ -1463,9 +1500,10 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
/// 获取真实连接的节点国家
|
||||
String kr_getRealConnectedNodeCountry() {
|
||||
final info = kr_getRealConnectedNodeInfo();
|
||||
final delay = kr_currentNodeLatency.value;
|
||||
final delay = info['delay'] as int; // 使用真实连接的节点延迟,而不是 kr_currentNodeLatency.value
|
||||
final country1 = kr_getCurrentNodeCountry();
|
||||
print('country----$country1');
|
||||
print('kr_getRealConnectedNodeCountry - delay from info: $delay, country from info: ${info['country']}');
|
||||
final country = kr_getCountryFullName(info['country']);
|
||||
if (delay == -2) {
|
||||
return '--';
|
||||
@@ -1932,7 +1970,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
final results = await KRLatencyTester.testMultipleNodes(
|
||||
nodes: nodeAddresses,
|
||||
concurrency: 10, // 每批10个并发
|
||||
timeout: 5000, // 超时时间5秒(毫秒)
|
||||
timeout: const Duration(seconds: 5),
|
||||
);
|
||||
|
||||
// 更新节点延迟
|
||||
@@ -2078,6 +2116,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
kr_isConnected.value = true;
|
||||
kr_startConnectionTimer();
|
||||
kr_updateConnectionInfo();
|
||||
KRSingBoxImp.instance.kr_debugTunConnectivity();
|
||||
|
||||
// 🔧 修复:同步已启动状态时,尝试更新延迟值
|
||||
if (!_kr_tryUpdateDelayFromActiveGroups()) {
|
||||
@@ -2173,6 +2212,225 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
});
|
||||
}
|
||||
|
||||
/// 获取国家-auto组的当前选中子节点信息
|
||||
Map<String, dynamic>? kr_getCountryAutoSelectedNode(String countryCode) {
|
||||
try {
|
||||
final activeGroups = KRSingBoxImp.instance.kr_activeGroups;
|
||||
final allGroups = KRSingBoxImp.instance.kr_allGroups;
|
||||
final autoGroupTag = '${countryCode}-auto';
|
||||
|
||||
KRLogUtil.kr_i('🔍 开始获取国家-auto选中节点: countryCode=$countryCode, autoGroupTag=$autoGroupTag', tag: 'HomeController');
|
||||
KRLogUtil.kr_i('📊 活跃组数量: ${activeGroups.length}, 所有组数量: ${allGroups.length}', tag: 'HomeController');
|
||||
|
||||
// 优先检查活跃组
|
||||
if (activeGroups.isNotEmpty) {
|
||||
KRLogUtil.kr_i('✅ 活跃组不为空,优先检查活跃组', tag: 'HomeController');
|
||||
for (var group in activeGroups) {
|
||||
KRLogUtil.kr_i('🔄 检查活跃组: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'HomeController');
|
||||
|
||||
if (group.tag == autoGroupTag && group.type == ProxyType.urltest) {
|
||||
KRLogUtil.kr_i('✅ 在活跃组中找到匹配的urltest组: $autoGroupTag', tag: 'HomeController');
|
||||
return _kr_extractSelectedNodeInfo(group, countryCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果活跃组中没有,检查所有组
|
||||
if (allGroups.isNotEmpty) {
|
||||
KRLogUtil.kr_i('🔍 活跃组中未找到,检查所有组', tag: 'HomeController');
|
||||
for (var group in allGroups) {
|
||||
KRLogUtil.kr_i('🔄 检查所有组: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'HomeController');
|
||||
|
||||
if (group.tag == autoGroupTag && group.type == ProxyType.urltest) {
|
||||
KRLogUtil.kr_i('✅ 在所有组中找到匹配的urltest组: $autoGroupTag', tag: 'HomeController');
|
||||
return _kr_extractSelectedNodeInfo(group, countryCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果组数据都为空,使用备用方案:从订阅服务中找该国家最快的节点
|
||||
KRLogUtil.kr_i('🔄 组数据为空,使用备用方案从订阅服务获取最快节点', tag: 'HomeController');
|
||||
return _kr_getFastestNodeFromSubscribeService(countryCode);
|
||||
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('💥 获取国家-auto选中节点异常: $e', tag: 'HomeController');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 从订阅服务获取该国家最快节点的备用方案
|
||||
Map<String, dynamic>? _kr_getFastestNodeFromSubscribeService(String countryCode) {
|
||||
try {
|
||||
KRLogUtil.kr_i('🔄 使用订阅服务查找国家最快节点: $countryCode', tag: 'HomeController');
|
||||
|
||||
// 从订阅服务中获取该国家的所有节点
|
||||
final allNodes = kr_subscribeService.allList.where((node) =>
|
||||
node.country == countryCode && !node.tag.endsWith('-auto')
|
||||
).toList();
|
||||
|
||||
KRLogUtil.kr_i('📊 找到 ${allNodes.length} 个该国家的普通节点', tag: 'HomeController');
|
||||
|
||||
if (allNodes.isEmpty) {
|
||||
KRLogUtil.kr_w('⚠️ 未找到该国家的任何普通节点: $countryCode', tag: 'HomeController');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 找出延迟最小的节点(排除0和超时)
|
||||
KROutboundItem? fastestNode;
|
||||
int fastestDelay = 999999;
|
||||
|
||||
for (var node in allNodes) {
|
||||
final delay = node.urlTestDelay.value;
|
||||
if (delay > 0 && delay < 3000 && delay < fastestDelay) {
|
||||
fastestDelay = delay;
|
||||
fastestNode = node;
|
||||
}
|
||||
}
|
||||
|
||||
if (fastestNode != null) {
|
||||
KRLogUtil.kr_i('✅ 找到最快节点: ${fastestNode.tag}, 延迟: ${fastestDelay}ms', tag: 'HomeController');
|
||||
return {
|
||||
'tag': fastestNode.tag,
|
||||
'delay': fastestDelay,
|
||||
'country': countryCode,
|
||||
};
|
||||
} else {
|
||||
KRLogUtil.kr_w('⚠️ 该国家的节点都没有有效延迟数据', tag: 'HomeController');
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('💥 获取订阅服务最快节点异常: $e', tag: 'HomeController');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 提取选中节点信息的辅助方法
|
||||
Map<String, dynamic>? _kr_extractSelectedNodeInfo(dynamic group, String countryCode) {
|
||||
try {
|
||||
// 获取当前选中的节点
|
||||
final selectedNode = group.selected;
|
||||
KRLogUtil.kr_i('🎯 当前选中节点: $selectedNode', tag: 'HomeController');
|
||||
KRLogUtil.kr_i('📋 组内项目数量: ${group.items.length}', tag: 'HomeController');
|
||||
|
||||
if (selectedNode != null && selectedNode.isNotEmpty) {
|
||||
KRLogUtil.kr_i('✅ 选中节点有效,开始查找节点详情', tag: 'HomeController');
|
||||
|
||||
// 打印所有节点信息用于调试
|
||||
for (var item in group.items) {
|
||||
KRLogUtil.kr_i('📄 组内节点: tag=${item.tag}, delay=${item.urlTestDelay}', tag: 'HomeController');
|
||||
}
|
||||
|
||||
// 在组内查找选中的节点
|
||||
try {
|
||||
final selectedItem = group.items.firstWhere(
|
||||
(item) => item.tag == selectedNode,
|
||||
);
|
||||
|
||||
KRLogUtil.kr_i('🎉 成功找到选中节点: tag=${selectedItem.tag}, delay=${selectedItem.urlTestDelay}', tag: 'HomeController');
|
||||
|
||||
return {
|
||||
'tag': selectedNode,
|
||||
'delay': selectedItem.urlTestDelay,
|
||||
'country': countryCode,
|
||||
};
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ 在组内未找到选中节点: $selectedNode', tag: 'HomeController');
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
KRLogUtil.kr_w('⚠️ 选中节点为空或无效', tag: 'HomeController');
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('💥 提取选中节点信息异常: $e', tag: 'HomeController');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取全局 auto 组的当前选中子节点信息
|
||||
Map<String, dynamic>? kr_getGlobalAutoSelectedNode() {
|
||||
try {
|
||||
final activeGroups = KRSingBoxImp.instance.kr_activeGroups;
|
||||
|
||||
KRLogUtil.kr_i('🌍 开始获取全局auto选中节点信息', tag: 'HomeController');
|
||||
KRLogUtil.kr_i('📊 活跃组数量: ${activeGroups.length}', tag: 'HomeController');
|
||||
|
||||
for (var group in activeGroups) {
|
||||
KRLogUtil.kr_i('🔄 检查全局组: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'HomeController');
|
||||
KRLogUtil.kr_i('📋 全局组内项目数量: ${group.items.length}', tag: 'HomeController');
|
||||
|
||||
if (group.tag == 'auto' && group.type == ProxyType.urltest) {
|
||||
KRLogUtil.kr_i('✅ 找到全局auto urltest组', tag: 'HomeController');
|
||||
|
||||
// 获取当前选中的节点
|
||||
final selectedNode = group.selected;
|
||||
KRLogUtil.kr_i('🎯 全局auto当前选中节点: $selectedNode', tag: 'HomeController');
|
||||
|
||||
if (selectedNode != null && selectedNode.isNotEmpty) {
|
||||
KRLogUtil.kr_i('✅ 全局auto选中节点有效,开始查找详情', tag: 'HomeController');
|
||||
|
||||
// 打印所有节点信息用于调试
|
||||
for (var item in group.items) {
|
||||
KRLogUtil.kr_i('📄 全局auto组内节点: tag=${item.tag}, delay=${item.urlTestDelay}', tag: 'HomeController');
|
||||
}
|
||||
|
||||
// 在组内查找选中的节点
|
||||
try {
|
||||
final selectedItem = group.items.firstWhere(
|
||||
(item) => item.tag == selectedNode,
|
||||
);
|
||||
|
||||
KRLogUtil.kr_i('🎉 成功找到全局auto选中节点: tag=${selectedItem.tag}, delay=${selectedItem.urlTestDelay}', tag: 'HomeController');
|
||||
|
||||
return {
|
||||
'tag': selectedNode,
|
||||
'delay': selectedItem.urlTestDelay,
|
||||
'country': '',
|
||||
};
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ 在全局auto组内未找到选中节点: $selectedNode', tag: 'HomeController');
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
KRLogUtil.kr_w('⚠️ 全局auto选中节点为空或无效', tag: 'HomeController');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
KRLogUtil.kr_w('❌ 未找到全局auto组', tag: 'HomeController');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('💥 获取全局auto选中节点异常: $e', tag: 'HomeController');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 获取指定国家的所有真实节点延迟列表
|
||||
List<Map<String, dynamic>> kr_getCountryRealNodeDelays(String countryCode) {
|
||||
final delays = <Map<String, dynamic>>[];
|
||||
|
||||
try {
|
||||
// 从订阅服务中获取该国家的节点列表
|
||||
final countryNodes = kr_subscribeService.keyList.values
|
||||
.where((item) => item.country == countryCode && !item.tag.endsWith('-auto'))
|
||||
.toList();
|
||||
|
||||
for (final node in countryNodes) {
|
||||
delays.add({
|
||||
'tag': node.tag,
|
||||
'delay': node.urlTestDelay.value,
|
||||
'city': node.city,
|
||||
});
|
||||
}
|
||||
|
||||
// 按延迟排序
|
||||
delays.sort((a, b) => a['delay'].compareTo(b['delay']));
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('获取国家真实节点延迟列表失败: $e', tag: 'HomeController');
|
||||
}
|
||||
|
||||
return delays;
|
||||
}
|
||||
|
||||
/// 尝试从活动组更新延迟值
|
||||
bool _kr_tryUpdateDelayFromActiveGroups() {
|
||||
try {
|
||||
@@ -2198,6 +2456,17 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果是国家-auto模式,从对应的国家urltest组获取延迟
|
||||
else if (kr_cutTag.value.endsWith('-auto')) {
|
||||
final countryCode = kr_cutTag.value.replaceAll('-auto', '');
|
||||
for (var item in group.items) {
|
||||
if (item.tag == kr_cutTag.value && item.urlTestDelay != 0) {
|
||||
kr_currentNodeLatency.value = item.urlTestDelay;
|
||||
KRLogUtil.kr_i('✅ ${countryCode}-auto模式延迟值: ${item.urlTestDelay}ms', tag: 'HomeController');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 手动选择模式
|
||||
else {
|
||||
for (var item in group.items) {
|
||||
|
||||
@@ -285,7 +285,7 @@ class KRSplashController extends GetxController {
|
||||
KRLogUtil.kr_e('⏱️ 初始化超时: $e', tag: 'SplashController');
|
||||
print('⏱️ 初始化超时,直接跳转到主页');
|
||||
// 超时后直接跳转到主页,让用户可以手动重试
|
||||
// Get.offAllNamed(Routes.KR_MAIN);
|
||||
// Get.offAllNamed(Routes.KR_HOME);
|
||||
HIDialog.show(
|
||||
message: '初始化超时,请检查网络或重试',
|
||||
confirmText: '重试',
|
||||
@@ -484,7 +484,7 @@ class KRSplashController extends GetxController {
|
||||
_initLog.logWarning('网络权限检查失败或超时,执行降级初始化', tag: 'Init');
|
||||
KRLogUtil.kr_w('⚠️ 网络权限检查失败或超时,执行降级初始化', tag: 'SplashController');
|
||||
await _executeMinimalInitialization();
|
||||
Get.offAllNamed(Routes.KR_MAIN);
|
||||
Get.offAllNamed(Routes.KR_HOME);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -493,7 +493,7 @@ class KRSplashController extends GetxController {
|
||||
_initLog.logError('网络权限检查异常,执行降级初始化', tag: 'Init', error: e);
|
||||
KRLogUtil.kr_w('⚠️ 网络权限检查异常: $e,执行降级初始化', tag: 'SplashController');
|
||||
await _executeMinimalInitialization();
|
||||
Get.offAllNamed(Routes.KR_MAIN);
|
||||
Get.offAllNamed(Routes.KR_HOME);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
@@ -582,7 +582,7 @@ class KRSplashController extends GetxController {
|
||||
_initLog.logError('启动页初始化异常,执行降级策略', tag: 'Continue', error: e);
|
||||
KRLogUtil.kr_w('⚠️ 启动页初始化异常,执行降级策略: $e', tag: 'SplashController');
|
||||
await _executeMinimalInitialization();
|
||||
Get.offAllNamed(Routes.KR_MAIN);
|
||||
Get.offAllNamed(Routes.KR_HOME);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -726,7 +726,7 @@ class KRSplashController extends GetxController {
|
||||
kr_isLoading.value = false;
|
||||
|
||||
// 直接跳转到主页
|
||||
Get.offAllNamed(Routes.KR_MAIN);
|
||||
Get.offAllNamed(Routes.KR_HOME);
|
||||
}
|
||||
|
||||
/// 🔧 修复1.1:清理旧的本地存储数据(DEBUG模式专用)
|
||||
|
||||
@@ -16,6 +16,7 @@ import 'package:path/path.dart' as p;
|
||||
import '../../../core/model/directories.dart';
|
||||
import '../../../singbox/model/singbox_config_option.dart';
|
||||
import '../../../singbox/model/singbox_outbound.dart';
|
||||
import '../../../singbox/model/singbox_proxy_type.dart';
|
||||
import '../../../singbox/model/singbox_stats.dart';
|
||||
import '../../../singbox/model/singbox_status.dart';
|
||||
import '../../utils/kr_country_util.dart';
|
||||
@@ -23,6 +24,8 @@ import '../../utils/kr_log_util.dart';
|
||||
import '../../utils/kr_secure_storage.dart';
|
||||
import '../../common/app_run_data.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
|
||||
|
||||
enum KRConnectionType {
|
||||
global,
|
||||
@@ -107,6 +110,9 @@ class KRSingBoxImp {
|
||||
/// 初始化标志,防止重复初始化
|
||||
bool _kr_isInitialized = false;
|
||||
|
||||
/// 初始化进行中共享 Future(single-flight 防并发)
|
||||
Future<void>? _kr_initFuture;
|
||||
|
||||
/// 当前混合代理端口是否就绪
|
||||
bool get kr_isProxyReady => kr_status.value is SingboxStarted;
|
||||
|
||||
@@ -139,12 +145,23 @@ class KRSingBoxImp {
|
||||
|
||||
/// 初始化
|
||||
Future<void> init() async {
|
||||
// 防止重复初始化
|
||||
// 防止重复初始化(已完成)
|
||||
if (_kr_isInitialized) {
|
||||
KRLogUtil.kr_i('SingBox 已经初始化,跳过重复初始化', tag: 'SingBox');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 防止并发重复初始化(进行中复用同一个 Future)
|
||||
if (_kr_initFuture != null) {
|
||||
KRLogUtil.kr_i('SingBox 初始化进行中,等待完成(single-flight)', tag: 'SingBox');
|
||||
await _kr_initFuture;
|
||||
return;
|
||||
}
|
||||
|
||||
// 建立 single-flight 共享 Future
|
||||
final completer = Completer<void>();
|
||||
_kr_initFuture = completer.future;
|
||||
|
||||
try {
|
||||
KRLogUtil.kr_i('开始初始化 SingBox');
|
||||
// 在应用启动时初始化
|
||||
@@ -167,16 +184,16 @@ class KRSingBoxImp {
|
||||
KRLogUtil.kr_i('iOS 路径获取完成: $paths');
|
||||
|
||||
kr_configDics = (
|
||||
baseDir: Directory(paths?["base"]! as String),
|
||||
workingDir: Directory(paths?["working"]! as String),
|
||||
tempDir: Directory(paths?["temp"]! as String),
|
||||
baseDir: Directory(paths?["base"]! as String),
|
||||
workingDir: Directory(paths?["working"]! as String),
|
||||
tempDir: Directory(paths?["temp"]! as String),
|
||||
);
|
||||
} else {
|
||||
final baseDir = await getApplicationSupportDirectory();
|
||||
final workingDir =
|
||||
Platform.isAndroid ? await getExternalStorageDirectory() : baseDir;
|
||||
Platform.isAndroid ? await getExternalStorageDirectory() : baseDir;
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
|
||||
|
||||
// Windows 路径规范化:确保使用正确的路径分隔符
|
||||
Directory normalizePath(Directory dir) {
|
||||
if (Platform.isWindows) {
|
||||
@@ -188,11 +205,11 @@ class KRSingBoxImp {
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
|
||||
kr_configDics = (
|
||||
baseDir: normalizePath(baseDir),
|
||||
workingDir: normalizePath(workingDir!),
|
||||
tempDir: normalizePath(tempDir),
|
||||
baseDir: normalizePath(baseDir),
|
||||
workingDir: normalizePath(workingDir!),
|
||||
tempDir: normalizePath(tempDir),
|
||||
);
|
||||
KRLogUtil.kr_i('其他平台路径初始化完成');
|
||||
}
|
||||
@@ -201,7 +218,7 @@ class KRSingBoxImp {
|
||||
KRLogUtil.kr_i('baseDir: ${kr_configDics.baseDir.path}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('workingDir: ${kr_configDics.workingDir.path}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('tempDir: ${kr_configDics.tempDir.path}', tag: 'SingBox');
|
||||
|
||||
|
||||
// 确保所有目录都存在
|
||||
if (!kr_configDics.baseDir.existsSync()) {
|
||||
await kr_configDics.baseDir.create(recursive: true);
|
||||
@@ -215,11 +232,11 @@ class KRSingBoxImp {
|
||||
await kr_configDics.tempDir.create(recursive: true);
|
||||
KRLogUtil.kr_i('已创建 tempDir', tag: 'SingBox');
|
||||
}
|
||||
|
||||
|
||||
// 创建 libcore 数据库所需的 data 目录(在 workingDir 下)
|
||||
// 注意:libcore 的 Setup 会调用 os.Chdir(workingPath),所以 data 目录必须在 workingDir 下
|
||||
final dataDir = Directory(p.join(kr_configDics.workingDir.path, 'data'));
|
||||
|
||||
|
||||
// 强制确保 data 目录存在(Windows 可能需要多次尝试)
|
||||
int retryCount = 0;
|
||||
const maxRetries = 5;
|
||||
@@ -228,7 +245,7 @@ class KRSingBoxImp {
|
||||
await dataDir.create(recursive: true);
|
||||
// 等待文件系统同步(Windows 上可能需要一点时间)
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
|
||||
// 验证目录确实创建成功
|
||||
if (dataDir.existsSync()) {
|
||||
KRLogUtil.kr_i('✅ 已创建 data 目录: ${dataDir.path}', tag: 'SingBox');
|
||||
@@ -248,13 +265,13 @@ class KRSingBoxImp {
|
||||
await Future.delayed(Duration(milliseconds: delayMs));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!dataDir.existsSync()) {
|
||||
final error = 'data 目录不存在: ${dataDir.path}';
|
||||
KRLogUtil.kr_e('❌ $error', tag: 'SingBox');
|
||||
throw Exception(error);
|
||||
}
|
||||
|
||||
|
||||
// 验证目录权限(尝试创建一个测试文件)
|
||||
try {
|
||||
final testFile = File(p.join(dataDir.path, '.test_write'));
|
||||
@@ -265,13 +282,13 @@ class KRSingBoxImp {
|
||||
KRLogUtil.kr_e('⚠️ data 目录写入权限验证失败: $e', tag: 'SingBox');
|
||||
// 不抛出异常,让 libcore 自己处理
|
||||
}
|
||||
|
||||
|
||||
// 在 Windows 上额外等待,确保文件系统操作完成
|
||||
if (Platform.isWindows) {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
KRLogUtil.kr_i('⏳ Windows 文件系统同步等待完成', tag: 'SingBox');
|
||||
}
|
||||
|
||||
|
||||
// 最终验证:在 setup() 之前再次确认 workingDir 和 data 目录都存在且可访问
|
||||
// libcore 的 Setup() 会调用 os.Chdir(workingPath),然后使用相对路径 "./data"
|
||||
// 如果 os.Chdir() 失败(路径不存在或权限问题),后续的相对路径访问会失败
|
||||
@@ -280,7 +297,7 @@ class KRSingBoxImp {
|
||||
KRLogUtil.kr_e(error, tag: 'SingBox');
|
||||
throw Exception(error);
|
||||
}
|
||||
|
||||
|
||||
// 验证 workingDir 可读可写
|
||||
try {
|
||||
final testWorkingFile = File(p.join(kr_configDics.workingDir.path, '.test_working_dir'));
|
||||
@@ -292,7 +309,7 @@ class KRSingBoxImp {
|
||||
KRLogUtil.kr_e(error, tag: 'SingBox');
|
||||
throw Exception(error);
|
||||
}
|
||||
|
||||
|
||||
final finalDataDir = Directory(p.join(kr_configDics.workingDir.path, 'data'));
|
||||
if (!finalDataDir.existsSync()) {
|
||||
KRLogUtil.kr_e('❌ 最终验证失败:data 目录不存在', tag: 'SingBox');
|
||||
@@ -308,7 +325,7 @@ class KRSingBoxImp {
|
||||
}
|
||||
throw Exception('data 目录在 setup() 前验证失败: ${finalDataDir.path}');
|
||||
}
|
||||
|
||||
|
||||
// 再次尝试写入测试,确保目录确实可用
|
||||
try {
|
||||
final verifyFile = File(p.join(finalDataDir.path, '.verify_setup'));
|
||||
@@ -364,7 +381,7 @@ class KRSingBoxImp {
|
||||
KRLogUtil.kr_i(' - tempDir: ${kr_configDics.tempDir.path}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - data 目录: ${p.join(kr_configDics.workingDir.path, "data")}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - data 目录存在: ${finalDataDir.existsSync()}', tag: 'SingBox');
|
||||
|
||||
|
||||
// 在 Windows 上,列出 data 目录内容(如果有文件)
|
||||
if (Platform.isWindows && finalDataDir.existsSync()) {
|
||||
try {
|
||||
@@ -378,10 +395,10 @@ class KRSingBoxImp {
|
||||
KRLogUtil.kr_e(' - 无法列出 data 目录内容: $e', tag: 'SingBox');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
KRLogUtil.kr_i(' - libcore 将通过 os.Chdir() 切换到: ${kr_configDics.workingDir.path}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - 然后使用相对路径 "./data" 访问数据库', tag: 'SingBox');
|
||||
|
||||
|
||||
// Windows 特定:验证路径格式是否正确
|
||||
if (Platform.isWindows) {
|
||||
final workingPath = kr_configDics.workingDir.path;
|
||||
@@ -400,11 +417,11 @@ class KRSingBoxImp {
|
||||
KRLogUtil.kr_i('📡 开始调用 setup() 注册 FFI 端口', tag: 'SingBox');
|
||||
final setupResult = await kr_singBox.setup(kr_configDics, false).run();
|
||||
setupResult.match(
|
||||
(error) {
|
||||
(error) {
|
||||
KRLogUtil.kr_e('❌ setup() 失败: $error', tag: 'SingBox');
|
||||
throw Exception('FFI setup 失败: $error');
|
||||
},
|
||||
(_) {
|
||||
(_) {
|
||||
KRLogUtil.kr_i('✅ setup() 成功,FFI 端口已注册', tag: 'SingBox');
|
||||
},
|
||||
);
|
||||
@@ -416,6 +433,10 @@ class KRSingBoxImp {
|
||||
// 这样可以确保 UI 始终与 libcore 的实际状态同步
|
||||
_kr_subscribeToStatus();
|
||||
KRLogUtil.kr_i('✅ 状态订阅已设置', tag: 'SingBox');
|
||||
|
||||
// 完成 single-flight
|
||||
completer.complete();
|
||||
_kr_initFuture = null;
|
||||
} catch (e, stackTrace) {
|
||||
KRLogUtil.kr_e('❌ SingBox 初始化失败: $e');
|
||||
KRLogUtil.kr_e('📚 错误堆栈: $stackTrace');
|
||||
@@ -451,6 +472,11 @@ class KRSingBoxImp {
|
||||
|
||||
// 如果初始化失败,允许下次重试
|
||||
_kr_isInitialized = false;
|
||||
// 失败时通知等待者并清理 single-flight
|
||||
if (!completer.isCompleted) {
|
||||
completer.completeError(e, stackTrace);
|
||||
}
|
||||
_kr_initFuture = null;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -464,13 +490,13 @@ class KRSingBoxImp {
|
||||
"region": "other", // 参考 hiddify-app: 默认使用 "other" 跳过规则集下载
|
||||
"block-ads": false, // 参考 hiddify-app: 默认关闭广告拦截
|
||||
"use-xray-core-when-possible": false,
|
||||
"execute-config-as-is": false,
|
||||
"execute-config-as-is": true, // 🔧 修复:使用我们生成的完整配置,不让 libcore 重写
|
||||
"log-level": "info", // 调试阶段使用 info,生产环境改为 warn
|
||||
"resolve-destination": false,
|
||||
"ipv6-mode": "ipv4_only", // 参考 hiddify-app: 仅使用 IPv4 (有效值: ipv4_only, prefer_ipv4, prefer_ipv6, ipv6_only)
|
||||
"remote-dns-address": "udp://1.1.1.1", // 参考 hiddify-app: 使用 Cloudflare DNS
|
||||
"remote-dns-address": "https://dns.google/dns-query", // 使用 Google DoH,避免中转节点 DNS 死锁
|
||||
"remote-dns-domain-strategy": "prefer_ipv4",
|
||||
"direct-dns-address": "udp://1.1.1.1", // 参考 hiddify-app: 统一使用 1.1.1.1
|
||||
"direct-dns-address": "local" ,
|
||||
"direct-dns-domain-strategy": "prefer_ipv4",
|
||||
"mixed-port": kr_port,
|
||||
"tproxy-port": kr_port,
|
||||
@@ -561,7 +587,7 @@ class KRSingBoxImp {
|
||||
|
||||
_kr_subscriptions.add(
|
||||
kr_singBox.watchStatus().listen(
|
||||
(status) {
|
||||
(status) {
|
||||
if (kDebugMode) {
|
||||
print('🔵 收到 Native 状态更新: ${status.runtimeType}');
|
||||
}
|
||||
@@ -599,7 +625,7 @@ class KRSingBoxImp {
|
||||
// 所以外层必须有 try-catch
|
||||
final stream = kr_singBox.watchStats();
|
||||
final subscription = stream.listen(
|
||||
(stats) {
|
||||
(stats) {
|
||||
kr_stats.value = stats;
|
||||
},
|
||||
onError: (error) {
|
||||
@@ -624,11 +650,11 @@ class KRSingBoxImp {
|
||||
|
||||
_kr_subscriptions.add(
|
||||
kr_singBox.watchActiveGroups().listen(
|
||||
(groups) {
|
||||
(groups) {
|
||||
print('[watchActiveGroups] 📡 收到活动组更新,数量: ${groups.length}');
|
||||
KRLogUtil.kr_i('📡 收到活动组更新,数量: ${groups.length}', tag: 'SingBox');
|
||||
kr_activeGroups.value = groups;
|
||||
|
||||
|
||||
// 详细打印每个组的信息
|
||||
for (int i = 0; i < groups.length; i++) {
|
||||
final group = groups[i];
|
||||
@@ -638,7 +664,7 @@ class KRSingBoxImp {
|
||||
KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
KRLogUtil.kr_i('✅ 活动组处理完成', tag: 'SingBox');
|
||||
},
|
||||
onError: (error) {
|
||||
@@ -651,7 +677,7 @@ class KRSingBoxImp {
|
||||
|
||||
_kr_subscriptions.add(
|
||||
kr_singBox.watchGroups().listen(
|
||||
(groups) {
|
||||
(groups) {
|
||||
print('[watchGroups] 📡 收到所有组更新,数量: ${groups.length}');
|
||||
kr_allGroups.value = groups;
|
||||
// 打印每个组的基本信息
|
||||
@@ -679,7 +705,7 @@ class KRSingBoxImp {
|
||||
|
||||
// 查找 "select" 组
|
||||
final selectGroup = kr_activeGroups.firstWhere(
|
||||
(group) => group.tag == 'select',
|
||||
(group) => group.tag == 'select',
|
||||
orElse: () => throw Exception('未找到 "select" 选择器组'),
|
||||
);
|
||||
|
||||
@@ -719,11 +745,11 @@ class KRSingBoxImp {
|
||||
///
|
||||
/// 确保 command.sock 准备好后再执行节点选择
|
||||
Future<void> _kr_selectOutboundWithRetry(
|
||||
String groupTag,
|
||||
String outboundTag, {
|
||||
int maxAttempts = 3,
|
||||
int initialDelay = 100,
|
||||
}) async {
|
||||
String groupTag,
|
||||
String outboundTag, {
|
||||
int maxAttempts = 3,
|
||||
int initialDelay = 100,
|
||||
}) async {
|
||||
int attempt = 0;
|
||||
int delay = initialDelay;
|
||||
|
||||
@@ -999,10 +1025,25 @@ class KRSingBoxImp {
|
||||
KRLogUtil.kr_i('💾 开始保存配置文件...', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📊 出站节点数量: ${outbounds.length}', tag: 'SingBox');
|
||||
|
||||
// 分离真实节点和自动选择节点
|
||||
final realNodes = <Map<String, dynamic>>[];
|
||||
final autoNodes = <Map<String, dynamic>>[];
|
||||
|
||||
for (final outbound in outbounds) {
|
||||
if (outbound['tag']?.toString().endsWith('-auto') == true) {
|
||||
autoNodes.add(outbound);
|
||||
} else {
|
||||
realNodes.add(outbound);
|
||||
}
|
||||
}
|
||||
|
||||
KRLogUtil.kr_i('📊 真实节点数量: ${realNodes.length}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📊 自动选择节点数量: ${autoNodes.length}', tag: 'SingBox');
|
||||
|
||||
// 打印每个节点的详细配置
|
||||
for (int i = 0; i < outbounds.length; i++) {
|
||||
final outbound = outbounds[i];
|
||||
KRLogUtil.kr_i('📋 节点[$i] 配置:', tag: 'SingBox');
|
||||
for (int i = 0; i < realNodes.length; i++) {
|
||||
final outbound = realNodes[i];
|
||||
KRLogUtil.kr_i('📋 真实节点[$i] 配置:', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - type: ${outbound['type']}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - tag: ${outbound['tag']}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - server: ${outbound['server']}', tag: 'SingBox');
|
||||
@@ -1016,11 +1057,20 @@ class KRSingBoxImp {
|
||||
if (outbound['uuid'] != null) {
|
||||
KRLogUtil.kr_i(' - uuid: ${outbound['uuid']?.toString().substring(0, 8)}...', tag: 'SingBox');
|
||||
}
|
||||
KRLogUtil.kr_i(' - 完整配置: ${jsonEncode(outbound)}', tag: 'SingBox');
|
||||
}
|
||||
|
||||
for (int i = 0; i < autoNodes.length; i++) {
|
||||
final outbound = autoNodes[i];
|
||||
KRLogUtil.kr_i('🤖 自动选择节点[$i] 配置:', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - type: ${outbound['type']}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i(' - tag: ${outbound['tag']}', tag: 'SingBox');
|
||||
if (outbound['outbounds'] != null) {
|
||||
KRLogUtil.kr_i(' - outbounds: ${outbound['outbounds']}', tag: 'SingBox');
|
||||
}
|
||||
}
|
||||
|
||||
// ⚠️ 临时过滤 Hysteria2 节点以避免 libcore 崩溃
|
||||
kr_outbounds = outbounds.where((outbound) {
|
||||
final filteredRealNodes = realNodes.where((outbound) {
|
||||
final type = outbound['type'];
|
||||
if (type == 'hysteria2' || type == 'hysteria') {
|
||||
KRLogUtil.kr_w('⚠️ 跳过 Hysteria2 节点: ${outbound['tag']} (libcore bug)', tag: 'SingBox');
|
||||
@@ -1029,19 +1079,111 @@ class KRSingBoxImp {
|
||||
return true;
|
||||
}).toList();
|
||||
|
||||
KRLogUtil.kr_i('✅ 过滤后节点数量: ${kr_outbounds.length}/${outbounds.length}', tag: 'SingBox');
|
||||
// 过滤自动选择节点中的 Hysteria2
|
||||
final filteredAutoNodes = autoNodes.where((outbound) {
|
||||
if (outbound['outbounds'] != null) {
|
||||
final outbounds = outbound['outbounds'] as List<dynamic>;
|
||||
// 检查是否包含 Hysteria2 节点
|
||||
final hasHysteria2 = outbounds.any((tag) => tag.toString().toLowerCase().contains('hysteria'));
|
||||
if (hasHysteria2) {
|
||||
KRLogUtil.kr_w('⚠️ 跳过包含 Hysteria2 的自动选择节点: ${outbound['tag']} (libcore bug)', tag: 'SingBox');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
|
||||
// 只保存 outbounds,Mobile.buildConfig() 会添加其他配置
|
||||
final map = {
|
||||
"outbounds": kr_outbounds
|
||||
KRLogUtil.kr_i('✅ 过滤后真实节点数量: ${filteredRealNodes.length}/${realNodes.length}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('✅ 过滤后自动选择节点数量: ${filteredAutoNodes.length}/${autoNodes.length}', tag: 'SingBox');
|
||||
|
||||
// 构建所有可用标签列表(selector 包含真实和自动节点)
|
||||
final allAvailableTags = <String>[];
|
||||
allAvailableTags.addAll(filteredAutoNodes.map((o) => o['tag'] as String));
|
||||
allAvailableTags.addAll(filteredRealNodes.map((o) => o['tag'] as String));
|
||||
|
||||
// 🔧 修复:生成完整的 SingBox 配置
|
||||
// 现在保存完整配置,包含所有必需字段,并且 outbounds 顺序为:selector→自动组→真实节点→direct/block/dns-out
|
||||
final Map<String, dynamic> fullConfig = {
|
||||
"log": {
|
||||
"level": "debug",
|
||||
"timestamp": true
|
||||
},
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"tag": "dns-remote",
|
||||
"address": "https://dns.google/dns-query",
|
||||
"detour": "direct",
|
||||
"address_resolver": "dns-direct"
|
||||
},
|
||||
{
|
||||
"tag": "dns-direct",
|
||||
"address": "local",
|
||||
"detour": "direct"
|
||||
}
|
||||
],
|
||||
"rules": [],
|
||||
"final": "dns-remote",
|
||||
"strategy": "prefer_ipv4"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "tun",
|
||||
"tag": "tun-in",
|
||||
"interface_name": "utun",
|
||||
"inet4_address": "172.19.0.1/30",
|
||||
"auto_route": true,
|
||||
"strict_route": true,
|
||||
"sniff": true,
|
||||
"sniff_override_destination": false
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
// 🔧 修复:添加 selector 组,包含真实和自动节点标签
|
||||
{
|
||||
"type": "selector",
|
||||
"tag": "proxy",
|
||||
"outbounds": allAvailableTags,
|
||||
"default": allAvailableTags.isNotEmpty ? allAvailableTags[0] : "direct",
|
||||
},
|
||||
// 🔧 修复:自动选择组排在真实节点前面
|
||||
...filteredAutoNodes,
|
||||
// 🔧 修复:真实节点排在后面
|
||||
...filteredRealNodes,
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"type": "block",
|
||||
"tag": "block"
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out"
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"rules": [
|
||||
{ "type": "dns", "outbound": "dns-out" }, // sing-box 内部解析交给 dns-out
|
||||
{ "type": "field", "port": 53, "network": "udp", "outbound": "direct" }, // 系统 UDP 53 直连
|
||||
{ "type": "field", "port": 53, "network": "tcp", "outbound": "direct" }, // 系统 TCP 53 直连
|
||||
{ "type": "field", "port": 853, "network": "tcp", "outbound": "direct" }, // 系统 TCP 853(Private DNS)直连
|
||||
{ "type": "field", "domain": ["dns.google", "cloudflare-dns.com", "one.one.one.one"], "outbound": "direct" } // 常见 DoH 域名直连,避免误走代理
|
||||
],
|
||||
"final": "proxy", // 🔧 修复:使用 selector 组作为默认出站
|
||||
"auto_detect_interface": true
|
||||
}
|
||||
};
|
||||
|
||||
final file = _file(kr_configName);
|
||||
final temp = _tempFile(kr_configName);
|
||||
final mapStr = jsonEncode(map);
|
||||
final mapStr = jsonEncode(fullConfig);
|
||||
|
||||
KRLogUtil.kr_i('📄 配置文件内容长度: ${mapStr.length}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📄 完整配置文件内容: $mapStr', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📄 完整配置文件长度: ${mapStr.length}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📄 Outbounds 总数: ${filteredAutoNodes.length + filteredRealNodes.length}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📄 Selector 可用标签数: ${allAvailableTags.length}', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📄 配置前800字符:\n${mapStr.substring(0, mapStr.length > 800 ? 800 : mapStr.length)}', tag: 'SingBox');
|
||||
|
||||
await file.writeAsString(mapStr);
|
||||
await temp.writeAsString(mapStr);
|
||||
@@ -1089,11 +1231,11 @@ class KRSingBoxImp {
|
||||
final oOption = SingboxConfigOption.fromJson(_getConfigOption());
|
||||
final changeResult = await kr_singBox.changeOptions(oOption).run();
|
||||
changeResult.match(
|
||||
(error) {
|
||||
(error) {
|
||||
KRLogUtil.kr_e('❌ changeOptions() 失败: $error', tag: 'SingBox');
|
||||
throw Exception('初始化 HiddifyOptions 失败: $error');
|
||||
},
|
||||
(_) {
|
||||
(_) {
|
||||
KRLogUtil.kr_i('✅ HiddifyOptions 初始化成功', tag: 'SingBox');
|
||||
},
|
||||
);
|
||||
@@ -1112,7 +1254,7 @@ class KRSingBoxImp {
|
||||
_kr_subscribeToStatus();
|
||||
|
||||
await kr_singBox.start(_cutPath, kr_configName, false).map(
|
||||
(r) {
|
||||
(r) {
|
||||
KRLogUtil.kr_i('✅ SingBox 启动成功', tag: 'SingBox');
|
||||
},
|
||||
).mapLeft((err) {
|
||||
@@ -1316,9 +1458,9 @@ class KRSingBoxImp {
|
||||
mode = KRCountryUtil.kr_getCurrentCountryCode();
|
||||
KRLogUtil.kr_i('🎯 切换到规则代理模式: $mode', tag: 'SingBox');
|
||||
break;
|
||||
// case KRConnectionType.direct:
|
||||
// mode = "direct";
|
||||
// break;
|
||||
// case KRConnectionType.direct:
|
||||
// mode = "direct";
|
||||
// break;
|
||||
}
|
||||
oOption["region"] = mode;
|
||||
KRLogUtil.kr_i('📝 更新 region 配置: $mode', tag: 'SingBox');
|
||||
@@ -1454,8 +1596,8 @@ class KRSingBoxImp {
|
||||
// 使用 then/catchError 避免异常导致 UI 阻塞
|
||||
kr_singBox.selectOutbound("select", tag).run().then((result) {
|
||||
result.match(
|
||||
(error) => KRLogUtil.kr_w('🔁 定时器重选节点失败: $error', tag: 'SingBox'),
|
||||
(_) => KRLogUtil.kr_d('🔁 定时器重选节点成功', tag: 'SingBox'),
|
||||
(error) => KRLogUtil.kr_w('🔁 定时器重选节点失败: $error', tag: 'SingBox'),
|
||||
(_) => KRLogUtil.kr_d('🔁 定时器重选节点成功', tag: 'SingBox'),
|
||||
);
|
||||
}).catchError((error) {
|
||||
KRLogUtil.kr_w('🔁 定时器重选节点异常: $error', tag: 'SingBox');
|
||||
@@ -1465,86 +1607,6 @@ class KRSingBoxImp {
|
||||
}
|
||||
}
|
||||
|
||||
/// 🌍 国家级节点选择
|
||||
/// 如果传入 'auto',则调用原有 kr_selectOutbound('auto')
|
||||
/// 否则根据国家名筛选该国节点,自动选择延迟最低的节点
|
||||
Future<void> kr_selectCountry(String country) async {
|
||||
KRLogUtil.kr_i('🌎 [v2.1] 开始选择国家: $country', tag: 'SingBox');
|
||||
|
||||
if (country == 'auto') {
|
||||
KRLogUtil.kr_i('🌀 国家为 auto,执行自动节点选择逻辑', tag: 'SingBox');
|
||||
await kr_selectOutbound('auto');
|
||||
return;
|
||||
}
|
||||
|
||||
if (kr_activeGroups.isEmpty) {
|
||||
KRLogUtil.kr_w('⚠️ kr_activeGroups 当前为空,无法进行国家选择', tag: 'SingBox');
|
||||
return;
|
||||
}
|
||||
|
||||
/*final selectGroup = kr_activeGroups.firstWhere(
|
||||
(group) => group.tag == 'select',
|
||||
orElse: () => throw Exception('未找到 "select" 组'),
|
||||
);
|
||||
|
||||
final countryNodes = selectGroup.items.where((item) {
|
||||
final nodeCountry = item.options?['country'] ?? '';
|
||||
return nodeCountry.toString().toLowerCase() == country.toLowerCase();
|
||||
}).toList();
|
||||
|
||||
if (countryNodes.isEmpty) {
|
||||
KRLogUtil.kr_w('⚠️ 未找到该国家的节点: $country', tag: 'SingBox');
|
||||
for (var item in selectGroup.items) {
|
||||
KRLogUtil.kr_d(
|
||||
' 可用节点: ${item.tag} (${item.options?['country'] ?? '未知'})',
|
||||
tag: 'SingBox',
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
KRLogUtil.kr_i('🇨🇭 找到 ${countryNodes.length} 个属于 "$country" 的节点', tag: 'SingBox');
|
||||
|
||||
// 按延迟排序(无延迟则 9999)
|
||||
countryNodes.sort((a, b) =>
|
||||
(a.urlTestDelay ?? 9999).compareTo(b.urlTestDelay ?? 9999));
|
||||
|
||||
final fastestNode = countryNodes.first;
|
||||
final selectedTag = fastestNode.tag;
|
||||
final delay = fastestNode.urlTestDelay ?? -1;
|
||||
|
||||
KRLogUtil.kr_i('✅ 选中该国家最快节点: $selectedTag (${delay}ms)', tag: 'SingBox');
|
||||
|
||||
try {
|
||||
await KRSecureStorage().kr_saveData(key: 'selected_country', value: country);
|
||||
await KRSecureStorage().kr_saveData(key: _keySelectedNode, value: selectedTag);
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('⚠️ 保存国家/节点信息失败: $e', tag: 'SingBox');
|
||||
}
|
||||
|
||||
try {
|
||||
await _kr_ensureCommandClientInitialized();
|
||||
await _kr_selectOutboundWithRetry('select', selectedTag,
|
||||
maxAttempts: 3, initialDelay: 100);
|
||||
KRLogUtil.kr_i('✅ 国家内节点切换成功: $selectedTag', tag: 'SingBox');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ 国家内节点选择失败: $e', tag: 'SingBox');
|
||||
rethrow;
|
||||
}
|
||||
|
||||
_nodeSelectionTimer?.cancel();
|
||||
KRLogUtil.kr_i('🔁 启动定时器防止节点被 auto 覆盖', tag: 'SingBox');
|
||||
_nodeSelectionTimer =
|
||||
Timer.periodic(const Duration(seconds: 20), (timer) async {
|
||||
try {
|
||||
await kr_singBox.selectOutbound('select', selectedTag).run();
|
||||
KRLogUtil.kr_d('🔁 定时确认国家内节点: $selectedTag', tag: 'SingBox');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('🔁 定时确认失败: $e', tag: 'SingBox');
|
||||
}
|
||||
});*/
|
||||
}
|
||||
|
||||
|
||||
/// 配合文件地址
|
||||
|
||||
@@ -1561,7 +1623,7 @@ class KRSingBoxImp {
|
||||
Future<void> kr_urlTest(String groupTag) async {
|
||||
KRLogUtil.kr_i('🧪 开始 URL 测试: $groupTag', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📊 当前活动组数量: ${kr_activeGroups.length}', tag: 'SingBox');
|
||||
|
||||
|
||||
// 打印所有活动组信息
|
||||
for (int i = 0; i < kr_activeGroups.length; i++) {
|
||||
final group = kr_activeGroups[i];
|
||||
@@ -1571,15 +1633,15 @@ class KRSingBoxImp {
|
||||
KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
KRLogUtil.kr_i('🚀 调用 SingBox URL 测试 API...', tag: 'SingBox');
|
||||
final result = await kr_singBox.urlTest(groupTag).run();
|
||||
KRLogUtil.kr_i('✅ URL 测试完成: $groupTag, 结果: $result', tag: 'SingBox');
|
||||
|
||||
|
||||
// 等待一段时间让 SingBox 完成测试
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
|
||||
// 再次检查活动组状态
|
||||
KRLogUtil.kr_i('🔄 测试后活动组状态检查:', tag: 'SingBox');
|
||||
for (int i = 0; i < kr_activeGroups.length; i++) {
|
||||
@@ -1595,4 +1657,221 @@ class KRSingBoxImp {
|
||||
KRLogUtil.kr_e('📚 错误详情: ${e.toString()}', tag: 'SingBox');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> kr_debugTunConnectivity() async {
|
||||
KRLogUtil.kr_i('🧪 [TUN Debug] 开始调试', tag: 'SingBoxTun');
|
||||
final statusType = kr_status.value.runtimeType.toString();
|
||||
KRLogUtil.kr_i('🔎 当前状态: $statusType', tag: 'SingBoxTun');
|
||||
|
||||
final selectedCountry = await KRSecureStorage().kr_readData(key: 'SELECTED_COUNTRY_TAG');
|
||||
final selectedNode = await KRSecureStorage().kr_readData(key: 'SELECTED_NODE_TAG');
|
||||
KRLogUtil.kr_i('🎯 选择: country=$selectedCountry, node=$selectedNode', tag: 'SingBox');
|
||||
|
||||
final activeTags = kr_activeGroups.map((g) => g.tag).join(', ');
|
||||
KRLogUtil.kr_i('🧩 分组: all=${kr_allGroups.length}, active=${kr_activeGroups.length} [$activeTags]', tag: 'SingBoxTun');
|
||||
|
||||
// 打印每个 active 分组的节点信息
|
||||
for (var group in kr_activeGroups) {
|
||||
KRLogUtil.kr_i(
|
||||
'📊 组[${group.tag}] 类型=${group.type} 节点数=${group.items.length}',
|
||||
tag: 'SingBoxTun');
|
||||
final selected = group.selected;
|
||||
KRLogUtil.kr_i('🎯 当前选中节点: $selected', tag: 'SingBoxTun');
|
||||
}
|
||||
|
||||
// 1️⃣ TCP 测试
|
||||
try {
|
||||
final sock =
|
||||
await Socket.connect('1.1.1.1', 443, timeout: const Duration(seconds: 3));
|
||||
sock.destroy();
|
||||
KRLogUtil.kr_i('🌐 TCP 443连接成功 (1.1.1.1:443)', tag: 'SingBoxTun');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('🌐 TCP 443连接失败: $e', tag: 'SingBoxTun');
|
||||
}
|
||||
|
||||
// 2️⃣ HTTPS 测试(Google Connectivity)
|
||||
try {
|
||||
final client = HttpClient()..connectionTimeout = const Duration(seconds: 3);
|
||||
final req = await client
|
||||
.getUrl(Uri.parse('https://connectivitycheck.gstatic.com/generate_204'));
|
||||
req.headers.add('User-Agent', 'kr-debug');
|
||||
final resp = await req.close();
|
||||
KRLogUtil.kr_i('🔐 HTTPS 204 状态: ${resp.statusCode}', tag: 'SingBoxTun');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('🔐 HTTPS 204 错误: $e', tag: 'SingBoxTun');
|
||||
}
|
||||
|
||||
// 3️⃣ 系统DNS解析
|
||||
try {
|
||||
final addrs = await InternetAddress.lookup('google.com');
|
||||
KRLogUtil.kr_i(
|
||||
'🧭 DNS解析成功: ${addrs.map((a) => a.address).join(", ")}',
|
||||
tag: 'SingBoxTun');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('🧭 DNS解析错误: $e', tag: 'SingBoxTun');
|
||||
}
|
||||
// 3️⃣ 系统DNS解析
|
||||
try {
|
||||
final addrs = await InternetAddress.lookup('1.1.1.1');
|
||||
KRLogUtil.kr_i(
|
||||
'🧭 DNS解析成功1.1.1.1: ${addrs.map((a) => a.address).join(", ")}',
|
||||
tag: 'SingBoxTun');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('🧭 DNS解析错误1.1.1.1: $e', tag: 'SingBoxTun');
|
||||
}
|
||||
|
||||
// 4️⃣ TCP 53测试
|
||||
try {
|
||||
final sock =
|
||||
await Socket.connect('8.8.8.8', 53, timeout: const Duration(seconds: 3));
|
||||
sock.destroy();
|
||||
KRLogUtil.kr_i('🛰️ TCP 53连接成功 (8.8.8.8:53)', tag: 'SingBoxTun');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('🛰️ TCP 53连接失败: $e', tag: 'SingBoxTun');
|
||||
}
|
||||
|
||||
// 5️⃣ 检查出口 IP
|
||||
try {
|
||||
final client = HttpClient()..connectionTimeout = const Duration(seconds: 3);
|
||||
final req = await client.getUrl(Uri.parse('https://api.ipify.org'));
|
||||
final resp = await req.close();
|
||||
final ip = await resp.transform(utf8.decoder).join();
|
||||
KRLogUtil.kr_i('🌍 出口IP检测成功: $ip', tag: 'SingBoxTun');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('🌍 出口IP检测失败: $e', tag: 'SingBoxTun');
|
||||
}
|
||||
|
||||
// 6️⃣ 网络类型(仅在移动端)
|
||||
try {
|
||||
final connectivity = await (Connectivity().checkConnectivity());
|
||||
KRLogUtil.kr_i('📶 当前网络类型: $connectivity', tag: 'SingBoxTun');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('📶 网络类型检测失败: $e', tag: 'SingBoxTun');
|
||||
}
|
||||
|
||||
// 7️⃣ HTTP延迟测试(快速验证)
|
||||
try {
|
||||
final sw = Stopwatch()..start();
|
||||
final client = HttpClient()..connectionTimeout = const Duration(seconds: 3);
|
||||
final req = await client
|
||||
.getUrl(Uri.parse('https://www.google.com/generate_204'));
|
||||
final resp = await req.close();
|
||||
sw.stop();
|
||||
KRLogUtil.kr_i('⏱️ HTTP延迟: ${sw.elapsedMilliseconds}ms (status=${resp.statusCode})',
|
||||
tag: 'SingBoxTun');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('⏱️ HTTP延迟测试失败: $e', tag: 'SingBoxTun');
|
||||
}
|
||||
|
||||
// 7️⃣ 检查出口国家信息(通过 geoip API)
|
||||
try {
|
||||
final client = HttpClient();
|
||||
final req = await client.getUrl(Uri.parse('https://ipapi.co/json/'));
|
||||
final resp = await req.close();
|
||||
final data = await resp.transform(utf8.decoder).join();
|
||||
KRLogUtil.kr_i('🌎 GeoIP: $data', tag: 'SingBoxTun');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('🌎 GeoIP检测失败: $e', tag: 'SingBoxTun');
|
||||
}
|
||||
|
||||
// 8️⃣ 简单测速(仅调试)
|
||||
final stopwatch = Stopwatch()..start();
|
||||
try {
|
||||
final client = HttpClient();
|
||||
final req = await client.getUrl(Uri.parse('https://speed.cloudflare.com/__down?bytes=1000000'));
|
||||
final resp = await req.close();
|
||||
final totalBytes = await resp.fold<int>(0, (prev, e) => prev + e.length);
|
||||
stopwatch.stop();
|
||||
KRLogUtil.kr_i('⚡ 下载测试: ${totalBytes ~/ 1024} KB in ${stopwatch.elapsedMilliseconds}ms', tag: 'SingBoxTun');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('⚡ 下载测试失败: $e', tag: 'SingBoxTun');
|
||||
}
|
||||
|
||||
KRLogUtil.kr_i('✅ [TUN Debug] 结束调试', tag: 'SingBoxTun');
|
||||
}
|
||||
|
||||
// 根据所选国家解析当前实际连接节点(适配 auto 选择)
|
||||
// 策略:
|
||||
// 1) 优先在 activeGroups 中查找 tag 与国家匹配的分组(完全匹配或包含关系)
|
||||
// 2) 使用该分组的 selected 字段在 items 中找到具体节点
|
||||
// 3) 若找不到,退化为在所有分组中查找其 items 是否包含与国家相关的节点,并返回其 selected 对应节点
|
||||
SingboxOutboundGroupItem? _kr_resolveCurrentNodeForCountry(String? selectedCountry) {
|
||||
if (selectedCountry == null || selectedCountry.isEmpty) return null;
|
||||
final countryLower = selectedCountry.toLowerCase();
|
||||
|
||||
// 1) 直接通过分组 tag 匹配国家
|
||||
for (final g in kr_activeGroups) {
|
||||
final tagLower = g.tag.toLowerCase();
|
||||
final tagMatch = tagLower == countryLower || tagLower.contains(countryLower);
|
||||
if (tagMatch) {
|
||||
final selTag = g.selected;
|
||||
for (final item in g.items) {
|
||||
if (item.tag == selTag) return item;
|
||||
}
|
||||
// 如果 selected 指向的不是本组的叶子,直接返回 null(调用方仍可查看组日志)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 在所有分组中尝试通过 items 与国家关联性匹配
|
||||
for (final g in kr_activeGroups) {
|
||||
final maybeCountryRelated = g.items.any(
|
||||
(i) => i.tag.toLowerCase().contains(countryLower));
|
||||
if (maybeCountryRelated) {
|
||||
final selTag = g.selected;
|
||||
for (final item in g.items) {
|
||||
if (item.tag == selTag) return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 启动自动切换调试订阅:实时打印 active-groups 的选中节点变化
|
||||
void kr_startAutoSwitchDebug() {
|
||||
// 这里不做重复订阅的严格检查,交由调用方控制多次调用
|
||||
final sub = kr_singBox.watchActiveGroups().listen((groups) {
|
||||
// 更新本地状态,供其他调试方法读取
|
||||
kr_activeGroups.value = groups;
|
||||
|
||||
for (final g in groups) {
|
||||
final selected = g.selected;
|
||||
final selItem = g.items.firstWhere(
|
||||
(i) => i.tag == selected,
|
||||
orElse: () => SingboxOutboundGroupItem(
|
||||
tag: '(未知)',
|
||||
type: ProxyType.unknown,
|
||||
urlTestDelay: -1,
|
||||
),
|
||||
);
|
||||
KRLogUtil.kr_i(
|
||||
'🔄 组[${g.tag}] 选中 -> ${selItem.tag} (${selItem.type.label}), delay=${selItem.urlTestDelay}ms',
|
||||
tag: 'SingBoxAuto',
|
||||
);
|
||||
}
|
||||
}, onError: (e) {
|
||||
KRLogUtil.kr_e('❌ AutoSwitch 调试订阅错误: $e', tag: 'SingBoxAuto');
|
||||
});
|
||||
_kr_subscriptions.add(sub);
|
||||
KRLogUtil.kr_i('✅ AutoSwitch 调试订阅已开启', tag: 'SingBox');
|
||||
}
|
||||
|
||||
/// 通过 DoH 进行 DNS 解析探针(走代理),便于判断 DNS 失败是否触发切换
|
||||
Future<void> kr_probeDoH(List<String> names) async {
|
||||
final client = HttpClient()..connectionTimeout = const Duration(seconds: 5);
|
||||
client.findProxy = (_) => kr_buildProxyRule();
|
||||
for (final name in names) {
|
||||
try {
|
||||
final uri = Uri.parse('https://dns.google/resolve?name=$name&type=A');
|
||||
final req = await client.getUrl(uri);
|
||||
final resp = await req.close();
|
||||
final body = await resp.transform(utf8.decoder).join();
|
||||
KRLogUtil.kr_i('🧪 DoH 解析 $name -> 状态 ${resp.statusCode}, 响应 $body', tag: 'SingBoxDoH');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('🧪 DoH 解析 $name 失败: $e', tag: 'SingBoxDoH');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,127 +1,151 @@
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||
import 'kr_log_util.dart';
|
||||
|
||||
/// 延迟测试工具类
|
||||
/// 提供真实的 TCP 连接延迟测试功能
|
||||
/// 真正的节点延迟测试工具
|
||||
class KRLatencyTester {
|
||||
/// 测试单个节点的延迟
|
||||
///
|
||||
/// 参数:
|
||||
/// - host: 主机地址
|
||||
/// - port: 端口号
|
||||
/// - timeout: 超时时间(毫秒)
|
||||
///
|
||||
/// 返回:
|
||||
/// - 延迟时间(毫秒),如果失败返回 65535
|
||||
static Future<int> testNode({
|
||||
/// TCP 连接测试延迟(真实测试)
|
||||
/// 返回延迟毫秒数,失败返回 65535
|
||||
static Future<int> testTcpLatency({
|
||||
required String host,
|
||||
required int port,
|
||||
int timeout = 5000,
|
||||
Duration timeout = const Duration(seconds: 5),
|
||||
}) async {
|
||||
Socket? socket;
|
||||
final stopwatch = Stopwatch();
|
||||
|
||||
try {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
final socket = await Socket.connect(
|
||||
KRLogUtil.kr_i('🔌 开始测试: $host:$port', tag: 'LatencyTester');
|
||||
|
||||
stopwatch.start();
|
||||
|
||||
// 尝试 TCP 连接
|
||||
socket = await Socket.connect(
|
||||
host,
|
||||
port,
|
||||
timeout: Duration(milliseconds: timeout),
|
||||
).timeout(Duration(milliseconds: timeout));
|
||||
|
||||
timeout: timeout,
|
||||
);
|
||||
|
||||
stopwatch.stop();
|
||||
|
||||
// 立即关闭连接
|
||||
await socket.close();
|
||||
socket.destroy();
|
||||
|
||||
|
||||
final latency = stopwatch.elapsedMilliseconds;
|
||||
KRLogUtil.kr_i('✅ 延迟测试成功: $host:$port = ${latency}ms', tag: 'KRLatencyTester');
|
||||
|
||||
|
||||
KRLogUtil.kr_i('✅ 测试成功: $host:$port - ${latency}ms', tag: 'LatencyTester');
|
||||
|
||||
return latency;
|
||||
|
||||
} on SocketException catch (e) {
|
||||
stopwatch.stop();
|
||||
KRLogUtil.kr_w('❌ 连接失败: $host:$port - ${e.message}', tag: 'LatencyTester');
|
||||
return 65535;
|
||||
|
||||
} on TimeoutException catch (e) {
|
||||
stopwatch.stop();
|
||||
KRLogUtil.kr_w('⏱️ 连接超时: $host:$port - $e', tag: 'LatencyTester');
|
||||
return 65535;
|
||||
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('❌ 延迟测试失败: $host:$port - $e', tag: 'KRLatencyTester');
|
||||
return 65535; // 测试失败返回最大值
|
||||
stopwatch.stop();
|
||||
KRLogUtil.kr_e('❌ 测试异常: $host:$port - $e', tag: 'LatencyTester');
|
||||
return 65535;
|
||||
|
||||
} finally {
|
||||
// 确保关闭连接
|
||||
try {
|
||||
await socket?.close();
|
||||
} catch (e) {
|
||||
// 忽略关闭错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 批量测试多个节点的延迟
|
||||
///
|
||||
/// 参数:
|
||||
/// - nodes: 节点列表,格式为 [{"host": "example.com", "port": 443}]
|
||||
/// - concurrency: 并发数量
|
||||
/// - timeout: 超时时间(毫秒)
|
||||
///
|
||||
/// 返回:
|
||||
/// - 测试结果映射,键为 "host:port",值为延迟时间
|
||||
/// 批量测试多个节点延迟(并发测试)
|
||||
/// 返回 Map<节点tag, 延迟ms>
|
||||
static Future<Map<String, int>> testMultipleNodes({
|
||||
required List<MapEntry<String, SocketAddress>> nodes,
|
||||
int concurrency = 10,
|
||||
int timeout = 5000,
|
||||
int concurrency = 10, // 并发数
|
||||
Duration timeout = const Duration(seconds: 5),
|
||||
}) async {
|
||||
final results = <String, int>{};
|
||||
final semaphore = Completer<void>();
|
||||
var activeCount = 0;
|
||||
var completedCount = 0;
|
||||
final List<List<MapEntry<String, SocketAddress>>> batches = [];
|
||||
|
||||
KRLogUtil.kr_i('🚀 开始批量延迟测试,共 ${nodes.length} 个节点,并发数: $concurrency', tag: 'KRLatencyTester');
|
||||
// 分批处理
|
||||
for (int i = 0; i < nodes.length; i += concurrency) {
|
||||
batches.add(
|
||||
nodes.sublist(i, i + concurrency > nodes.length ? nodes.length : i + concurrency)
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> processNode(MapEntry<String, SocketAddress> node) async {
|
||||
try {
|
||||
final host = node.value.address;
|
||||
final port = node.value.port;
|
||||
final key = node.key;
|
||||
|
||||
final latency = await testNode(
|
||||
host: host,
|
||||
port: port,
|
||||
KRLogUtil.kr_i('📊 开始批量测试: ${nodes.length} 个节点,分 ${batches.length} 批,每批 $concurrency 个', tag: 'LatencyTester');
|
||||
|
||||
int completedCount = 0;
|
||||
|
||||
// 逐批测试
|
||||
for (int batchIndex = 0; batchIndex < batches.length; batchIndex++) {
|
||||
final batch = batches[batchIndex];
|
||||
|
||||
KRLogUtil.kr_i('📦 测试第 ${batchIndex + 1}/${batches.length} 批(${batch.length} 个节点)', tag: 'LatencyTester');
|
||||
|
||||
// 并发测试当前批次
|
||||
final futures = batch.map((node) async {
|
||||
final tag = node.key;
|
||||
final address = node.value;
|
||||
|
||||
final latency = await testTcpLatency(
|
||||
host: address.host,
|
||||
port: address.port,
|
||||
timeout: timeout,
|
||||
);
|
||||
|
||||
results[key] = latency;
|
||||
} catch (e) {
|
||||
results[node.key] = 65535;
|
||||
KRLogUtil.kr_e('❌ 节点测试异常: ${node.key} - $e', tag: 'KRLatencyTester');
|
||||
} finally {
|
||||
|
||||
completedCount++;
|
||||
activeCount--;
|
||||
|
||||
if (completedCount >= nodes.length) {
|
||||
semaphore.complete();
|
||||
|
||||
if (completedCount % 5 == 0 || completedCount == nodes.length) {
|
||||
KRLogUtil.kr_i('📈 测试进度: $completedCount/${nodes.length}', tag: 'LatencyTester');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分批处理节点
|
||||
for (var i = 0; i < nodes.length; i += concurrency) {
|
||||
final batch = nodes.skip(i).take(concurrency);
|
||||
|
||||
for (final node in batch) {
|
||||
activeCount++;
|
||||
processNode(node);
|
||||
}
|
||||
|
||||
return MapEntry(tag, latency);
|
||||
}).toList();
|
||||
|
||||
// 等待当前批次完成
|
||||
if (i + concurrency < nodes.length) {
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
final batchResults = await Future.wait(futures);
|
||||
|
||||
// 收集结果
|
||||
for (final result in batchResults) {
|
||||
results[result.key] = result.value;
|
||||
}
|
||||
}
|
||||
|
||||
// 统计结果
|
||||
final successCount = results.values.where((latency) => latency < 65535).length;
|
||||
final failCount = results.length - successCount;
|
||||
|
||||
KRLogUtil.kr_i('✅ 批量测试完成', tag: 'LatencyTester');
|
||||
KRLogUtil.kr_i('📊 成功: $successCount, 失败: $failCount', tag: 'LatencyTester');
|
||||
|
||||
// 显示延迟最低的前3个
|
||||
final successNodes = results.entries
|
||||
.where((e) => e.value < 65535)
|
||||
.toList()
|
||||
..sort((a, b) => a.value.compareTo(b.value));
|
||||
|
||||
if (successNodes.isNotEmpty) {
|
||||
KRLogUtil.kr_i('🏆 延迟最低的前3个节点:', tag: 'LatencyTester');
|
||||
for (int i = 0; i < 3 && i < successNodes.length; i++) {
|
||||
KRLogUtil.kr_i(' ${i + 1}. ${successNodes[i].key}: ${successNodes[i].value}ms', tag: 'LatencyTester');
|
||||
}
|
||||
}
|
||||
|
||||
// 等待所有任务完成
|
||||
await semaphore.future;
|
||||
|
||||
KRLogUtil.kr_i('✅ 批量延迟测试完成,成功: ${results.length} 个', tag: 'KRLatencyTester');
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
/// Socket 地址类
|
||||
/// 表示网络地址和端口的组合
|
||||
/// 节点地址信息
|
||||
class SocketAddress {
|
||||
final String address;
|
||||
final String host;
|
||||
final int port;
|
||||
|
||||
SocketAddress(this.address, this.port);
|
||||
SocketAddress(this.host, this.port);
|
||||
|
||||
@override
|
||||
String toString() => '$address:$port';
|
||||
}
|
||||
String toString() => '$host:$port';
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ echo 正在复制必要的 DLL 文件...
|
||||
copy "%VC_REDIST_DIR%\msvcp140.dll" "%DLL_DIR%"
|
||||
copy "%VC_REDIST_DIR%\vcruntime140.dll" "%DLL_DIR%"
|
||||
copy "%VC_REDIST_DIR%\vcruntime140_1.dll" "%DLL_DIR%"
|
||||
copy "%~dp0..\libcore\libcore.dll" "%DLL_DIR%"
|
||||
|
||||
echo DLL 文件复制完成!
|
||||
pause
|
||||
pause
|
||||
Reference in New Issue
Block a user