Compare commits
9 Commits
dev
...
76e83e0135
| Author | SHA1 | Date | |
|---|---|---|---|
| 76e83e0135 | |||
| 89d6ec3c76 | |||
| 670eb7ebc9 | |||
| 46295f4543 | |||
| a47174df56 | |||
| 748ec6bee9 | |||
| a884e6c838 | |||
| 920b86e56a | |||
| a78b40c339 |
+3
-2
@@ -1,3 +1,4 @@
|
|||||||
[submodule "libcore"]
|
[submodule "libcore"]
|
||||||
path = libcore
|
path = libcore
|
||||||
url = https://github.com/hiddify/hiddify-next-core
|
url = https://github.com/hiddify/hiddify-next-core
|
||||||
|
branch = main
|
||||||
|
|||||||
@@ -105,8 +105,8 @@ android {
|
|||||||
debugSymbolLevel 'FULL'
|
debugSymbolLevel 'FULL'
|
||||||
}
|
}
|
||||||
// 禁用代码混淆和资源压缩,解决VPN连接问题
|
// 禁用代码混淆和资源压缩,解决VPN连接问题
|
||||||
minifyEnabled true
|
minifyEnabled false
|
||||||
shrinkResources true
|
shrinkResources false
|
||||||
// proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
// proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,7 +193,7 @@
|
|||||||
"rewardDetails": "Reward Details >",
|
"rewardDetails": "Reward Details >",
|
||||||
"steps": "Invitation Steps",
|
"steps": "Invitation Steps",
|
||||||
"inviteFriend": "Invite Friends",
|
"inviteFriend": "Invite Friends",
|
||||||
"acceptInvite": "Friends accept invitation\nPlace order and register",
|
"acceptInvite": "Friends accept invitationPlace order and register",
|
||||||
"getReward": "Get Reward",
|
"getReward": "Get Reward",
|
||||||
"shareLink": "Share Link",
|
"shareLink": "Share Link",
|
||||||
"shareQR": "Share QR Code",
|
"shareQR": "Share QR Code",
|
||||||
@@ -398,7 +398,7 @@
|
|||||||
"content": "Would you like to update now?",
|
"content": "Would you like to update now?",
|
||||||
"updateNow": "Update Now",
|
"updateNow": "Update Now",
|
||||||
"updateLater": "Later",
|
"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": {
|
"country": {
|
||||||
"cn": "China",
|
"cn": "China",
|
||||||
|
|||||||
@@ -256,7 +256,7 @@
|
|||||||
"rewardDetails": "Detalles de recompensa >",
|
"rewardDetails": "Detalles de recompensa >",
|
||||||
"steps": "Pasos de invitación",
|
"steps": "Pasos de invitación",
|
||||||
"inviteFriend": "Invitar amigo",
|
"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",
|
"getReward": "Obtener recompensa",
|
||||||
"shareLink": "Compartir por enlace",
|
"shareLink": "Compartir por enlace",
|
||||||
"shareQR": "Compartir por código QR",
|
"shareQR": "Compartir por código QR",
|
||||||
@@ -404,7 +404,7 @@
|
|||||||
"content": "¿Actualizar ahora?",
|
"content": "¿Actualizar ahora?",
|
||||||
"updateNow": "Actualizar ahora",
|
"updateNow": "Actualizar ahora",
|
||||||
"updateLater": "Más tarde",
|
"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": {
|
"kr_invite": {
|
||||||
"close": "Cerrar",
|
"close": "Cerrar",
|
||||||
|
|||||||
@@ -193,7 +193,7 @@
|
|||||||
"rewardDetails": "Tasu üksikasjad >",
|
"rewardDetails": "Tasu üksikasjad >",
|
||||||
"steps": "Kutse Sammud",
|
"steps": "Kutse Sammud",
|
||||||
"inviteFriend": "Kutsu Sõbrad",
|
"inviteFriend": "Kutsu Sõbrad",
|
||||||
"acceptInvite": "Sõbrad aktsepteerivad kutset\nTee tellimus ja registreeru",
|
"acceptInvite": "Sõbrad aktsepteerivad kutsetTee tellimus ja registreeru",
|
||||||
"getReward": "Saada Tasu",
|
"getReward": "Saada Tasu",
|
||||||
"shareLink": "Jaga Linki",
|
"shareLink": "Jaga Linki",
|
||||||
"shareQR": "Jaga QR-koodi",
|
"shareQR": "Jaga QR-koodi",
|
||||||
@@ -395,7 +395,7 @@
|
|||||||
"content": "Uuendada nüüd?",
|
"content": "Uuendada nüüd?",
|
||||||
"updateNow": "Uuenda nüüd",
|
"updateNow": "Uuenda nüüd",
|
||||||
"updateLater": "Hiljem",
|
"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": {
|
"country": {
|
||||||
"cn": "Hiina",
|
"cn": "Hiina",
|
||||||
|
|||||||
@@ -209,7 +209,7 @@
|
|||||||
"rewardDetails": "報酬の詳細 >",
|
"rewardDetails": "報酬の詳細 >",
|
||||||
"steps": "招待の手順",
|
"steps": "招待の手順",
|
||||||
"inviteFriend": "友達を招待",
|
"inviteFriend": "友達を招待",
|
||||||
"acceptInvite": "友達が招待を受け入れ\n注文して登録",
|
"acceptInvite": "友達が招待を受け入れ注文して登録",
|
||||||
"getReward": "報酬を獲得",
|
"getReward": "報酬を獲得",
|
||||||
"shareLink": "リンクを共有",
|
"shareLink": "リンクを共有",
|
||||||
"shareQR": "QRコードを共有",
|
"shareQR": "QRコードを共有",
|
||||||
@@ -412,7 +412,7 @@
|
|||||||
"content": "今すぐアップデートしますか?",
|
"content": "今すぐアップデートしますか?",
|
||||||
"updateNow": "今すぐアップデート",
|
"updateNow": "今すぐアップデート",
|
||||||
"updateLater": "後で",
|
"updateLater": "後で",
|
||||||
"defaultContent": "1. アプリのパフォーマンス最適化\n2. 既知の問題の修正\n3. ユーザー体験の向上"
|
"defaultContent": "1. アプリのパフォーマンス最適化2. 既知の問題の修正3. ユーザー体験の向上"
|
||||||
},
|
},
|
||||||
"country": {
|
"country": {
|
||||||
"cn": "中国",
|
"cn": "中国",
|
||||||
|
|||||||
@@ -387,7 +387,7 @@
|
|||||||
"content": "Хотите обновить сейчас?",
|
"content": "Хотите обновить сейчас?",
|
||||||
"updateNow": "Обновить сейчас",
|
"updateNow": "Обновить сейчас",
|
||||||
"later": "Позже",
|
"later": "Позже",
|
||||||
"defaultContent": "1. Оптимизация производительности приложения\n2. Исправление известных проблем\n3. Улучшение пользовательского опыта"
|
"defaultContent": "1. Оптимизация производительности приложения2. Исправление известных проблем3. Улучшение пользовательского опыта"
|
||||||
},
|
},
|
||||||
"country": {
|
"country": {
|
||||||
"cn": "Китай",
|
"cn": "Китай",
|
||||||
|
|||||||
@@ -232,13 +232,13 @@
|
|||||||
"longestConnection": "最长连接时间",
|
"longestConnection": "最长连接时间",
|
||||||
"days": "{days}天",
|
"days": "{days}天",
|
||||||
"daysOfWeek": {
|
"daysOfWeek": {
|
||||||
"monday": "周\n一",
|
"monday": "周一",
|
||||||
"tuesday": "周\n二",
|
"tuesday": "周二",
|
||||||
"wednesday": "周\n三",
|
"wednesday": "周三",
|
||||||
"thursday": "周\n四",
|
"thursday": "周四",
|
||||||
"friday": "周\n五",
|
"friday": "周五",
|
||||||
"saturday": "周\n六",
|
"saturday": "周六",
|
||||||
"sunday": "周\n日"
|
"sunday": "周日"
|
||||||
},
|
},
|
||||||
"processTrafficFailed": "处理流量日志数据失败"
|
"processTrafficFailed": "处理流量日志数据失败"
|
||||||
},
|
},
|
||||||
@@ -256,7 +256,7 @@
|
|||||||
"rewardDetails": "奖励明细 >",
|
"rewardDetails": "奖励明细 >",
|
||||||
"steps": "邀请步骤",
|
"steps": "邀请步骤",
|
||||||
"inviteFriend": "邀请好友",
|
"inviteFriend": "邀请好友",
|
||||||
"acceptInvite": "好友接受邀请\n下单并注册",
|
"acceptInvite": "好友接受邀请下单并注册",
|
||||||
"getReward": "获得奖励",
|
"getReward": "获得奖励",
|
||||||
"shareLink": "分享链接",
|
"shareLink": "分享链接",
|
||||||
"shareQR": "分享二维码",
|
"shareQR": "分享二维码",
|
||||||
@@ -415,7 +415,7 @@
|
|||||||
"content": "是否立即更新?",
|
"content": "是否立即更新?",
|
||||||
"updateNow": "立即更新",
|
"updateNow": "立即更新",
|
||||||
"updateLater": "稍后再说",
|
"updateLater": "稍后再说",
|
||||||
"defaultContent": "1. 优化应用性能\n2. 修复已知问题\n3. 提升用户体验"
|
"defaultContent": "1. 优化应用性能2. 修复已知问题3. 提升用户体验"
|
||||||
},
|
},
|
||||||
"country": {
|
"country": {
|
||||||
"cn": "中国",
|
"cn": "中国",
|
||||||
|
|||||||
@@ -170,13 +170,13 @@
|
|||||||
"longestConnection": "最長連接時間",
|
"longestConnection": "最長連接時間",
|
||||||
"days": "{days}天",
|
"days": "{days}天",
|
||||||
"daysOfWeek": {
|
"daysOfWeek": {
|
||||||
"monday": "週\n一",
|
"monday": "週一",
|
||||||
"tuesday": "週\n二",
|
"tuesday": "週二",
|
||||||
"wednesday": "週\n三",
|
"wednesday": "週三",
|
||||||
"thursday": "週\n四",
|
"thursday": "週四",
|
||||||
"friday": "週\n五",
|
"friday": "週五",
|
||||||
"saturday": "週\n六",
|
"saturday": "週六",
|
||||||
"sunday": "週\n日"
|
"sunday": "週日"
|
||||||
},
|
},
|
||||||
"processTrafficFailed": "處理流量日誌資料失敗"
|
"processTrafficFailed": "處理流量日誌資料失敗"
|
||||||
},
|
},
|
||||||
@@ -194,7 +194,7 @@
|
|||||||
"rewardDetails": "獎勵明細 >",
|
"rewardDetails": "獎勵明細 >",
|
||||||
"steps": "邀請步驟",
|
"steps": "邀請步驟",
|
||||||
"inviteFriend": "邀請好友",
|
"inviteFriend": "邀請好友",
|
||||||
"acceptInvite": "好友接受邀請\n下單並註冊",
|
"acceptInvite": "好友接受邀請下單並註冊",
|
||||||
"getReward": "獲得獎勵",
|
"getReward": "獲得獎勵",
|
||||||
"shareLink": "分享連結",
|
"shareLink": "分享連結",
|
||||||
"shareQR": "分享二維碼",
|
"shareQR": "分享二維碼",
|
||||||
@@ -332,7 +332,7 @@
|
|||||||
"content": "是否立即更新?",
|
"content": "是否立即更新?",
|
||||||
"updateNow": "立即更新",
|
"updateNow": "立即更新",
|
||||||
"updateLater": "稍後",
|
"updateLater": "稍後",
|
||||||
"defaultContent": "1. 優化應用性能\n2. 修復已知問題\n3. 改進用戶體驗"
|
"defaultContent": "1. 優化應用性能2. 修復已知問題3. 改進用戶體驗"
|
||||||
},
|
},
|
||||||
"orderStatus": {
|
"orderStatus": {
|
||||||
"title": "訂單狀態",
|
"title": "訂單狀態",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
name: BearVPN
|
name: BearVPN
|
||||||
|
app_name: BearVPN
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
build_number: 1
|
build_number: 1
|
||||||
targets:
|
targets:
|
||||||
@@ -11,3 +12,7 @@ targets:
|
|||||||
enable: true
|
enable: true
|
||||||
# 不签名
|
# 不签名
|
||||||
sign: false
|
sign: false
|
||||||
|
windows:
|
||||||
|
exe:
|
||||||
|
enable: true
|
||||||
|
icon: assets/images/tray_icon.ico
|
||||||
@@ -28,14 +28,12 @@ class KROutboundItem {
|
|||||||
|
|
||||||
/// URL
|
/// URL
|
||||||
String url = "";
|
String url = "";
|
||||||
@override
|
// ✅ 1. 将传入的 nodeListItem 保存为类的 final 成员变量
|
||||||
String toString() {
|
final KrNodeListItem nodeListItem;
|
||||||
return 'KROutboundItem(tag: $tag, country: $country, delay: ${urlTestDelay.value}ms)';
|
|
||||||
}
|
|
||||||
/// 服务器类型
|
/// 服务器类型
|
||||||
|
|
||||||
/// 构造函数,接受 KrNodeListItem 对象并初始化 KROutboundItem
|
/// 构造函数,接受 KrItem 对象并初始化 KROutboundItem
|
||||||
KROutboundItem(KrNodeListItem nodeListItem) {
|
KROutboundItem(this.nodeListItem) {
|
||||||
id = nodeListItem.id.toString();
|
id = nodeListItem.id.toString();
|
||||||
protocol = nodeListItem.protocol;
|
protocol = nodeListItem.protocol;
|
||||||
latitude = nodeListItem.latitude;
|
latitude = nodeListItem.latitude;
|
||||||
@@ -152,7 +150,7 @@ class KROutboundItem {
|
|||||||
break;
|
break;
|
||||||
case "hysteria":
|
case "hysteria":
|
||||||
case "hysteria2":
|
case "hysteria2":
|
||||||
// 后端的 "hysteria" 实际上是 Hysteria2 协议
|
// 后端的 "hysteria" 实际上是 Hysteria2 协议
|
||||||
final securityConfig =
|
final securityConfig =
|
||||||
json["security_config"] as Map<String, dynamic>? ?? {};
|
json["security_config"] as Map<String, dynamic>? ?? {};
|
||||||
config = {
|
config = {
|
||||||
@@ -214,6 +212,11 @@ class KROutboundItem {
|
|||||||
// 解析配置
|
// 解析配置
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
// 现在它可以正确地访问已保存的 nodeListItem 成员
|
||||||
|
return 'KROutboundItem(name: ${nodeListItem.name}, protocol: ${nodeListItem.protocol}, server: ${nodeListItem.serverAddr}, port: ${nodeListItem.port})';
|
||||||
|
}
|
||||||
/// 构建传输配置
|
/// 构建传输配置
|
||||||
Map<String, dynamic> _buildTransport(Map<String, dynamic> json) {
|
Map<String, dynamic> _buildTransport(Map<String, dynamic> json) {
|
||||||
final transportType = json["transport"] as String?;
|
final transportType = json["transport"] as String?;
|
||||||
@@ -274,6 +277,7 @@ class KROutboundItem {
|
|||||||
// 🔧 尝试从 config 字段解析 transport 配置
|
// 🔧 尝试从 config 字段解析 transport 配置
|
||||||
Map<String, dynamic>? transportConfig;
|
Map<String, dynamic>? transportConfig;
|
||||||
Map<String, dynamic>? securityConfig;
|
Map<String, dynamic>? securityConfig;
|
||||||
|
int actualPort = nodeListItem.port; // 🔧 使用局部变量存储实际端口
|
||||||
|
|
||||||
// 🔧 关键修复:优先从 protocols 字段解析配置
|
// 🔧 关键修复:优先从 protocols 字段解析配置
|
||||||
if (nodeListItem.protocols.isNotEmpty) {
|
if (nodeListItem.protocols.isNotEmpty) {
|
||||||
@@ -307,6 +311,28 @@ class KROutboundItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (matchedProtocol != null) {
|
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 配置
|
// 提取 transport 配置
|
||||||
if (matchedProtocol['network'] != null || matchedProtocol['transport'] != null) {
|
if (matchedProtocol['network'] != null || matchedProtocol['transport'] != null) {
|
||||||
final network = matchedProtocol['network'] ?? matchedProtocol['transport'];
|
final network = matchedProtocol['network'] ?? matchedProtocol['transport'];
|
||||||
@@ -402,7 +428,7 @@ class KROutboundItem {
|
|||||||
|
|
||||||
switch (nodeListItem.protocol) {
|
switch (nodeListItem.protocol) {
|
||||||
case "shadowsocks":
|
case "shadowsocks":
|
||||||
// 优先使用 protocols 解析出来的 cipher,其次是 method 字段,最后才是默认值
|
// 优先使用 protocols 解析出来的 cipher,其次是 method 字段,最后才是默认值
|
||||||
String finalMethod = nodeListItem.method.isNotEmpty
|
String finalMethod = nodeListItem.method.isNotEmpty
|
||||||
? nodeListItem.method
|
? nodeListItem.method
|
||||||
: "2022-blake3-aes-256-gcm";
|
: "2022-blake3-aes-256-gcm";
|
||||||
@@ -411,22 +437,21 @@ class KROutboundItem {
|
|||||||
"type": "shadowsocks",
|
"type": "shadowsocks",
|
||||||
"tag": nodeListItem.name,
|
"tag": nodeListItem.name,
|
||||||
"server": nodeListItem.serverAddr,
|
"server": nodeListItem.serverAddr,
|
||||||
"server_port": nodeListItem.port,
|
"server_port": actualPort,
|
||||||
"method": finalMethod,
|
"method": finalMethod,
|
||||||
"password": nodeListItem.uuid
|
"password": nodeListItem.uuid
|
||||||
};
|
};
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
|
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
print('✅ Shadowsocks 节点配置构建成功: ${nodeListItem.name}');
|
print('✅ Shadowsocks 节点配置构建成功: ${nodeListItem.name}');
|
||||||
}
|
|
||||||
if (kDebugMode) {
|
|
||||||
print('📄 使用加密方法: $finalMethod');
|
print('📄 使用加密方法: $finalMethod');
|
||||||
}
|
print('📄 完整配置 JSON:');
|
||||||
if (kDebugMode) {
|
print(jsonEncode(config));
|
||||||
print('📄 完整配置: $config');
|
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "vless":
|
case "vless":
|
||||||
// 判断是否为域名(非IP地址)
|
// 判断是否为域名(非IP地址)
|
||||||
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||||
.hasMatch(nodeListItem.serverAddr);
|
.hasMatch(nodeListItem.serverAddr);
|
||||||
|
|
||||||
@@ -436,17 +461,25 @@ class KROutboundItem {
|
|||||||
serverName = securityConfig['sni'].toString();
|
serverName = securityConfig['sni'].toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔧 关键修复:根据 security_config 判断是否启用 TLS
|
// 🔧 关键修复:智能判断是否启用 TLS
|
||||||
final bool vlessTlsEnabled = securityConfig?['tls_enabled'] ?? false;
|
// 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) {
|
if (kDebugMode) {
|
||||||
print('🔐 VLESS TLS 状态: enabled=$vlessTlsEnabled');
|
print('🔐 VLESS TLS 状态: enabled=$vlessTlsEnabled (port=${nodeListItem.port}, isDomain=$isDomain)');
|
||||||
}
|
}
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"type": "vless",
|
"type": "vless",
|
||||||
"tag": nodeListItem.name,
|
"tag": nodeListItem.name,
|
||||||
"server": nodeListItem.serverAddr,
|
"server": nodeListItem.serverAddr,
|
||||||
"server_port": nodeListItem.port,
|
"server_port": actualPort,
|
||||||
"uuid": nodeListItem.uuid,
|
"uuid": nodeListItem.uuid,
|
||||||
if (transportConfig != null) "transport": transportConfig,
|
if (transportConfig != null) "transport": transportConfig,
|
||||||
if (vlessTlsEnabled) "tls": {
|
if (vlessTlsEnabled) "tls": {
|
||||||
@@ -460,14 +493,27 @@ class KROutboundItem {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
|
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
print('✅ VLESS 节点配置构建成功: ${nodeListItem.name}');
|
print('✅ VLESS 节点配置构建成功: ${nodeListItem.name}');
|
||||||
}
|
print('📋 原始节点信息:');
|
||||||
if (kDebugMode) {
|
print(' - serverAddr: ${nodeListItem.serverAddr}');
|
||||||
print('📄 完整配置: $config');
|
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;
|
break;
|
||||||
case "vmess":
|
case "vmess":
|
||||||
// 判断是否为域名(非IP地址)
|
// 判断是否为域名(非IP地址)
|
||||||
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||||
.hasMatch(nodeListItem.serverAddr);
|
.hasMatch(nodeListItem.serverAddr);
|
||||||
|
|
||||||
@@ -477,17 +523,25 @@ class KROutboundItem {
|
|||||||
serverName = securityConfig['sni'].toString();
|
serverName = securityConfig['sni'].toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔧 关键修复:根据 security_config 判断是否启用 TLS
|
// 🔧 关键修复:智能判断是否启用 TLS
|
||||||
final bool tlsEnabled = securityConfig?['tls_enabled'] ?? false;
|
// 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) {
|
if (kDebugMode) {
|
||||||
print('🔐 TLS 状态: enabled=$tlsEnabled');
|
print('🔐 VMess TLS 状态: enabled=$tlsEnabled (port=${nodeListItem.port}, isDomain=$isDomain)');
|
||||||
}
|
}
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"type": "vmess",
|
"type": "vmess",
|
||||||
"tag": nodeListItem.name,
|
"tag": nodeListItem.name,
|
||||||
"server": nodeListItem.serverAddr,
|
"server": nodeListItem.serverAddr,
|
||||||
"server_port": nodeListItem.port,
|
"server_port": actualPort,
|
||||||
"uuid": nodeListItem.uuid,
|
"uuid": nodeListItem.uuid,
|
||||||
"alter_id": 0,
|
"alter_id": 0,
|
||||||
"security": "auto",
|
"security": "auto",
|
||||||
@@ -503,14 +557,27 @@ class KROutboundItem {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
|
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
print('✅ VMess 节点配置构建成功: ${nodeListItem.name}');
|
print('✅ VMess 节点配置构建成功: ${nodeListItem.name}');
|
||||||
}
|
print('📋 原始节点信息:');
|
||||||
if (kDebugMode) {
|
print(' - serverAddr: ${nodeListItem.serverAddr}');
|
||||||
print('📄 完整配置: $config');
|
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;
|
break;
|
||||||
case "trojan":
|
case "trojan":
|
||||||
// 判断是否为域名(非IP地址)
|
// 判断是否为域名(非IP地址)
|
||||||
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||||
.hasMatch(nodeListItem.serverAddr);
|
.hasMatch(nodeListItem.serverAddr);
|
||||||
|
|
||||||
@@ -524,7 +591,7 @@ class KROutboundItem {
|
|||||||
"type": "trojan",
|
"type": "trojan",
|
||||||
"tag": nodeListItem.name,
|
"tag": nodeListItem.name,
|
||||||
"server": nodeListItem.serverAddr,
|
"server": nodeListItem.serverAddr,
|
||||||
"server_port": nodeListItem.port,
|
"server_port": actualPort,
|
||||||
"password": nodeListItem.uuid,
|
"password": nodeListItem.uuid,
|
||||||
if (transportConfig != null) "transport": transportConfig,
|
if (transportConfig != null) "transport": transportConfig,
|
||||||
"tls": {
|
"tls": {
|
||||||
@@ -546,7 +613,7 @@ class KROutboundItem {
|
|||||||
break;
|
break;
|
||||||
case "hysteria":
|
case "hysteria":
|
||||||
case "hysteria2":
|
case "hysteria2":
|
||||||
// 后端的 "hysteria" 实际上是 Hysteria2 协议
|
// 后端的 "hysteria" 实际上是 Hysteria2 协议
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print('🔍 构建 Hysteria2 节点: ${nodeListItem.name}');
|
print('🔍 构建 Hysteria2 节点: ${nodeListItem.name}');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'dart:convert';
|
|||||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||||
|
|
||||||
class KRNodeList {
|
class KRNodeList {
|
||||||
final List<KrNodeListItem> list;
|
final List<KrNodeListItem> list;
|
||||||
final String subscribeId;
|
final String subscribeId;
|
||||||
final String startTime;
|
final String startTime;
|
||||||
final String expireTime;
|
final String expireTime;
|
||||||
@@ -35,8 +35,8 @@ class KRNodeList {
|
|||||||
// 查找 id 匹配的订阅项
|
// 查找 id 匹配的订阅项
|
||||||
try {
|
try {
|
||||||
subscribeData = listData.firstWhere(
|
subscribeData = listData.firstWhere(
|
||||||
(item) => (item as Map<String, dynamic>)['id']?.toString() == requestSubscribeId,
|
(item) => (item as Map<String, dynamic>)['id']?.toString() == requestSubscribeId,
|
||||||
orElse: () => listData[0] as Map<String, dynamic>
|
orElse: () => listData[0] as Map<String, dynamic>
|
||||||
) as Map<String, dynamic>;
|
) as Map<String, dynamic>;
|
||||||
KRLogUtil.kr_i('✅ 找到匹配的订阅项: id=$requestSubscribeId', tag: 'NodeList');
|
KRLogUtil.kr_i('✅ 找到匹配的订阅项: id=$requestSubscribeId', tag: 'NodeList');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -70,7 +70,7 @@ class KRNodeList {
|
|||||||
|
|
||||||
class KrNodeListItem {
|
class KrNodeListItem {
|
||||||
final int id;
|
final int id;
|
||||||
String name;
|
String name;
|
||||||
final String uuid;
|
final String uuid;
|
||||||
final String protocol;
|
final String protocol;
|
||||||
final String relayMode;
|
final String relayMode;
|
||||||
@@ -136,25 +136,68 @@ class KrNodeListItem {
|
|||||||
String method = json['method']?.toString() ?? ''; // 加密方法(Shadowsocks等)
|
String method = json['method']?.toString() ?? ''; // 加密方法(Shadowsocks等)
|
||||||
final protocols = json['protocols']?.toString() ?? ''; // 协议配置JSON
|
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) {
|
if (protocols.isNotEmpty) {
|
||||||
try {
|
try {
|
||||||
final protocolsList = jsonDecode(protocols) as List;
|
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) {
|
if (protocolsList.isNotEmpty) {
|
||||||
final firstProtocol = protocolsList[0] as Map<String, dynamic>;
|
// 🔧 修复:查找与当前协议类型匹配的配置,而不是直接使用第一个
|
||||||
// 优先使用 protocols 中的配置
|
Map<String, dynamic>? matchedProtocolConfig;
|
||||||
if (firstProtocol['port'] != null) {
|
|
||||||
port = _parseIntSafely(firstProtocol['port']);
|
// 尝试找到协议类型匹配的配置
|
||||||
|
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) {
|
} catch (e) {
|
||||||
KRLogUtil.kr_w('解析 protocols 字段失败: $e', tag: 'NodeList');
|
KRLogUtil.kr_w('⚠️ 解析 protocols 字段失败: $e', tag: 'NodeList');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'NodeList');
|
||||||
|
|
||||||
return KrNodeListItem(
|
return KrNodeListItem(
|
||||||
id: _parseIntSafely(json['id']),
|
id: _parseIntSafely(json['id']),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// hi_node_list_view.dart
|
// hi_node_list_view.dart
|
||||||
|
|
||||||
import 'dart:math';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
@@ -48,17 +47,9 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
) {
|
) {
|
||||||
if (outboundList.isEmpty) return 0;
|
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));
|
outboundList.sort((a, b) => a.urlTestDelay.value.compareTo(b.urlTestDelay.value));
|
||||||
return validNodes.first.urlTestDelay.value;
|
return outboundList.first.urlTestDelay.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 找出延迟最低(最快)的节点对象
|
/// 找出延迟最低(最快)的节点对象
|
||||||
@@ -88,8 +79,8 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
// 并设置透明背景,让父组件的背景可以透出来
|
// 并设置透明背景,让父组件的背景可以透出来
|
||||||
return Material(
|
return Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: _buildSubscribeList(context)
|
child: _buildSubscribeList(context)
|
||||||
// child: _kr_buildRegionList(context)
|
// child: _kr_buildRegionList(context)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,9 +190,20 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
),
|
),
|
||||||
...controller.kr_subscribeService.countryOutboundList.map((country) {
|
...controller.kr_subscribeService.countryOutboundList.map((country) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: () async {
|
||||||
// 自动选择这个国家下的节点延迟中最快的
|
try {
|
||||||
controller.homeController.onCountrySelected(country.country);
|
|
||||||
|
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),
|
child: _kr_buildCountryListItem(context, country: country),
|
||||||
);
|
);
|
||||||
@@ -233,10 +235,19 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
_buildEmptyListPlaceholder(context, AppTranslations.kr_home.noNodes)
|
_buildEmptyListPlaceholder(context, AppTranslations.kr_home.noNodes)
|
||||||
else ...[
|
else ...[
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () {
|
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
|
||||||
controller.homeController.kr_selectNode('auto');
|
onTap: () async {
|
||||||
controller.homeController.kr_currentListStatus.value =
|
try {
|
||||||
KRHomeViewsListStatus.kr_none;
|
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(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -310,7 +321,23 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
),
|
),
|
||||||
...controller.kr_subscribeService.allList.map((item) {
|
...controller.kr_subscribeService.allList.map((item) {
|
||||||
return InkWell(
|
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),
|
child: _kr_buildNodeListItem(context, item: item),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
@@ -344,15 +371,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
|
/// 构建单个节点列表项的UI
|
||||||
Widget _kr_buildNodeListItem(BuildContext context, {required KROutboundItem item}) {
|
Widget _kr_buildNodeListItem(BuildContext context, {required KROutboundItem item}) {
|
||||||
return Container(
|
return Container(
|
||||||
@@ -379,7 +397,7 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
controller.homeController.kr_getCountryFullName(item.country),
|
'${controller.homeController.kr_getCountryFullName(item.country)}-${item.tag}',
|
||||||
style: KrAppTextStyle(fontSize: 14, color: Colors.white),
|
style: KrAppTextStyle(fontSize: 14, color: Colors.white),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
@@ -403,13 +421,36 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
),
|
),
|
||||||
SizedBox(height: 2.w),
|
SizedBox(height: 2.w),
|
||||||
Obx(() {
|
Obx(() {
|
||||||
// 2. 获取用于显示的延迟值
|
// 1. 获取延迟值和测速状态
|
||||||
final int delay = _getDisplayDelay(controller, item);
|
int displayDelay = item.urlTestDelay.value;
|
||||||
|
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(
|
return Text(
|
||||||
'${delay}ms',
|
delayText,
|
||||||
style: KrAppTextStyle(
|
style: KrAppTextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: krModernGreen,
|
color: delayColor, // 使用动态计算出的颜色
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -441,18 +482,7 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
|
|
||||||
/// 构建国家列表项的UI
|
/// 构建国家列表项的UI
|
||||||
Widget _kr_buildCountryListItem(BuildContext context, {required country}) {
|
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(
|
return Container(
|
||||||
key: ValueKey(country),
|
key: ValueKey(country),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -476,22 +506,42 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Obx(() {
|
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(
|
return Text(
|
||||||
delay == 0
|
delayText,
|
||||||
? ''
|
|
||||||
: delay >= 3000
|
|
||||||
? AppTranslations.kr_home.timeout
|
|
||||||
: '${delay}ms',
|
|
||||||
style: KrAppTextStyle(
|
style: KrAppTextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: getLatencyColor(delay),
|
color: delayColor, // 使用动态计算出的颜色
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
SizedBox(width: 12.w),
|
SizedBox(width: 12.w),
|
||||||
Obx(() => controller.homeController.kr_coutryText.value == country.country
|
Obx(() => controller.homeController.kr_cutTag.value == '${country.country}-auto'
|
||||||
? KrLocalImage(
|
? KrLocalImage(
|
||||||
imageName: 'radio-active-icon',
|
imageName: 'radio-active-icon',
|
||||||
imageType: ImageType.svg,
|
imageType: ImageType.svg,
|
||||||
|
|||||||
@@ -69,8 +69,12 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
|||||||
children: [
|
children: [
|
||||||
Obx(() {
|
Obx(() {
|
||||||
final account = KRAppRunData.getInstance().kr_account.value;
|
final account = KRAppRunData.getInstance().kr_account.value;
|
||||||
|
final isDeviceLogin = account != null && account.startsWith('9000');
|
||||||
|
|
||||||
|
if (isDeviceLogin) return const SizedBox();
|
||||||
|
|
||||||
return Text(
|
return Text(
|
||||||
(account != null && account.isNotEmpty) ? account : '未绑定',
|
account ?? '',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 20.sp,
|
fontSize: 20.sp,
|
||||||
@@ -433,7 +437,7 @@ class HIUserInfoView extends GetView<HIUserInfoController> {
|
|||||||
),
|
),
|
||||||
SizedBox(height: 4.h),
|
SizedBox(height: 4.h),
|
||||||
Text(
|
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(
|
style: TextStyle(
|
||||||
color: Colors.white.withOpacity(0.9),
|
color: Colors.white.withOpacity(0.9),
|
||||||
fontSize: 10.sp,
|
fontSize: 10.sp,
|
||||||
|
|||||||
@@ -133,11 +133,21 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
if (value != null) {
|
if (value != null) {
|
||||||
isQuickConnectEnabled.value = value;
|
isQuickConnectEnabled.value = value;
|
||||||
// 保存闪连状态到本地存储
|
// 保存闪连状态到本地存储
|
||||||
await _storage.kr_saveBool(key: _quickConnectKey, value: value);
|
await _saveQuickConnectStatus(value);
|
||||||
KRLogUtil.kr_i('闪连状态已更新: $value', tag: 'QuickConnect');
|
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 {
|
Future<void> _loadQuickConnectStatus() async {
|
||||||
try {
|
try {
|
||||||
@@ -839,6 +849,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
}
|
}
|
||||||
await KRSingBoxImp.instance.kr_start();
|
await KRSingBoxImp.instance.kr_start();
|
||||||
|
|
||||||
KRLogUtil.kr_i('✅ 连接命令已发送', tag: 'HomeController');
|
KRLogUtil.kr_i('✅ 连接命令已发送', tag: 'HomeController');
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
}
|
}
|
||||||
@@ -1265,10 +1276,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_selectNode 方法
|
||||||
/// 现在只是委托给新的 kr_performNodeSwitch 方法
|
/// 现在只是委托给新的 kr_performNodeSwitch 方法
|
||||||
@@ -1932,7 +1939,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
final results = await KRLatencyTester.testMultipleNodes(
|
final results = await KRLatencyTester.testMultipleNodes(
|
||||||
nodes: nodeAddresses,
|
nodes: nodeAddresses,
|
||||||
concurrency: 10, // 每批10个并发
|
concurrency: 10, // 每批10个并发
|
||||||
timeout: 5000, // 超时时间5秒(毫秒)
|
timeout: const Duration(seconds: 5),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 更新节点延迟
|
// 更新节点延迟
|
||||||
@@ -2078,6 +2085,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
kr_isConnected.value = true;
|
kr_isConnected.value = true;
|
||||||
kr_startConnectionTimer();
|
kr_startConnectionTimer();
|
||||||
kr_updateConnectionInfo();
|
kr_updateConnectionInfo();
|
||||||
|
KRSingBoxImp.instance.kr_debugTunConnectivity();
|
||||||
|
|
||||||
// 🔧 修复:同步已启动状态时,尝试更新延迟值
|
// 🔧 修复:同步已启动状态时,尝试更新延迟值
|
||||||
if (!_kr_tryUpdateDelayFromActiveGroups()) {
|
if (!_kr_tryUpdateDelayFromActiveGroups()) {
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ class KRSplashController extends GetxController {
|
|||||||
KRLogUtil.kr_e('⏱️ 初始化超时: $e', tag: 'SplashController');
|
KRLogUtil.kr_e('⏱️ 初始化超时: $e', tag: 'SplashController');
|
||||||
print('⏱️ 初始化超时,直接跳转到主页');
|
print('⏱️ 初始化超时,直接跳转到主页');
|
||||||
// 超时后直接跳转到主页,让用户可以手动重试
|
// 超时后直接跳转到主页,让用户可以手动重试
|
||||||
// Get.offAllNamed(Routes.KR_MAIN);
|
// Get.offAllNamed(Routes.KR_HOME);
|
||||||
HIDialog.show(
|
HIDialog.show(
|
||||||
message: '初始化超时,请检查网络或重试',
|
message: '初始化超时,请检查网络或重试',
|
||||||
confirmText: '重试',
|
confirmText: '重试',
|
||||||
@@ -484,7 +484,7 @@ class KRSplashController extends GetxController {
|
|||||||
_initLog.logWarning('网络权限检查失败或超时,执行降级初始化', tag: 'Init');
|
_initLog.logWarning('网络权限检查失败或超时,执行降级初始化', tag: 'Init');
|
||||||
KRLogUtil.kr_w('⚠️ 网络权限检查失败或超时,执行降级初始化', tag: 'SplashController');
|
KRLogUtil.kr_w('⚠️ 网络权限检查失败或超时,执行降级初始化', tag: 'SplashController');
|
||||||
await _executeMinimalInitialization();
|
await _executeMinimalInitialization();
|
||||||
Get.offAllNamed(Routes.KR_MAIN);
|
Get.offAllNamed(Routes.KR_HOME);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,7 +493,7 @@ class KRSplashController extends GetxController {
|
|||||||
_initLog.logError('网络权限检查异常,执行降级初始化', tag: 'Init', error: e);
|
_initLog.logError('网络权限检查异常,执行降级初始化', tag: 'Init', error: e);
|
||||||
KRLogUtil.kr_w('⚠️ 网络权限检查异常: $e,执行降级初始化', tag: 'SplashController');
|
KRLogUtil.kr_w('⚠️ 网络权限检查异常: $e,执行降级初始化', tag: 'SplashController');
|
||||||
await _executeMinimalInitialization();
|
await _executeMinimalInitialization();
|
||||||
Get.offAllNamed(Routes.KR_MAIN);
|
Get.offAllNamed(Routes.KR_HOME);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -582,7 +582,7 @@ class KRSplashController extends GetxController {
|
|||||||
_initLog.logError('启动页初始化异常,执行降级策略', tag: 'Continue', error: e);
|
_initLog.logError('启动页初始化异常,执行降级策略', tag: 'Continue', error: e);
|
||||||
KRLogUtil.kr_w('⚠️ 启动页初始化异常,执行降级策略: $e', tag: 'SplashController');
|
KRLogUtil.kr_w('⚠️ 启动页初始化异常,执行降级策略: $e', tag: 'SplashController');
|
||||||
await _executeMinimalInitialization();
|
await _executeMinimalInitialization();
|
||||||
Get.offAllNamed(Routes.KR_MAIN);
|
Get.offAllNamed(Routes.KR_HOME);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -726,7 +726,7 @@ class KRSplashController extends GetxController {
|
|||||||
kr_isLoading.value = false;
|
kr_isLoading.value = false;
|
||||||
|
|
||||||
// 直接跳转到主页
|
// 直接跳转到主页
|
||||||
Get.offAllNamed(Routes.KR_MAIN);
|
Get.offAllNamed(Routes.KR_HOME);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 🔧 修复1.1:清理旧的本地存储数据(DEBUG模式专用)
|
/// 🔧 修复1.1:清理旧的本地存储数据(DEBUG模式专用)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import 'package:path/path.dart' as p;
|
|||||||
import '../../../core/model/directories.dart';
|
import '../../../core/model/directories.dart';
|
||||||
import '../../../singbox/model/singbox_config_option.dart';
|
import '../../../singbox/model/singbox_config_option.dart';
|
||||||
import '../../../singbox/model/singbox_outbound.dart';
|
import '../../../singbox/model/singbox_outbound.dart';
|
||||||
|
import '../../../singbox/model/singbox_proxy_type.dart';
|
||||||
import '../../../singbox/model/singbox_stats.dart';
|
import '../../../singbox/model/singbox_stats.dart';
|
||||||
import '../../../singbox/model/singbox_status.dart';
|
import '../../../singbox/model/singbox_status.dart';
|
||||||
import '../../utils/kr_country_util.dart';
|
import '../../utils/kr_country_util.dart';
|
||||||
@@ -23,6 +24,8 @@ import '../../utils/kr_log_util.dart';
|
|||||||
import '../../utils/kr_secure_storage.dart';
|
import '../../utils/kr_secure_storage.dart';
|
||||||
import '../../common/app_run_data.dart';
|
import '../../common/app_run_data.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
|
|
||||||
|
|
||||||
enum KRConnectionType {
|
enum KRConnectionType {
|
||||||
global,
|
global,
|
||||||
@@ -107,6 +110,9 @@ class KRSingBoxImp {
|
|||||||
/// 初始化标志,防止重复初始化
|
/// 初始化标志,防止重复初始化
|
||||||
bool _kr_isInitialized = false;
|
bool _kr_isInitialized = false;
|
||||||
|
|
||||||
|
/// 初始化进行中共享 Future(single-flight 防并发)
|
||||||
|
Future<void>? _kr_initFuture;
|
||||||
|
|
||||||
/// 当前混合代理端口是否就绪
|
/// 当前混合代理端口是否就绪
|
||||||
bool get kr_isProxyReady => kr_status.value is SingboxStarted;
|
bool get kr_isProxyReady => kr_status.value is SingboxStarted;
|
||||||
|
|
||||||
@@ -139,12 +145,23 @@ class KRSingBoxImp {
|
|||||||
|
|
||||||
/// 初始化
|
/// 初始化
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
// 防止重复初始化
|
// 防止重复初始化(已完成)
|
||||||
if (_kr_isInitialized) {
|
if (_kr_isInitialized) {
|
||||||
KRLogUtil.kr_i('SingBox 已经初始化,跳过重复初始化', tag: 'SingBox');
|
KRLogUtil.kr_i('SingBox 已经初始化,跳过重复初始化', tag: 'SingBox');
|
||||||
return;
|
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 {
|
try {
|
||||||
KRLogUtil.kr_i('开始初始化 SingBox');
|
KRLogUtil.kr_i('开始初始化 SingBox');
|
||||||
// 在应用启动时初始化
|
// 在应用启动时初始化
|
||||||
@@ -167,14 +184,14 @@ class KRSingBoxImp {
|
|||||||
KRLogUtil.kr_i('iOS 路径获取完成: $paths');
|
KRLogUtil.kr_i('iOS 路径获取完成: $paths');
|
||||||
|
|
||||||
kr_configDics = (
|
kr_configDics = (
|
||||||
baseDir: Directory(paths?["base"]! as String),
|
baseDir: Directory(paths?["base"]! as String),
|
||||||
workingDir: Directory(paths?["working"]! as String),
|
workingDir: Directory(paths?["working"]! as String),
|
||||||
tempDir: Directory(paths?["temp"]! as String),
|
tempDir: Directory(paths?["temp"]! as String),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final baseDir = await getApplicationSupportDirectory();
|
final baseDir = await getApplicationSupportDirectory();
|
||||||
final workingDir =
|
final workingDir =
|
||||||
Platform.isAndroid ? await getExternalStorageDirectory() : baseDir;
|
Platform.isAndroid ? await getExternalStorageDirectory() : baseDir;
|
||||||
final tempDir = await getTemporaryDirectory();
|
final tempDir = await getTemporaryDirectory();
|
||||||
|
|
||||||
// Windows 路径规范化:确保使用正确的路径分隔符
|
// Windows 路径规范化:确保使用正确的路径分隔符
|
||||||
@@ -190,9 +207,9 @@ class KRSingBoxImp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kr_configDics = (
|
kr_configDics = (
|
||||||
baseDir: normalizePath(baseDir),
|
baseDir: normalizePath(baseDir),
|
||||||
workingDir: normalizePath(workingDir!),
|
workingDir: normalizePath(workingDir!),
|
||||||
tempDir: normalizePath(tempDir),
|
tempDir: normalizePath(tempDir),
|
||||||
);
|
);
|
||||||
KRLogUtil.kr_i('其他平台路径初始化完成');
|
KRLogUtil.kr_i('其他平台路径初始化完成');
|
||||||
}
|
}
|
||||||
@@ -400,11 +417,11 @@ class KRSingBoxImp {
|
|||||||
KRLogUtil.kr_i('📡 开始调用 setup() 注册 FFI 端口', tag: 'SingBox');
|
KRLogUtil.kr_i('📡 开始调用 setup() 注册 FFI 端口', tag: 'SingBox');
|
||||||
final setupResult = await kr_singBox.setup(kr_configDics, false).run();
|
final setupResult = await kr_singBox.setup(kr_configDics, false).run();
|
||||||
setupResult.match(
|
setupResult.match(
|
||||||
(error) {
|
(error) {
|
||||||
KRLogUtil.kr_e('❌ setup() 失败: $error', tag: 'SingBox');
|
KRLogUtil.kr_e('❌ setup() 失败: $error', tag: 'SingBox');
|
||||||
throw Exception('FFI setup 失败: $error');
|
throw Exception('FFI setup 失败: $error');
|
||||||
},
|
},
|
||||||
(_) {
|
(_) {
|
||||||
KRLogUtil.kr_i('✅ setup() 成功,FFI 端口已注册', tag: 'SingBox');
|
KRLogUtil.kr_i('✅ setup() 成功,FFI 端口已注册', tag: 'SingBox');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -416,6 +433,10 @@ class KRSingBoxImp {
|
|||||||
// 这样可以确保 UI 始终与 libcore 的实际状态同步
|
// 这样可以确保 UI 始终与 libcore 的实际状态同步
|
||||||
_kr_subscribeToStatus();
|
_kr_subscribeToStatus();
|
||||||
KRLogUtil.kr_i('✅ 状态订阅已设置', tag: 'SingBox');
|
KRLogUtil.kr_i('✅ 状态订阅已设置', tag: 'SingBox');
|
||||||
|
|
||||||
|
// 完成 single-flight
|
||||||
|
completer.complete();
|
||||||
|
_kr_initFuture = null;
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
KRLogUtil.kr_e('❌ SingBox 初始化失败: $e');
|
KRLogUtil.kr_e('❌ SingBox 初始化失败: $e');
|
||||||
KRLogUtil.kr_e('📚 错误堆栈: $stackTrace');
|
KRLogUtil.kr_e('📚 错误堆栈: $stackTrace');
|
||||||
@@ -451,6 +472,11 @@ class KRSingBoxImp {
|
|||||||
|
|
||||||
// 如果初始化失败,允许下次重试
|
// 如果初始化失败,允许下次重试
|
||||||
_kr_isInitialized = false;
|
_kr_isInitialized = false;
|
||||||
|
// 失败时通知等待者并清理 single-flight
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.completeError(e, stackTrace);
|
||||||
|
}
|
||||||
|
_kr_initFuture = null;
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -464,13 +490,13 @@ class KRSingBoxImp {
|
|||||||
"region": "other", // 参考 hiddify-app: 默认使用 "other" 跳过规则集下载
|
"region": "other", // 参考 hiddify-app: 默认使用 "other" 跳过规则集下载
|
||||||
"block-ads": false, // 参考 hiddify-app: 默认关闭广告拦截
|
"block-ads": false, // 参考 hiddify-app: 默认关闭广告拦截
|
||||||
"use-xray-core-when-possible": false,
|
"use-xray-core-when-possible": false,
|
||||||
"execute-config-as-is": false,
|
"execute-config-as-is": true, // 🔧 修复:使用我们生成的完整配置,不让 libcore 重写
|
||||||
"log-level": "info", // 调试阶段使用 info,生产环境改为 warn
|
"log-level": "info", // 调试阶段使用 info,生产环境改为 warn
|
||||||
"resolve-destination": false,
|
"resolve-destination": false,
|
||||||
"ipv6-mode": "ipv4_only", // 参考 hiddify-app: 仅使用 IPv4 (有效值: ipv4_only, prefer_ipv4, prefer_ipv6, ipv6_only)
|
"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",
|
"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",
|
"direct-dns-domain-strategy": "prefer_ipv4",
|
||||||
"mixed-port": kr_port,
|
"mixed-port": kr_port,
|
||||||
"tproxy-port": kr_port,
|
"tproxy-port": kr_port,
|
||||||
@@ -561,7 +587,7 @@ class KRSingBoxImp {
|
|||||||
|
|
||||||
_kr_subscriptions.add(
|
_kr_subscriptions.add(
|
||||||
kr_singBox.watchStatus().listen(
|
kr_singBox.watchStatus().listen(
|
||||||
(status) {
|
(status) {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print('🔵 收到 Native 状态更新: ${status.runtimeType}');
|
print('🔵 收到 Native 状态更新: ${status.runtimeType}');
|
||||||
}
|
}
|
||||||
@@ -599,7 +625,7 @@ class KRSingBoxImp {
|
|||||||
// 所以外层必须有 try-catch
|
// 所以外层必须有 try-catch
|
||||||
final stream = kr_singBox.watchStats();
|
final stream = kr_singBox.watchStats();
|
||||||
final subscription = stream.listen(
|
final subscription = stream.listen(
|
||||||
(stats) {
|
(stats) {
|
||||||
kr_stats.value = stats;
|
kr_stats.value = stats;
|
||||||
},
|
},
|
||||||
onError: (error) {
|
onError: (error) {
|
||||||
@@ -624,7 +650,7 @@ class KRSingBoxImp {
|
|||||||
|
|
||||||
_kr_subscriptions.add(
|
_kr_subscriptions.add(
|
||||||
kr_singBox.watchActiveGroups().listen(
|
kr_singBox.watchActiveGroups().listen(
|
||||||
(groups) {
|
(groups) {
|
||||||
print('[watchActiveGroups] 📡 收到活动组更新,数量: ${groups.length}');
|
print('[watchActiveGroups] 📡 收到活动组更新,数量: ${groups.length}');
|
||||||
KRLogUtil.kr_i('📡 收到活动组更新,数量: ${groups.length}', tag: 'SingBox');
|
KRLogUtil.kr_i('📡 收到活动组更新,数量: ${groups.length}', tag: 'SingBox');
|
||||||
kr_activeGroups.value = groups;
|
kr_activeGroups.value = groups;
|
||||||
@@ -651,7 +677,7 @@ class KRSingBoxImp {
|
|||||||
|
|
||||||
_kr_subscriptions.add(
|
_kr_subscriptions.add(
|
||||||
kr_singBox.watchGroups().listen(
|
kr_singBox.watchGroups().listen(
|
||||||
(groups) {
|
(groups) {
|
||||||
print('[watchGroups] 📡 收到所有组更新,数量: ${groups.length}');
|
print('[watchGroups] 📡 收到所有组更新,数量: ${groups.length}');
|
||||||
kr_allGroups.value = groups;
|
kr_allGroups.value = groups;
|
||||||
// 打印每个组的基本信息
|
// 打印每个组的基本信息
|
||||||
@@ -679,7 +705,7 @@ class KRSingBoxImp {
|
|||||||
|
|
||||||
// 查找 "select" 组
|
// 查找 "select" 组
|
||||||
final selectGroup = kr_activeGroups.firstWhere(
|
final selectGroup = kr_activeGroups.firstWhere(
|
||||||
(group) => group.tag == 'select',
|
(group) => group.tag == 'select',
|
||||||
orElse: () => throw Exception('未找到 "select" 选择器组'),
|
orElse: () => throw Exception('未找到 "select" 选择器组'),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -719,11 +745,11 @@ class KRSingBoxImp {
|
|||||||
///
|
///
|
||||||
/// 确保 command.sock 准备好后再执行节点选择
|
/// 确保 command.sock 准备好后再执行节点选择
|
||||||
Future<void> _kr_selectOutboundWithRetry(
|
Future<void> _kr_selectOutboundWithRetry(
|
||||||
String groupTag,
|
String groupTag,
|
||||||
String outboundTag, {
|
String outboundTag, {
|
||||||
int maxAttempts = 3,
|
int maxAttempts = 3,
|
||||||
int initialDelay = 100,
|
int initialDelay = 100,
|
||||||
}) async {
|
}) async {
|
||||||
int attempt = 0;
|
int attempt = 0;
|
||||||
int delay = initialDelay;
|
int delay = initialDelay;
|
||||||
|
|
||||||
@@ -1031,17 +1057,86 @@ class KRSingBoxImp {
|
|||||||
|
|
||||||
KRLogUtil.kr_i('✅ 过滤后节点数量: ${kr_outbounds.length}/${outbounds.length}', tag: 'SingBox');
|
KRLogUtil.kr_i('✅ 过滤后节点数量: ${kr_outbounds.length}/${outbounds.length}', tag: 'SingBox');
|
||||||
|
|
||||||
// 只保存 outbounds,Mobile.buildConfig() 会添加其他配置
|
// 🔧 修复:生成完整的 SingBox 配置
|
||||||
final map = {
|
// 之前只保存 {"outbounds": [...]}, 导致 libcore 无法正确处理
|
||||||
"outbounds": kr_outbounds
|
// 现在保存完整配置,包含所有必需字段
|
||||||
|
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": kr_outbounds.map((o) => o['tag'] as String).toList(),
|
||||||
|
"default": kr_outbounds.isNotEmpty ? kr_outbounds[0]['tag'] : "direct",
|
||||||
|
},
|
||||||
|
...kr_outbounds,
|
||||||
|
{
|
||||||
|
"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 file = _file(kr_configName);
|
||||||
final temp = _tempFile(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.length}', tag: 'SingBox');
|
||||||
KRLogUtil.kr_i('📄 完整配置文件内容: $mapStr', tag: 'SingBox');
|
KRLogUtil.kr_i('📄 Outbounds 数量: ${kr_outbounds.length}', tag: 'SingBox');
|
||||||
|
KRLogUtil.kr_i('📄 配置前800字符:\n${mapStr.substring(0, mapStr.length > 800 ? 800 : mapStr.length)}', tag: 'SingBox');
|
||||||
|
|
||||||
await file.writeAsString(mapStr);
|
await file.writeAsString(mapStr);
|
||||||
await temp.writeAsString(mapStr);
|
await temp.writeAsString(mapStr);
|
||||||
@@ -1089,11 +1184,11 @@ class KRSingBoxImp {
|
|||||||
final oOption = SingboxConfigOption.fromJson(_getConfigOption());
|
final oOption = SingboxConfigOption.fromJson(_getConfigOption());
|
||||||
final changeResult = await kr_singBox.changeOptions(oOption).run();
|
final changeResult = await kr_singBox.changeOptions(oOption).run();
|
||||||
changeResult.match(
|
changeResult.match(
|
||||||
(error) {
|
(error) {
|
||||||
KRLogUtil.kr_e('❌ changeOptions() 失败: $error', tag: 'SingBox');
|
KRLogUtil.kr_e('❌ changeOptions() 失败: $error', tag: 'SingBox');
|
||||||
throw Exception('初始化 HiddifyOptions 失败: $error');
|
throw Exception('初始化 HiddifyOptions 失败: $error');
|
||||||
},
|
},
|
||||||
(_) {
|
(_) {
|
||||||
KRLogUtil.kr_i('✅ HiddifyOptions 初始化成功', tag: 'SingBox');
|
KRLogUtil.kr_i('✅ HiddifyOptions 初始化成功', tag: 'SingBox');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -1112,7 +1207,7 @@ class KRSingBoxImp {
|
|||||||
_kr_subscribeToStatus();
|
_kr_subscribeToStatus();
|
||||||
|
|
||||||
await kr_singBox.start(_cutPath, kr_configName, false).map(
|
await kr_singBox.start(_cutPath, kr_configName, false).map(
|
||||||
(r) {
|
(r) {
|
||||||
KRLogUtil.kr_i('✅ SingBox 启动成功', tag: 'SingBox');
|
KRLogUtil.kr_i('✅ SingBox 启动成功', tag: 'SingBox');
|
||||||
},
|
},
|
||||||
).mapLeft((err) {
|
).mapLeft((err) {
|
||||||
@@ -1316,9 +1411,9 @@ class KRSingBoxImp {
|
|||||||
mode = KRCountryUtil.kr_getCurrentCountryCode();
|
mode = KRCountryUtil.kr_getCurrentCountryCode();
|
||||||
KRLogUtil.kr_i('🎯 切换到规则代理模式: $mode', tag: 'SingBox');
|
KRLogUtil.kr_i('🎯 切换到规则代理模式: $mode', tag: 'SingBox');
|
||||||
break;
|
break;
|
||||||
// case KRConnectionType.direct:
|
// case KRConnectionType.direct:
|
||||||
// mode = "direct";
|
// mode = "direct";
|
||||||
// break;
|
// break;
|
||||||
}
|
}
|
||||||
oOption["region"] = mode;
|
oOption["region"] = mode;
|
||||||
KRLogUtil.kr_i('📝 更新 region 配置: $mode', tag: 'SingBox');
|
KRLogUtil.kr_i('📝 更新 region 配置: $mode', tag: 'SingBox');
|
||||||
@@ -1454,8 +1549,8 @@ class KRSingBoxImp {
|
|||||||
// 使用 then/catchError 避免异常导致 UI 阻塞
|
// 使用 then/catchError 避免异常导致 UI 阻塞
|
||||||
kr_singBox.selectOutbound("select", tag).run().then((result) {
|
kr_singBox.selectOutbound("select", tag).run().then((result) {
|
||||||
result.match(
|
result.match(
|
||||||
(error) => KRLogUtil.kr_w('🔁 定时器重选节点失败: $error', tag: 'SingBox'),
|
(error) => KRLogUtil.kr_w('🔁 定时器重选节点失败: $error', tag: 'SingBox'),
|
||||||
(_) => KRLogUtil.kr_d('🔁 定时器重选节点成功', tag: 'SingBox'),
|
(_) => KRLogUtil.kr_d('🔁 定时器重选节点成功', tag: 'SingBox'),
|
||||||
);
|
);
|
||||||
}).catchError((error) {
|
}).catchError((error) {
|
||||||
KRLogUtil.kr_w('🔁 定时器重选节点异常: $error', tag: 'SingBox');
|
KRLogUtil.kr_w('🔁 定时器重选节点异常: $error', tag: 'SingBox');
|
||||||
@@ -1465,86 +1560,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');
|
|
||||||
}
|
|
||||||
});*/
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// 配合文件地址
|
/// 配合文件地址
|
||||||
|
|
||||||
@@ -1595,4 +1610,221 @@ class KRSingBoxImp {
|
|||||||
KRLogUtil.kr_e('📚 错误详情: ${e.toString()}', tag: 'SingBox');
|
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:io';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
import 'kr_log_util.dart';
|
||||||
|
|
||||||
/// 延迟测试工具类
|
/// 真正的节点延迟测试工具
|
||||||
/// 提供真实的 TCP 连接延迟测试功能
|
|
||||||
class KRLatencyTester {
|
class KRLatencyTester {
|
||||||
/// 测试单个节点的延迟
|
/// TCP 连接测试延迟(真实测试)
|
||||||
///
|
/// 返回延迟毫秒数,失败返回 65535
|
||||||
/// 参数:
|
static Future<int> testTcpLatency({
|
||||||
/// - host: 主机地址
|
|
||||||
/// - port: 端口号
|
|
||||||
/// - timeout: 超时时间(毫秒)
|
|
||||||
///
|
|
||||||
/// 返回:
|
|
||||||
/// - 延迟时间(毫秒),如果失败返回 65535
|
|
||||||
static Future<int> testNode({
|
|
||||||
required String host,
|
required String host,
|
||||||
required int port,
|
required int port,
|
||||||
int timeout = 5000,
|
Duration timeout = const Duration(seconds: 5),
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
Socket? socket;
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch();
|
||||||
|
|
||||||
final socket = await Socket.connect(
|
try {
|
||||||
|
KRLogUtil.kr_i('🔌 开始测试: $host:$port', tag: 'LatencyTester');
|
||||||
|
|
||||||
|
stopwatch.start();
|
||||||
|
|
||||||
|
// 尝试 TCP 连接
|
||||||
|
socket = await Socket.connect(
|
||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
timeout: Duration(milliseconds: timeout),
|
timeout: timeout,
|
||||||
).timeout(Duration(milliseconds: timeout));
|
);
|
||||||
|
|
||||||
stopwatch.stop();
|
stopwatch.stop();
|
||||||
|
|
||||||
// 立即关闭连接
|
|
||||||
await socket.close();
|
|
||||||
socket.destroy();
|
|
||||||
|
|
||||||
final latency = stopwatch.elapsedMilliseconds;
|
final latency = stopwatch.elapsedMilliseconds;
|
||||||
KRLogUtil.kr_i('✅ 延迟测试成功: $host:$port = ${latency}ms', tag: 'KRLatencyTester');
|
|
||||||
|
KRLogUtil.kr_i('✅ 测试成功: $host:$port - ${latency}ms', tag: 'LatencyTester');
|
||||||
|
|
||||||
return latency;
|
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) {
|
} catch (e) {
|
||||||
KRLogUtil.kr_w('❌ 延迟测试失败: $host:$port - $e', tag: 'KRLatencyTester');
|
stopwatch.stop();
|
||||||
return 65535; // 测试失败返回最大值
|
KRLogUtil.kr_e('❌ 测试异常: $host:$port - $e', tag: 'LatencyTester');
|
||||||
|
return 65535;
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
// 确保关闭连接
|
||||||
|
try {
|
||||||
|
await socket?.close();
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略关闭错误
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 批量测试多个节点的延迟
|
/// 批量测试多个节点延迟(并发测试)
|
||||||
///
|
/// 返回 Map<节点tag, 延迟ms>
|
||||||
/// 参数:
|
|
||||||
/// - nodes: 节点列表,格式为 [{"host": "example.com", "port": 443}]
|
|
||||||
/// - concurrency: 并发数量
|
|
||||||
/// - timeout: 超时时间(毫秒)
|
|
||||||
///
|
|
||||||
/// 返回:
|
|
||||||
/// - 测试结果映射,键为 "host:port",值为延迟时间
|
|
||||||
static Future<Map<String, int>> testMultipleNodes({
|
static Future<Map<String, int>> testMultipleNodes({
|
||||||
required List<MapEntry<String, SocketAddress>> nodes,
|
required List<MapEntry<String, SocketAddress>> nodes,
|
||||||
int concurrency = 10,
|
int concurrency = 10, // 并发数
|
||||||
int timeout = 5000,
|
Duration timeout = const Duration(seconds: 5),
|
||||||
}) async {
|
}) async {
|
||||||
final results = <String, int>{};
|
final results = <String, int>{};
|
||||||
final semaphore = Completer<void>();
|
final List<List<MapEntry<String, SocketAddress>>> batches = [];
|
||||||
var activeCount = 0;
|
|
||||||
var completedCount = 0;
|
|
||||||
|
|
||||||
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 {
|
KRLogUtil.kr_i('📊 开始批量测试: ${nodes.length} 个节点,分 ${batches.length} 批,每批 $concurrency 个', tag: 'LatencyTester');
|
||||||
try {
|
|
||||||
final host = node.value.address;
|
|
||||||
final port = node.value.port;
|
|
||||||
final key = node.key;
|
|
||||||
|
|
||||||
final latency = await testNode(
|
int completedCount = 0;
|
||||||
host: host,
|
|
||||||
port: port,
|
// 逐批测试
|
||||||
|
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,
|
timeout: timeout,
|
||||||
);
|
);
|
||||||
|
|
||||||
results[key] = latency;
|
|
||||||
} catch (e) {
|
|
||||||
results[node.key] = 65535;
|
|
||||||
KRLogUtil.kr_e('❌ 节点测试异常: ${node.key} - $e', tag: 'KRLatencyTester');
|
|
||||||
} finally {
|
|
||||||
completedCount++;
|
completedCount++;
|
||||||
activeCount--;
|
|
||||||
|
|
||||||
if (completedCount >= nodes.length) {
|
if (completedCount % 5 == 0 || completedCount == nodes.length) {
|
||||||
semaphore.complete();
|
KRLogUtil.kr_i('📈 测试进度: $completedCount/${nodes.length}', tag: 'LatencyTester');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分批处理节点
|
return MapEntry(tag, latency);
|
||||||
for (var i = 0; i < nodes.length; i += concurrency) {
|
}).toList();
|
||||||
final batch = nodes.skip(i).take(concurrency);
|
|
||||||
|
|
||||||
for (final node in batch) {
|
|
||||||
activeCount++;
|
|
||||||
processNode(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 等待当前批次完成
|
// 等待当前批次完成
|
||||||
if (i + concurrency < nodes.length) {
|
final batchResults = await Future.wait(futures);
|
||||||
await Future.delayed(Duration(milliseconds: 100));
|
|
||||||
|
// 收集结果
|
||||||
|
for (final result in batchResults) {
|
||||||
|
results[result.key] = result.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 等待所有任务完成
|
// 统计结果
|
||||||
await semaphore.future;
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
KRLogUtil.kr_i('✅ 批量延迟测试完成,成功: ${results.length} 个', tag: 'KRLatencyTester');
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Socket 地址类
|
/// 节点地址信息
|
||||||
/// 表示网络地址和端口的组合
|
|
||||||
class SocketAddress {
|
class SocketAddress {
|
||||||
final String address;
|
final String host;
|
||||||
final int port;
|
final int port;
|
||||||
|
|
||||||
SocketAddress(this.address, this.port);
|
SocketAddress(this.host, this.port);
|
||||||
|
|
||||||
@override
|
@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%\msvcp140.dll" "%DLL_DIR%"
|
||||||
copy "%VC_REDIST_DIR%\vcruntime140.dll" "%DLL_DIR%"
|
copy "%VC_REDIST_DIR%\vcruntime140.dll" "%DLL_DIR%"
|
||||||
copy "%VC_REDIST_DIR%\vcruntime140_1.dll" "%DLL_DIR%"
|
copy "%VC_REDIST_DIR%\vcruntime140_1.dll" "%DLL_DIR%"
|
||||||
|
copy "%~dp0..\libcore\libcore.dll" "%DLL_DIR%"
|
||||||
|
|
||||||
echo DLL 文件复制完成!
|
echo DLL 文件复制完成!
|
||||||
pause
|
pause
|
||||||
Reference in New Issue
Block a user