feat: 初始化并发问题处理
This commit is contained in:
parent
748ec6bee9
commit
a47174df56
@ -28,13 +28,10 @@ class KROutboundItem {
|
|||||||
|
|
||||||
/// URL
|
/// URL
|
||||||
String url = "";
|
String url = "";
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'KROutboundItem(tag: $tag, country: $country, delay: ${urlTestDelay.value}ms)';
|
|
||||||
}
|
|
||||||
/// 服务器类型
|
/// 服务器类型
|
||||||
|
|
||||||
/// 构造函数,接受 KrNodeListItem 对象并初始化 KROutboundItem
|
/// 构造函数,接受 KrItem 对象并初始化 KROutboundItem
|
||||||
KROutboundItem(KrNodeListItem nodeListItem) {
|
KROutboundItem(KrNodeListItem nodeListItem) {
|
||||||
id = nodeListItem.id.toString();
|
id = nodeListItem.id.toString();
|
||||||
protocol = nodeListItem.protocol;
|
protocol = nodeListItem.protocol;
|
||||||
@ -84,13 +81,13 @@ class KROutboundItem {
|
|||||||
case "vless":
|
case "vless":
|
||||||
final securityConfig =
|
final securityConfig =
|
||||||
json["security_config"] as Map<String, dynamic>? ?? {};
|
json["security_config"] as Map<String, dynamic>? ?? {};
|
||||||
|
|
||||||
// 智能设置 server_name
|
// 智能设置 server_name
|
||||||
String serverName = securityConfig["sni"] ?? "";
|
String serverName = securityConfig["sni"] ?? "";
|
||||||
if (serverName.isEmpty) {
|
if (serverName.isEmpty) {
|
||||||
serverName = nodeListItem.serverAddr;
|
serverName = nodeListItem.serverAddr;
|
||||||
}
|
}
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"type": "vless",
|
"type": "vless",
|
||||||
"tag": nodeListItem.name,
|
"tag": nodeListItem.name,
|
||||||
@ -115,13 +112,13 @@ class KROutboundItem {
|
|||||||
case "vmess":
|
case "vmess":
|
||||||
final securityConfig =
|
final securityConfig =
|
||||||
json["security_config"] as Map<String, dynamic>? ?? {};
|
json["security_config"] as Map<String, dynamic>? ?? {};
|
||||||
|
|
||||||
// 智能设置 server_name
|
// 智能设置 server_name
|
||||||
String serverName = securityConfig["sni"] ?? "";
|
String serverName = securityConfig["sni"] ?? "";
|
||||||
if (serverName.isEmpty) {
|
if (serverName.isEmpty) {
|
||||||
serverName = nodeListItem.serverAddr;
|
serverName = nodeListItem.serverAddr;
|
||||||
}
|
}
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"type": "vmess",
|
"type": "vmess",
|
||||||
"tag": nodeListItem.name,
|
"tag": nodeListItem.name,
|
||||||
@ -152,7 +149,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 = {
|
||||||
@ -177,14 +174,14 @@ class KROutboundItem {
|
|||||||
case "trojan":
|
case "trojan":
|
||||||
final securityConfig =
|
final securityConfig =
|
||||||
json["security_config"] as Map<String, dynamic>? ?? {};
|
json["security_config"] as Map<String, dynamic>? ?? {};
|
||||||
|
|
||||||
// 智能设置 server_name
|
// 智能设置 server_name
|
||||||
String serverName = securityConfig["sni"] ?? "";
|
String serverName = securityConfig["sni"] ?? "";
|
||||||
if (serverName.isEmpty) {
|
if (serverName.isEmpty) {
|
||||||
// 如果没有配置 SNI,使用服务器地址
|
// 如果没有配置 SNI,使用服务器地址
|
||||||
serverName = nodeListItem.serverAddr;
|
serverName = nodeListItem.serverAddr;
|
||||||
}
|
}
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"type": "trojan",
|
"type": "trojan",
|
||||||
"tag": nodeListItem.name,
|
"tag": nodeListItem.name,
|
||||||
@ -274,6 +271,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 +305,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 +422,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 +431,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 +455,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 +487,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 +517,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 +551,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 +585,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 +607,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']),
|
||||||
|
|||||||
@ -80,8 +80,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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -380,7 +380,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,
|
||||||
|
|||||||
@ -849,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) {
|
||||||
}
|
}
|
||||||
@ -2185,6 +2186,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()) {
|
||||||
|
|||||||
@ -107,6 +107,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 +142,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,16 +181,16 @@ 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 路径规范化:确保使用正确的路径分隔符
|
||||||
Directory normalizePath(Directory dir) {
|
Directory normalizePath(Directory dir) {
|
||||||
if (Platform.isWindows) {
|
if (Platform.isWindows) {
|
||||||
@ -188,11 +202,11 @@ class KRSingBoxImp {
|
|||||||
}
|
}
|
||||||
return dir;
|
return dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
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('其他平台路径初始化完成');
|
||||||
}
|
}
|
||||||
@ -201,7 +215,7 @@ class KRSingBoxImp {
|
|||||||
KRLogUtil.kr_i('baseDir: ${kr_configDics.baseDir.path}', tag: 'SingBox');
|
KRLogUtil.kr_i('baseDir: ${kr_configDics.baseDir.path}', tag: 'SingBox');
|
||||||
KRLogUtil.kr_i('workingDir: ${kr_configDics.workingDir.path}', tag: 'SingBox');
|
KRLogUtil.kr_i('workingDir: ${kr_configDics.workingDir.path}', tag: 'SingBox');
|
||||||
KRLogUtil.kr_i('tempDir: ${kr_configDics.tempDir.path}', tag: 'SingBox');
|
KRLogUtil.kr_i('tempDir: ${kr_configDics.tempDir.path}', tag: 'SingBox');
|
||||||
|
|
||||||
// 确保所有目录都存在
|
// 确保所有目录都存在
|
||||||
if (!kr_configDics.baseDir.existsSync()) {
|
if (!kr_configDics.baseDir.existsSync()) {
|
||||||
await kr_configDics.baseDir.create(recursive: true);
|
await kr_configDics.baseDir.create(recursive: true);
|
||||||
@ -215,11 +229,11 @@ class KRSingBoxImp {
|
|||||||
await kr_configDics.tempDir.create(recursive: true);
|
await kr_configDics.tempDir.create(recursive: true);
|
||||||
KRLogUtil.kr_i('已创建 tempDir', tag: 'SingBox');
|
KRLogUtil.kr_i('已创建 tempDir', tag: 'SingBox');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建 libcore 数据库所需的 data 目录(在 workingDir 下)
|
// 创建 libcore 数据库所需的 data 目录(在 workingDir 下)
|
||||||
// 注意:libcore 的 Setup 会调用 os.Chdir(workingPath),所以 data 目录必须在 workingDir 下
|
// 注意:libcore 的 Setup 会调用 os.Chdir(workingPath),所以 data 目录必须在 workingDir 下
|
||||||
final dataDir = Directory(p.join(kr_configDics.workingDir.path, 'data'));
|
final dataDir = Directory(p.join(kr_configDics.workingDir.path, 'data'));
|
||||||
|
|
||||||
// 强制确保 data 目录存在(Windows 可能需要多次尝试)
|
// 强制确保 data 目录存在(Windows 可能需要多次尝试)
|
||||||
int retryCount = 0;
|
int retryCount = 0;
|
||||||
const maxRetries = 5;
|
const maxRetries = 5;
|
||||||
@ -228,7 +242,7 @@ class KRSingBoxImp {
|
|||||||
await dataDir.create(recursive: true);
|
await dataDir.create(recursive: true);
|
||||||
// 等待文件系统同步(Windows 上可能需要一点时间)
|
// 等待文件系统同步(Windows 上可能需要一点时间)
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
// 验证目录确实创建成功
|
// 验证目录确实创建成功
|
||||||
if (dataDir.existsSync()) {
|
if (dataDir.existsSync()) {
|
||||||
KRLogUtil.kr_i('✅ 已创建 data 目录: ${dataDir.path}', tag: 'SingBox');
|
KRLogUtil.kr_i('✅ 已创建 data 目录: ${dataDir.path}', tag: 'SingBox');
|
||||||
@ -248,13 +262,13 @@ class KRSingBoxImp {
|
|||||||
await Future.delayed(Duration(milliseconds: delayMs));
|
await Future.delayed(Duration(milliseconds: delayMs));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dataDir.existsSync()) {
|
if (!dataDir.existsSync()) {
|
||||||
final error = 'data 目录不存在: ${dataDir.path}';
|
final error = 'data 目录不存在: ${dataDir.path}';
|
||||||
KRLogUtil.kr_e('❌ $error', tag: 'SingBox');
|
KRLogUtil.kr_e('❌ $error', tag: 'SingBox');
|
||||||
throw Exception(error);
|
throw Exception(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证目录权限(尝试创建一个测试文件)
|
// 验证目录权限(尝试创建一个测试文件)
|
||||||
try {
|
try {
|
||||||
final testFile = File(p.join(dataDir.path, '.test_write'));
|
final testFile = File(p.join(dataDir.path, '.test_write'));
|
||||||
@ -265,13 +279,13 @@ class KRSingBoxImp {
|
|||||||
KRLogUtil.kr_e('⚠️ data 目录写入权限验证失败: $e', tag: 'SingBox');
|
KRLogUtil.kr_e('⚠️ data 目录写入权限验证失败: $e', tag: 'SingBox');
|
||||||
// 不抛出异常,让 libcore 自己处理
|
// 不抛出异常,让 libcore 自己处理
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在 Windows 上额外等待,确保文件系统操作完成
|
// 在 Windows 上额外等待,确保文件系统操作完成
|
||||||
if (Platform.isWindows) {
|
if (Platform.isWindows) {
|
||||||
await Future.delayed(const Duration(milliseconds: 300));
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
KRLogUtil.kr_i('⏳ Windows 文件系统同步等待完成', tag: 'SingBox');
|
KRLogUtil.kr_i('⏳ Windows 文件系统同步等待完成', tag: 'SingBox');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 最终验证:在 setup() 之前再次确认 workingDir 和 data 目录都存在且可访问
|
// 最终验证:在 setup() 之前再次确认 workingDir 和 data 目录都存在且可访问
|
||||||
// libcore 的 Setup() 会调用 os.Chdir(workingPath),然后使用相对路径 "./data"
|
// libcore 的 Setup() 会调用 os.Chdir(workingPath),然后使用相对路径 "./data"
|
||||||
// 如果 os.Chdir() 失败(路径不存在或权限问题),后续的相对路径访问会失败
|
// 如果 os.Chdir() 失败(路径不存在或权限问题),后续的相对路径访问会失败
|
||||||
@ -280,7 +294,7 @@ class KRSingBoxImp {
|
|||||||
KRLogUtil.kr_e(error, tag: 'SingBox');
|
KRLogUtil.kr_e(error, tag: 'SingBox');
|
||||||
throw Exception(error);
|
throw Exception(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证 workingDir 可读可写
|
// 验证 workingDir 可读可写
|
||||||
try {
|
try {
|
||||||
final testWorkingFile = File(p.join(kr_configDics.workingDir.path, '.test_working_dir'));
|
final testWorkingFile = File(p.join(kr_configDics.workingDir.path, '.test_working_dir'));
|
||||||
@ -292,7 +306,7 @@ class KRSingBoxImp {
|
|||||||
KRLogUtil.kr_e(error, tag: 'SingBox');
|
KRLogUtil.kr_e(error, tag: 'SingBox');
|
||||||
throw Exception(error);
|
throw Exception(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
final finalDataDir = Directory(p.join(kr_configDics.workingDir.path, 'data'));
|
final finalDataDir = Directory(p.join(kr_configDics.workingDir.path, 'data'));
|
||||||
if (!finalDataDir.existsSync()) {
|
if (!finalDataDir.existsSync()) {
|
||||||
KRLogUtil.kr_e('❌ 最终验证失败:data 目录不存在', tag: 'SingBox');
|
KRLogUtil.kr_e('❌ 最终验证失败:data 目录不存在', tag: 'SingBox');
|
||||||
@ -308,7 +322,7 @@ class KRSingBoxImp {
|
|||||||
}
|
}
|
||||||
throw Exception('data 目录在 setup() 前验证失败: ${finalDataDir.path}');
|
throw Exception('data 目录在 setup() 前验证失败: ${finalDataDir.path}');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 再次尝试写入测试,确保目录确实可用
|
// 再次尝试写入测试,确保目录确实可用
|
||||||
try {
|
try {
|
||||||
final verifyFile = File(p.join(finalDataDir.path, '.verify_setup'));
|
final verifyFile = File(p.join(finalDataDir.path, '.verify_setup'));
|
||||||
@ -364,7 +378,7 @@ class KRSingBoxImp {
|
|||||||
KRLogUtil.kr_i(' - tempDir: ${kr_configDics.tempDir.path}', tag: 'SingBox');
|
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 目录: ${p.join(kr_configDics.workingDir.path, "data")}', tag: 'SingBox');
|
||||||
KRLogUtil.kr_i(' - data 目录存在: ${finalDataDir.existsSync()}', tag: 'SingBox');
|
KRLogUtil.kr_i(' - data 目录存在: ${finalDataDir.existsSync()}', tag: 'SingBox');
|
||||||
|
|
||||||
// 在 Windows 上,列出 data 目录内容(如果有文件)
|
// 在 Windows 上,列出 data 目录内容(如果有文件)
|
||||||
if (Platform.isWindows && finalDataDir.existsSync()) {
|
if (Platform.isWindows && finalDataDir.existsSync()) {
|
||||||
try {
|
try {
|
||||||
@ -378,10 +392,10 @@ class KRSingBoxImp {
|
|||||||
KRLogUtil.kr_e(' - 无法列出 data 目录内容: $e', tag: 'SingBox');
|
KRLogUtil.kr_e(' - 无法列出 data 目录内容: $e', tag: 'SingBox');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
KRLogUtil.kr_i(' - libcore 将通过 os.Chdir() 切换到: ${kr_configDics.workingDir.path}', tag: 'SingBox');
|
KRLogUtil.kr_i(' - libcore 将通过 os.Chdir() 切换到: ${kr_configDics.workingDir.path}', tag: 'SingBox');
|
||||||
KRLogUtil.kr_i(' - 然后使用相对路径 "./data" 访问数据库', tag: 'SingBox');
|
KRLogUtil.kr_i(' - 然后使用相对路径 "./data" 访问数据库', tag: 'SingBox');
|
||||||
|
|
||||||
// Windows 特定:验证路径格式是否正确
|
// Windows 特定:验证路径格式是否正确
|
||||||
if (Platform.isWindows) {
|
if (Platform.isWindows) {
|
||||||
final workingPath = kr_configDics.workingDir.path;
|
final workingPath = kr_configDics.workingDir.path;
|
||||||
@ -400,11 +414,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 +430,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 +469,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 +487,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 +584,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 +622,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,11 +647,11 @@ 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;
|
||||||
|
|
||||||
// 详细打印每个组的信息
|
// 详细打印每个组的信息
|
||||||
for (int i = 0; i < groups.length; i++) {
|
for (int i = 0; i < groups.length; i++) {
|
||||||
final group = groups[i];
|
final group = groups[i];
|
||||||
@ -638,7 +661,7 @@ class KRSingBoxImp {
|
|||||||
KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox');
|
KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
KRLogUtil.kr_i('✅ 活动组处理完成', tag: 'SingBox');
|
KRLogUtil.kr_i('✅ 活动组处理完成', tag: 'SingBox');
|
||||||
},
|
},
|
||||||
onError: (error) {
|
onError: (error) {
|
||||||
@ -651,7 +674,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 +702,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 +742,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 +1054,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 +1181,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 +1204,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 +1408,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 +1546,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');
|
||||||
@ -1473,13 +1565,45 @@ class KRSingBoxImp {
|
|||||||
|
|
||||||
if (country == 'auto') {
|
if (country == 'auto') {
|
||||||
KRLogUtil.kr_i('🌀 国家为 auto,执行自动节点选择逻辑', tag: 'SingBox');
|
KRLogUtil.kr_i('🌀 国家为 auto,执行自动节点选择逻辑', tag: 'SingBox');
|
||||||
await kr_selectOutbound('auto');
|
try {
|
||||||
|
// 保存国家为 auto,并重建/保存全部节点
|
||||||
|
await KRSecureStorage().kr_saveData(key: 'SELECTED_COUNTRY_TAG', value: 'auto');
|
||||||
|
await KRSecureStorage().kr_saveData(key: _keySelectedNode, value: 'auto');
|
||||||
|
await kr_selectOutbound('auto');
|
||||||
|
_nodeSelectionTimer?.cancel();
|
||||||
|
_nodeSelectionTimer = null;
|
||||||
|
KRLogUtil.kr_i('✅ 已切换为 auto 节点选择', tag: 'SingBox');
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_e('❌ auto 国家选择失败: $e', tag: 'SingBox');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await KRSecureStorage().kr_saveData(key: 'SELECTED_COUNTRY_TAG', value: country);
|
||||||
|
final oOption = _getConfigOption();
|
||||||
|
oOption["region"] = country;
|
||||||
|
KRLogUtil.kr_i('📝 更新 region 配置为国家: $country', tag: 'SingBox');
|
||||||
|
final op = SingboxConfigOption.fromJson(oOption);
|
||||||
|
await kr_singBox.changeOptions(op)
|
||||||
|
..map((r) {
|
||||||
|
KRLogUtil.kr_i('✅ 国家 region 更新成功: $country', tag: 'SingBox');
|
||||||
|
}).mapLeft((err) {
|
||||||
|
KRLogUtil.kr_e('❌ 更新国家 region 失败: $err', tag: 'SingBox');
|
||||||
|
throw err;
|
||||||
|
}).run();
|
||||||
|
|
||||||
|
// 设置为 auto 以便在该国集合内自动选最快
|
||||||
|
await KRSecureStorage().kr_saveData(key: _keySelectedNode, value: 'auto');
|
||||||
|
KRLogUtil.kr_i('ℹ️ 已应用国家到配置,等待控制器执行重启', tag: 'SingBox');
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_e('❌ 国家选择流程失败: $e', tag: 'SingBox');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
|
||||||
if (kr_activeGroups.isEmpty) {
|
if (kr_activeGroups.isEmpty) {
|
||||||
KRLogUtil.kr_w('⚠️ kr_activeGroups 当前为空,无法进行国家选择', tag: 'SingBox');
|
KRLogUtil.kr_w('⚠️ kr_activeGroups 当前为空,国家选择后将由重启同步分组', tag: 'SingBox');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1500,7 +1624,7 @@ class KRSingBoxImp {
|
|||||||
Future<void> kr_urlTest(String groupTag) async {
|
Future<void> kr_urlTest(String groupTag) async {
|
||||||
KRLogUtil.kr_i('🧪 开始 URL 测试: $groupTag', tag: 'SingBox');
|
KRLogUtil.kr_i('🧪 开始 URL 测试: $groupTag', tag: 'SingBox');
|
||||||
KRLogUtil.kr_i('📊 当前活动组数量: ${kr_activeGroups.length}', tag: 'SingBox');
|
KRLogUtil.kr_i('📊 当前活动组数量: ${kr_activeGroups.length}', tag: 'SingBox');
|
||||||
|
|
||||||
// 打印所有活动组信息
|
// 打印所有活动组信息
|
||||||
for (int i = 0; i < kr_activeGroups.length; i++) {
|
for (int i = 0; i < kr_activeGroups.length; i++) {
|
||||||
final group = kr_activeGroups[i];
|
final group = kr_activeGroups[i];
|
||||||
@ -1510,15 +1634,15 @@ class KRSingBoxImp {
|
|||||||
KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox');
|
KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
KRLogUtil.kr_i('🚀 调用 SingBox URL 测试 API...', tag: 'SingBox');
|
KRLogUtil.kr_i('🚀 调用 SingBox URL 测试 API...', tag: 'SingBox');
|
||||||
final result = await kr_singBox.urlTest(groupTag).run();
|
final result = await kr_singBox.urlTest(groupTag).run();
|
||||||
KRLogUtil.kr_i('✅ URL 测试完成: $groupTag, 结果: $result', tag: 'SingBox');
|
KRLogUtil.kr_i('✅ URL 测试完成: $groupTag, 结果: $result', tag: 'SingBox');
|
||||||
|
|
||||||
// 等待一段时间让 SingBox 完成测试
|
// 等待一段时间让 SingBox 完成测试
|
||||||
await Future.delayed(const Duration(seconds: 2));
|
await Future.delayed(const Duration(seconds: 2));
|
||||||
|
|
||||||
// 再次检查活动组状态
|
// 再次检查活动组状态
|
||||||
KRLogUtil.kr_i('🔄 测试后活动组状态检查:', tag: 'SingBox');
|
KRLogUtil.kr_i('🔄 测试后活动组状态检查:', tag: 'SingBox');
|
||||||
for (int i = 0; i < kr_activeGroups.length; i++) {
|
for (int i = 0; i < kr_activeGroups.length; i++) {
|
||||||
@ -1534,4 +1658,56 @@ 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');
|
||||||
|
|
||||||
|
// 1) TCP 到 1.1.1.1:443(避免纯 IP HTTP 被对端重置)
|
||||||
|
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 204 连接性检测,更通用更稳健)
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) TCP 53检测(公共DNS TCP53可达性)
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
KRLogUtil.kr_i('✅ [TUN Debug] 结束调试', tag: 'SingBoxTun');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user