From a47174df56be68bd2c0f1222c5741dedeacd0987 Mon Sep 17 00:00:00 2001 From: speakeloudest Date: Thu, 13 Nov 2025 05:15:18 -0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E9=97=AE=E9=A2=98=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/app/model/business/kr_outbound_item.dart | 137 +++++--- lib/app/model/response/kr_node_list.dart | 69 +++- .../hi_node_list/views/hi_node_list_view.dart | 6 +- .../controllers/kr_home_controller.dart | 2 + .../services/singbox_imp/kr_sing_box_imp.dart | 304 ++++++++++++++---- 5 files changed, 400 insertions(+), 118 deletions(-) diff --git a/lib/app/model/business/kr_outbound_item.dart b/lib/app/model/business/kr_outbound_item.dart index 5682007..bfc190f 100755 --- a/lib/app/model/business/kr_outbound_item.dart +++ b/lib/app/model/business/kr_outbound_item.dart @@ -28,13 +28,10 @@ class KROutboundItem { /// URL String url = ""; - @override - String toString() { - return 'KROutboundItem(tag: $tag, country: $country, delay: ${urlTestDelay.value}ms)'; - } + /// 服务器类型 - /// 构造函数,接受 KrNodeListItem 对象并初始化 KROutboundItem + /// 构造函数,接受 KrItem 对象并初始化 KROutboundItem KROutboundItem(KrNodeListItem nodeListItem) { id = nodeListItem.id.toString(); protocol = nodeListItem.protocol; @@ -84,13 +81,13 @@ class KROutboundItem { case "vless": final securityConfig = json["security_config"] as Map? ?? {}; - + // 智能设置 server_name String serverName = securityConfig["sni"] ?? ""; if (serverName.isEmpty) { serverName = nodeListItem.serverAddr; } - + config = { "type": "vless", "tag": nodeListItem.name, @@ -115,13 +112,13 @@ class KROutboundItem { case "vmess": final securityConfig = json["security_config"] as Map? ?? {}; - + // 智能设置 server_name String serverName = securityConfig["sni"] ?? ""; if (serverName.isEmpty) { serverName = nodeListItem.serverAddr; } - + config = { "type": "vmess", "tag": nodeListItem.name, @@ -152,7 +149,7 @@ class KROutboundItem { break; case "hysteria": case "hysteria2": - // 后端的 "hysteria" 实际上是 Hysteria2 协议 + // 后端的 "hysteria" 实际上是 Hysteria2 协议 final securityConfig = json["security_config"] as Map? ?? {}; config = { @@ -177,14 +174,14 @@ class KROutboundItem { case "trojan": final securityConfig = json["security_config"] as Map? ?? {}; - + // 智能设置 server_name String serverName = securityConfig["sni"] ?? ""; if (serverName.isEmpty) { // 如果没有配置 SNI,使用服务器地址 serverName = nodeListItem.serverAddr; } - + config = { "type": "trojan", "tag": nodeListItem.name, @@ -274,6 +271,7 @@ class KROutboundItem { // 🔧 尝试从 config 字段解析 transport 配置 Map? transportConfig; Map? securityConfig; + int actualPort = nodeListItem.port; // 🔧 使用局部变量存储实际端口 // 🔧 关键修复:优先从 protocols 字段解析配置 if (nodeListItem.protocols.isNotEmpty) { @@ -307,6 +305,28 @@ class KROutboundItem { } if (matchedProtocol != null) { + // 🔧 关键修复:只在顶层端口为0时,才使用 protocols 中的端口 + // 这样可以保留顶层的正确端口(如 53441),不被 protocols 数组中的端口(如 287)覆盖 + if (actualPort == 0 && matchedProtocol['port'] != null) { + // 安全解析端口号 + int protocolPort = 0; + final portValue = matchedProtocol['port']; + if (portValue is int) { + protocolPort = portValue; + } else if (portValue is String) { + protocolPort = int.tryParse(portValue) ?? 0; + } + + if (protocolPort > 0) { + actualPort = protocolPort; + if (kDebugMode) { + print(' ✅ 从 protocols 使用端口: $protocolPort (顶层端口为0)'); + } + } + } else if (kDebugMode && matchedProtocol['port'] != null) { + print(' ✅ 保留顶层端口: $actualPort (protocols中的端口: ${matchedProtocol['port']})'); + } + // 提取 transport 配置 if (matchedProtocol['network'] != null || matchedProtocol['transport'] != null) { final network = matchedProtocol['network'] ?? matchedProtocol['transport']; @@ -402,7 +422,7 @@ class KROutboundItem { switch (nodeListItem.protocol) { case "shadowsocks": - // 优先使用 protocols 解析出来的 cipher,其次是 method 字段,最后才是默认值 + // 优先使用 protocols 解析出来的 cipher,其次是 method 字段,最后才是默认值 String finalMethod = nodeListItem.method.isNotEmpty ? nodeListItem.method : "2022-blake3-aes-256-gcm"; @@ -411,22 +431,21 @@ class KROutboundItem { "type": "shadowsocks", "tag": nodeListItem.name, "server": nodeListItem.serverAddr, - "server_port": nodeListItem.port, + "server_port": actualPort, "method": finalMethod, "password": nodeListItem.uuid }; if (kDebugMode) { + print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); print('✅ Shadowsocks 节点配置构建成功: ${nodeListItem.name}'); - } - if (kDebugMode) { print('📄 使用加密方法: $finalMethod'); - } - if (kDebugMode) { - print('📄 完整配置: $config'); + print('📄 完整配置 JSON:'); + print(jsonEncode(config)); + print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); } break; case "vless": - // 判断是否为域名(非IP地址) + // 判断是否为域名(非IP地址) final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$') .hasMatch(nodeListItem.serverAddr); @@ -436,17 +455,25 @@ class KROutboundItem { serverName = securityConfig['sni'].toString(); } - // 🔧 关键修复:根据 security_config 判断是否启用 TLS - final bool vlessTlsEnabled = securityConfig?['tls_enabled'] ?? false; + // 🔧 关键修复:智能判断是否启用 TLS + // 1. 优先使用 securityConfig['tls_enabled'] + // 2. 如果没有明确配置,根据端口和域名智能判断 + bool vlessTlsEnabled = securityConfig?['tls_enabled'] ?? true; // 默认启用 TLS + + // 如果端口是标准非TLS端口(80, 8080等),则禁用 TLS + if (nodeListItem.port == 80 || nodeListItem.port == 8080) { + vlessTlsEnabled = false; + } + if (kDebugMode) { - print('🔐 VLESS TLS 状态: enabled=$vlessTlsEnabled'); + print('🔐 VLESS TLS 状态: enabled=$vlessTlsEnabled (port=${nodeListItem.port}, isDomain=$isDomain)'); } config = { "type": "vless", "tag": nodeListItem.name, "server": nodeListItem.serverAddr, - "server_port": nodeListItem.port, + "server_port": actualPort, "uuid": nodeListItem.uuid, if (transportConfig != null) "transport": transportConfig, if (vlessTlsEnabled) "tls": { @@ -460,14 +487,27 @@ class KROutboundItem { } }; if (kDebugMode) { + print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); print('✅ VLESS 节点配置构建成功: ${nodeListItem.name}'); - } - if (kDebugMode) { - print('📄 完整配置: $config'); + print('📋 原始节点信息:'); + print(' - serverAddr: ${nodeListItem.serverAddr}'); + print(' - port: ${nodeListItem.port}'); + print(' - uuid: ${nodeListItem.uuid}'); + print(' - protocols: ${nodeListItem.protocols}'); + print('🔐 安全配置:'); + print(' - TLS 启用: $vlessTlsEnabled'); + print(' - 是域名: $isDomain'); + print(' - server_name: $serverName'); + print(' - securityConfig: $securityConfig'); + print('📡 传输配置:'); + print(' - transportConfig: $transportConfig'); + print('📄 最终生成的完整配置 JSON:'); + print(jsonEncode(config)); + print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); } break; case "vmess": - // 判断是否为域名(非IP地址) + // 判断是否为域名(非IP地址) final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$') .hasMatch(nodeListItem.serverAddr); @@ -477,17 +517,25 @@ class KROutboundItem { serverName = securityConfig['sni'].toString(); } - // 🔧 关键修复:根据 security_config 判断是否启用 TLS - final bool tlsEnabled = securityConfig?['tls_enabled'] ?? false; + // 🔧 关键修复:智能判断是否启用 TLS + // 1. 优先使用 securityConfig['tls_enabled'] + // 2. 如果没有明确配置,根据端口和域名智能判断 + bool tlsEnabled = securityConfig?['tls_enabled'] ?? true; // 默认启用 TLS + + // 如果端口是标准非TLS端口(80, 8080等),则禁用 TLS + if (nodeListItem.port == 80 || nodeListItem.port == 8080) { + tlsEnabled = false; + } + if (kDebugMode) { - print('🔐 TLS 状态: enabled=$tlsEnabled'); + print('🔐 VMess TLS 状态: enabled=$tlsEnabled (port=${nodeListItem.port}, isDomain=$isDomain)'); } config = { "type": "vmess", "tag": nodeListItem.name, "server": nodeListItem.serverAddr, - "server_port": nodeListItem.port, + "server_port": actualPort, "uuid": nodeListItem.uuid, "alter_id": 0, "security": "auto", @@ -503,14 +551,27 @@ class KROutboundItem { } }; if (kDebugMode) { + print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); print('✅ VMess 节点配置构建成功: ${nodeListItem.name}'); - } - if (kDebugMode) { - print('📄 完整配置: $config'); + print('📋 原始节点信息:'); + print(' - serverAddr: ${nodeListItem.serverAddr}'); + print(' - port: ${nodeListItem.port}'); + print(' - uuid: ${nodeListItem.uuid}'); + print(' - protocols: ${nodeListItem.protocols}'); + print('🔐 安全配置:'); + print(' - TLS 启用: $tlsEnabled'); + print(' - 是域名: $isDomain'); + print(' - server_name: $serverName'); + print(' - securityConfig: $securityConfig'); + print('📡 传输配置:'); + print(' - transportConfig: $transportConfig'); + print('📄 最终生成的完整配置 JSON:'); + print(jsonEncode(config)); + print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); } break; case "trojan": - // 判断是否为域名(非IP地址) + // 判断是否为域名(非IP地址) final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$') .hasMatch(nodeListItem.serverAddr); @@ -524,7 +585,7 @@ class KROutboundItem { "type": "trojan", "tag": nodeListItem.name, "server": nodeListItem.serverAddr, - "server_port": nodeListItem.port, + "server_port": actualPort, "password": nodeListItem.uuid, if (transportConfig != null) "transport": transportConfig, "tls": { @@ -546,7 +607,7 @@ class KROutboundItem { break; case "hysteria": case "hysteria2": - // 后端的 "hysteria" 实际上是 Hysteria2 协议 + // 后端的 "hysteria" 实际上是 Hysteria2 协议 if (kDebugMode) { print('🔍 构建 Hysteria2 节点: ${nodeListItem.name}'); } diff --git a/lib/app/model/response/kr_node_list.dart b/lib/app/model/response/kr_node_list.dart index 97120ec..8b57413 100755 --- a/lib/app/model/response/kr_node_list.dart +++ b/lib/app/model/response/kr_node_list.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:kaer_with_panels/app/utils/kr_log_util.dart'; class KRNodeList { - final List list; + final List list; final String subscribeId; final String startTime; final String expireTime; @@ -35,8 +35,8 @@ class KRNodeList { // 查找 id 匹配的订阅项 try { subscribeData = listData.firstWhere( - (item) => (item as Map)['id']?.toString() == requestSubscribeId, - orElse: () => listData[0] as Map + (item) => (item as Map)['id']?.toString() == requestSubscribeId, + orElse: () => listData[0] as Map ) as Map; KRLogUtil.kr_i('✅ 找到匹配的订阅项: id=$requestSubscribeId', tag: 'NodeList'); } catch (e) { @@ -70,7 +70,7 @@ class KRNodeList { class KrNodeListItem { final int id; - String name; + String name; final String uuid; final String protocol; final String relayMode; @@ -136,25 +136,68 @@ class KrNodeListItem { String method = json['method']?.toString() ?? ''; // 加密方法(Shadowsocks等) final protocols = json['protocols']?.toString() ?? ''; // 协议配置JSON - // 🔧 如果有 protocols 字段,从中解析 port 和 cipher + // 🔧 打印原始节点 JSON(用于调试) + KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'NodeList'); + KRLogUtil.kr_i('📥 收到节点 API 原始数据:', tag: 'NodeList'); + KRLogUtil.kr_i('节点名称: ${json['name']}', tag: 'NodeList'); + KRLogUtil.kr_i('协议类型: ${json['protocol']}', tag: 'NodeList'); + KRLogUtil.kr_i('完整 JSON:', tag: 'NodeList'); + KRLogUtil.kr_i(jsonEncode(json), tag: 'NodeList'); + + // 🔧 如果有 protocols 字段,从中解析 cipher(但不覆盖顶层的 port) if (protocols.isNotEmpty) { try { final protocolsList = jsonDecode(protocols) as List; + final currentProtocol = json['protocol']?.toString().toLowerCase() ?? ''; + + KRLogUtil.kr_i('📋 protocols 字段内容 (${protocolsList.length} 个协议):', tag: 'NodeList'); + for (var i = 0; i < protocolsList.length; i++) { + KRLogUtil.kr_i(' 协议 $i: ${jsonEncode(protocolsList[i])}', tag: 'NodeList'); + } + if (protocolsList.isNotEmpty) { - final firstProtocol = protocolsList[0] as Map; - // 优先使用 protocols 中的配置 - if (firstProtocol['port'] != null) { - port = _parseIntSafely(firstProtocol['port']); + // 🔧 修复:查找与当前协议类型匹配的配置,而不是直接使用第一个 + Map? matchedProtocolConfig; + + // 尝试找到协议类型匹配的配置 + for (var protocolConfig in protocolsList) { + final configMap = protocolConfig as Map; + // 🔧 修复: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); + + // 🔧 关键修复:只在顶层没有 port 字段时,才使用 protocols 中的端口 + // 这样可以保留顶层的正确端口(如 53441),不被 protocols 数组中的端口(如 287)覆盖 + if (port == 0 && targetProtocol['port'] != null) { + port = _parseIntSafely(targetProtocol['port']); + KRLogUtil.kr_i('✅ 从 protocols 解析端口: $port', tag: 'NodeList'); + } else { + KRLogUtil.kr_i('✅ 保留顶层端口: $port (protocols中的端口: ${targetProtocol['port']})', tag: 'NodeList'); + } + + // 提取 cipher(加密方法) + if (targetProtocol['cipher'] != null && targetProtocol['cipher'].toString().isNotEmpty) { + method = targetProtocol['cipher'].toString(); + KRLogUtil.kr_i('✅ 从 protocols 解析 cipher: $method', tag: 'NodeList'); } - KRLogUtil.kr_i('从 protocols 解析: port=$port, cipher=$method', tag: 'NodeList'); } } catch (e) { - KRLogUtil.kr_w('解析 protocols 字段失败: $e', tag: 'NodeList'); + KRLogUtil.kr_w('⚠️ 解析 protocols 字段失败: $e', tag: 'NodeList'); } } + KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'NodeList'); return KrNodeListItem( id: _parseIntSafely(json['id']), diff --git a/lib/app/modules/hi_node_list/views/hi_node_list_view.dart b/lib/app/modules/hi_node_list/views/hi_node_list_view.dart index bbc111e..0919cfe 100755 --- a/lib/app/modules/hi_node_list/views/hi_node_list_view.dart +++ b/lib/app/modules/hi_node_list/views/hi_node_list_view.dart @@ -80,8 +80,8 @@ class HINodeListView extends GetView { // 并设置透明背景,让父组件的背景可以透出来 return Material( color: Colors.transparent, - // child: _buildSubscribeList(context) - child: _kr_buildRegionList(context) + child: _buildSubscribeList(context) + // child: _kr_buildRegionList(context) ); } @@ -380,7 +380,7 @@ class HINodeListView extends GetView { children: [ Flexible( child: Text( - controller.homeController.kr_getCountryFullName(item.country), + '${controller.homeController.kr_getCountryFullName(item.country)}-${item.tag}', style: KrAppTextStyle(fontSize: 14, color: Colors.white), overflow: TextOverflow.ellipsis, maxLines: 1, diff --git a/lib/app/modules/kr_home/controllers/kr_home_controller.dart b/lib/app/modules/kr_home/controllers/kr_home_controller.dart index 1f1b196..6815201 100755 --- a/lib/app/modules/kr_home/controllers/kr_home_controller.dart +++ b/lib/app/modules/kr_home/controllers/kr_home_controller.dart @@ -849,6 +849,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { if (kDebugMode) { } await KRSingBoxImp.instance.kr_start(); + KRLogUtil.kr_i('✅ 连接命令已发送', tag: 'HomeController'); if (kDebugMode) { } @@ -2185,6 +2186,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { kr_isConnected.value = true; kr_startConnectionTimer(); kr_updateConnectionInfo(); + KRSingBoxImp.instance.kr_debugTunConnectivity(); // 🔧 修复:同步已启动状态时,尝试更新延迟值 if (!_kr_tryUpdateDelayFromActiveGroups()) { diff --git a/lib/app/services/singbox_imp/kr_sing_box_imp.dart b/lib/app/services/singbox_imp/kr_sing_box_imp.dart index da6a1d3..7a272ae 100755 --- a/lib/app/services/singbox_imp/kr_sing_box_imp.dart +++ b/lib/app/services/singbox_imp/kr_sing_box_imp.dart @@ -107,6 +107,9 @@ class KRSingBoxImp { /// 初始化标志,防止重复初始化 bool _kr_isInitialized = false; + /// 初始化进行中共享 Future(single-flight 防并发) + Future? _kr_initFuture; + /// 当前混合代理端口是否就绪 bool get kr_isProxyReady => kr_status.value is SingboxStarted; @@ -139,12 +142,23 @@ class KRSingBoxImp { /// 初始化 Future init() async { - // 防止重复初始化 + // 防止重复初始化(已完成) if (_kr_isInitialized) { KRLogUtil.kr_i('SingBox 已经初始化,跳过重复初始化', tag: 'SingBox'); return; } - + + // 防止并发重复初始化(进行中复用同一个 Future) + if (_kr_initFuture != null) { + KRLogUtil.kr_i('SingBox 初始化进行中,等待完成(single-flight)', tag: 'SingBox'); + await _kr_initFuture; + return; + } + + // 建立 single-flight 共享 Future + final completer = Completer(); + _kr_initFuture = completer.future; + try { KRLogUtil.kr_i('开始初始化 SingBox'); // 在应用启动时初始化 @@ -167,16 +181,16 @@ class KRSingBoxImp { KRLogUtil.kr_i('iOS 路径获取完成: $paths'); kr_configDics = ( - baseDir: Directory(paths?["base"]! as String), - workingDir: Directory(paths?["working"]! as String), - tempDir: Directory(paths?["temp"]! as String), + baseDir: Directory(paths?["base"]! as String), + workingDir: Directory(paths?["working"]! as String), + tempDir: Directory(paths?["temp"]! as String), ); } else { final baseDir = await getApplicationSupportDirectory(); final workingDir = - Platform.isAndroid ? await getExternalStorageDirectory() : baseDir; + Platform.isAndroid ? await getExternalStorageDirectory() : baseDir; final tempDir = await getTemporaryDirectory(); - + // Windows 路径规范化:确保使用正确的路径分隔符 Directory normalizePath(Directory dir) { if (Platform.isWindows) { @@ -188,11 +202,11 @@ class KRSingBoxImp { } return dir; } - + kr_configDics = ( - baseDir: normalizePath(baseDir), - workingDir: normalizePath(workingDir!), - tempDir: normalizePath(tempDir), + baseDir: normalizePath(baseDir), + workingDir: normalizePath(workingDir!), + tempDir: normalizePath(tempDir), ); KRLogUtil.kr_i('其他平台路径初始化完成'); } @@ -201,7 +215,7 @@ class KRSingBoxImp { KRLogUtil.kr_i('baseDir: ${kr_configDics.baseDir.path}', tag: 'SingBox'); KRLogUtil.kr_i('workingDir: ${kr_configDics.workingDir.path}', tag: 'SingBox'); KRLogUtil.kr_i('tempDir: ${kr_configDics.tempDir.path}', tag: 'SingBox'); - + // 确保所有目录都存在 if (!kr_configDics.baseDir.existsSync()) { await kr_configDics.baseDir.create(recursive: true); @@ -215,11 +229,11 @@ class KRSingBoxImp { await kr_configDics.tempDir.create(recursive: true); KRLogUtil.kr_i('已创建 tempDir', tag: 'SingBox'); } - + // 创建 libcore 数据库所需的 data 目录(在 workingDir 下) // 注意:libcore 的 Setup 会调用 os.Chdir(workingPath),所以 data 目录必须在 workingDir 下 final dataDir = Directory(p.join(kr_configDics.workingDir.path, 'data')); - + // 强制确保 data 目录存在(Windows 可能需要多次尝试) int retryCount = 0; const maxRetries = 5; @@ -228,7 +242,7 @@ class KRSingBoxImp { await dataDir.create(recursive: true); // 等待文件系统同步(Windows 上可能需要一点时间) await Future.delayed(const Duration(milliseconds: 100)); - + // 验证目录确实创建成功 if (dataDir.existsSync()) { KRLogUtil.kr_i('✅ 已创建 data 目录: ${dataDir.path}', tag: 'SingBox'); @@ -248,13 +262,13 @@ class KRSingBoxImp { await Future.delayed(Duration(milliseconds: delayMs)); } } - + if (!dataDir.existsSync()) { final error = 'data 目录不存在: ${dataDir.path}'; KRLogUtil.kr_e('❌ $error', tag: 'SingBox'); throw Exception(error); } - + // 验证目录权限(尝试创建一个测试文件) try { final testFile = File(p.join(dataDir.path, '.test_write')); @@ -265,13 +279,13 @@ class KRSingBoxImp { KRLogUtil.kr_e('⚠️ data 目录写入权限验证失败: $e', tag: 'SingBox'); // 不抛出异常,让 libcore 自己处理 } - + // 在 Windows 上额外等待,确保文件系统操作完成 if (Platform.isWindows) { await Future.delayed(const Duration(milliseconds: 300)); KRLogUtil.kr_i('⏳ Windows 文件系统同步等待完成', tag: 'SingBox'); } - + // 最终验证:在 setup() 之前再次确认 workingDir 和 data 目录都存在且可访问 // libcore 的 Setup() 会调用 os.Chdir(workingPath),然后使用相对路径 "./data" // 如果 os.Chdir() 失败(路径不存在或权限问题),后续的相对路径访问会失败 @@ -280,7 +294,7 @@ class KRSingBoxImp { KRLogUtil.kr_e(error, tag: 'SingBox'); throw Exception(error); } - + // 验证 workingDir 可读可写 try { final testWorkingFile = File(p.join(kr_configDics.workingDir.path, '.test_working_dir')); @@ -292,7 +306,7 @@ class KRSingBoxImp { KRLogUtil.kr_e(error, tag: 'SingBox'); throw Exception(error); } - + final finalDataDir = Directory(p.join(kr_configDics.workingDir.path, 'data')); if (!finalDataDir.existsSync()) { KRLogUtil.kr_e('❌ 最终验证失败:data 目录不存在', tag: 'SingBox'); @@ -308,7 +322,7 @@ class KRSingBoxImp { } throw Exception('data 目录在 setup() 前验证失败: ${finalDataDir.path}'); } - + // 再次尝试写入测试,确保目录确实可用 try { 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(' - data 目录: ${p.join(kr_configDics.workingDir.path, "data")}', tag: 'SingBox'); KRLogUtil.kr_i(' - data 目录存在: ${finalDataDir.existsSync()}', tag: 'SingBox'); - + // 在 Windows 上,列出 data 目录内容(如果有文件) if (Platform.isWindows && finalDataDir.existsSync()) { try { @@ -378,10 +392,10 @@ class KRSingBoxImp { KRLogUtil.kr_e(' - 无法列出 data 目录内容: $e', tag: 'SingBox'); } } - + KRLogUtil.kr_i(' - libcore 将通过 os.Chdir() 切换到: ${kr_configDics.workingDir.path}', tag: 'SingBox'); KRLogUtil.kr_i(' - 然后使用相对路径 "./data" 访问数据库', tag: 'SingBox'); - + // Windows 特定:验证路径格式是否正确 if (Platform.isWindows) { final workingPath = kr_configDics.workingDir.path; @@ -400,11 +414,11 @@ class KRSingBoxImp { KRLogUtil.kr_i('📡 开始调用 setup() 注册 FFI 端口', tag: 'SingBox'); final setupResult = await kr_singBox.setup(kr_configDics, false).run(); setupResult.match( - (error) { + (error) { KRLogUtil.kr_e('❌ setup() 失败: $error', tag: 'SingBox'); throw Exception('FFI setup 失败: $error'); }, - (_) { + (_) { KRLogUtil.kr_i('✅ setup() 成功,FFI 端口已注册', tag: 'SingBox'); }, ); @@ -416,6 +430,10 @@ class KRSingBoxImp { // 这样可以确保 UI 始终与 libcore 的实际状态同步 _kr_subscribeToStatus(); KRLogUtil.kr_i('✅ 状态订阅已设置', tag: 'SingBox'); + + // 完成 single-flight + completer.complete(); + _kr_initFuture = null; } catch (e, stackTrace) { KRLogUtil.kr_e('❌ SingBox 初始化失败: $e'); KRLogUtil.kr_e('📚 错误堆栈: $stackTrace'); @@ -451,6 +469,11 @@ class KRSingBoxImp { // 如果初始化失败,允许下次重试 _kr_isInitialized = false; + // 失败时通知等待者并清理 single-flight + if (!completer.isCompleted) { + completer.completeError(e, stackTrace); + } + _kr_initFuture = null; rethrow; } } @@ -464,13 +487,13 @@ class KRSingBoxImp { "region": "other", // 参考 hiddify-app: 默认使用 "other" 跳过规则集下载 "block-ads": false, // 参考 hiddify-app: 默认关闭广告拦截 "use-xray-core-when-possible": false, - "execute-config-as-is": false, + "execute-config-as-is": true, // 🔧 修复:使用我们生成的完整配置,不让 libcore 重写 "log-level": "info", // 调试阶段使用 info,生产环境改为 warn "resolve-destination": false, "ipv6-mode": "ipv4_only", // 参考 hiddify-app: 仅使用 IPv4 (有效值: ipv4_only, prefer_ipv4, prefer_ipv6, ipv6_only) - "remote-dns-address": "udp://1.1.1.1", // 参考 hiddify-app: 使用 Cloudflare DNS + "remote-dns-address": "https://dns.google/dns-query", // 使用 Google DoH,避免中转节点 DNS 死锁 "remote-dns-domain-strategy": "prefer_ipv4", - "direct-dns-address": "udp://1.1.1.1", // 参考 hiddify-app: 统一使用 1.1.1.1 + "direct-dns-address": "local" , "direct-dns-domain-strategy": "prefer_ipv4", "mixed-port": kr_port, "tproxy-port": kr_port, @@ -561,7 +584,7 @@ class KRSingBoxImp { _kr_subscriptions.add( kr_singBox.watchStatus().listen( - (status) { + (status) { if (kDebugMode) { print('🔵 收到 Native 状态更新: ${status.runtimeType}'); } @@ -599,7 +622,7 @@ class KRSingBoxImp { // 所以外层必须有 try-catch final stream = kr_singBox.watchStats(); final subscription = stream.listen( - (stats) { + (stats) { kr_stats.value = stats; }, onError: (error) { @@ -624,11 +647,11 @@ class KRSingBoxImp { _kr_subscriptions.add( kr_singBox.watchActiveGroups().listen( - (groups) { + (groups) { print('[watchActiveGroups] 📡 收到活动组更新,数量: ${groups.length}'); KRLogUtil.kr_i('📡 收到活动组更新,数量: ${groups.length}', tag: 'SingBox'); kr_activeGroups.value = groups; - + // 详细打印每个组的信息 for (int i = 0; i < groups.length; i++) { final group = groups[i]; @@ -638,7 +661,7 @@ class KRSingBoxImp { KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox'); } } - + KRLogUtil.kr_i('✅ 活动组处理完成', tag: 'SingBox'); }, onError: (error) { @@ -651,7 +674,7 @@ class KRSingBoxImp { _kr_subscriptions.add( kr_singBox.watchGroups().listen( - (groups) { + (groups) { print('[watchGroups] 📡 收到所有组更新,数量: ${groups.length}'); kr_allGroups.value = groups; // 打印每个组的基本信息 @@ -679,7 +702,7 @@ class KRSingBoxImp { // 查找 "select" 组 final selectGroup = kr_activeGroups.firstWhere( - (group) => group.tag == 'select', + (group) => group.tag == 'select', orElse: () => throw Exception('未找到 "select" 选择器组'), ); @@ -719,11 +742,11 @@ class KRSingBoxImp { /// /// 确保 command.sock 准备好后再执行节点选择 Future _kr_selectOutboundWithRetry( - String groupTag, - String outboundTag, { - int maxAttempts = 3, - int initialDelay = 100, - }) async { + String groupTag, + String outboundTag, { + int maxAttempts = 3, + int initialDelay = 100, + }) async { int attempt = 0; int delay = initialDelay; @@ -1031,17 +1054,86 @@ class KRSingBoxImp { KRLogUtil.kr_i('✅ 过滤后节点数量: ${kr_outbounds.length}/${outbounds.length}', tag: 'SingBox'); - // 只保存 outbounds,Mobile.buildConfig() 会添加其他配置 - final map = { - "outbounds": kr_outbounds + // 🔧 修复:生成完整的 SingBox 配置 + // 之前只保存 {"outbounds": [...]}, 导致 libcore 无法正确处理 + // 现在保存完整配置,包含所有必需字段 + final Map 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 temp = _tempFile(kr_configName); - final mapStr = jsonEncode(map); + final mapStr = jsonEncode(fullConfig); - KRLogUtil.kr_i('📄 配置文件内容长度: ${mapStr.length}', tag: 'SingBox'); - KRLogUtil.kr_i('📄 完整配置文件内容: $mapStr', tag: 'SingBox'); + KRLogUtil.kr_i('📄 完整配置文件长度: ${mapStr.length}', tag: 'SingBox'); + KRLogUtil.kr_i('📄 Outbounds 数量: ${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 temp.writeAsString(mapStr); @@ -1089,11 +1181,11 @@ class KRSingBoxImp { final oOption = SingboxConfigOption.fromJson(_getConfigOption()); final changeResult = await kr_singBox.changeOptions(oOption).run(); changeResult.match( - (error) { + (error) { KRLogUtil.kr_e('❌ changeOptions() 失败: $error', tag: 'SingBox'); throw Exception('初始化 HiddifyOptions 失败: $error'); }, - (_) { + (_) { KRLogUtil.kr_i('✅ HiddifyOptions 初始化成功', tag: 'SingBox'); }, ); @@ -1112,7 +1204,7 @@ class KRSingBoxImp { _kr_subscribeToStatus(); await kr_singBox.start(_cutPath, kr_configName, false).map( - (r) { + (r) { KRLogUtil.kr_i('✅ SingBox 启动成功', tag: 'SingBox'); }, ).mapLeft((err) { @@ -1316,9 +1408,9 @@ class KRSingBoxImp { mode = KRCountryUtil.kr_getCurrentCountryCode(); KRLogUtil.kr_i('🎯 切换到规则代理模式: $mode', tag: 'SingBox'); break; - // case KRConnectionType.direct: - // mode = "direct"; - // break; + // case KRConnectionType.direct: + // mode = "direct"; + // break; } oOption["region"] = mode; KRLogUtil.kr_i('📝 更新 region 配置: $mode', tag: 'SingBox'); @@ -1454,8 +1546,8 @@ class KRSingBoxImp { // 使用 then/catchError 避免异常导致 UI 阻塞 kr_singBox.selectOutbound("select", tag).run().then((result) { result.match( - (error) => KRLogUtil.kr_w('🔁 定时器重选节点失败: $error', tag: 'SingBox'), - (_) => KRLogUtil.kr_d('🔁 定时器重选节点成功', tag: 'SingBox'), + (error) => KRLogUtil.kr_w('🔁 定时器重选节点失败: $error', tag: 'SingBox'), + (_) => KRLogUtil.kr_d('🔁 定时器重选节点成功', tag: 'SingBox'), ); }).catchError((error) { KRLogUtil.kr_w('🔁 定时器重选节点异常: $error', tag: 'SingBox'); @@ -1473,13 +1565,45 @@ class KRSingBoxImp { if (country == 'auto') { 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; } + 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) { - KRLogUtil.kr_w('⚠️ kr_activeGroups 当前为空,无法进行国家选择', tag: 'SingBox'); - return; + KRLogUtil.kr_w('⚠️ kr_activeGroups 当前为空,国家选择后将由重启同步分组', tag: 'SingBox'); } } @@ -1500,7 +1624,7 @@ class KRSingBoxImp { Future kr_urlTest(String groupTag) async { KRLogUtil.kr_i('🧪 开始 URL 测试: $groupTag', tag: 'SingBox'); KRLogUtil.kr_i('📊 当前活动组数量: ${kr_activeGroups.length}', tag: 'SingBox'); - + // 打印所有活动组信息 for (int i = 0; i < kr_activeGroups.length; i++) { final group = kr_activeGroups[i]; @@ -1510,15 +1634,15 @@ class KRSingBoxImp { KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox'); } } - + try { KRLogUtil.kr_i('🚀 调用 SingBox URL 测试 API...', tag: 'SingBox'); final result = await kr_singBox.urlTest(groupTag).run(); KRLogUtil.kr_i('✅ URL 测试完成: $groupTag, 结果: $result', tag: 'SingBox'); - + // 等待一段时间让 SingBox 完成测试 await Future.delayed(const Duration(seconds: 2)); - + // 再次检查活动组状态 KRLogUtil.kr_i('🔄 测试后活动组状态检查:', tag: 'SingBox'); for (int i = 0; i < kr_activeGroups.length; i++) { @@ -1534,4 +1658,56 @@ class KRSingBoxImp { KRLogUtil.kr_e('📚 错误详情: ${e.toString()}', tag: 'SingBox'); } } -} \ No newline at end of file + + Future 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'); + } +}