diff --git a/lib/app/model/business/kr_outbound_item.dart b/lib/app/model/business/kr_outbound_item.dart index bfc190f..2091469 100755 --- a/lib/app/model/business/kr_outbound_item.dart +++ b/lib/app/model/business/kr_outbound_item.dart @@ -28,11 +28,12 @@ class KROutboundItem { /// URL String url = ""; - + // ✅ 1. 将传入的 nodeListItem 保存为类的 final 成员变量 + final KrNodeListItem nodeListItem; /// 服务器类型 /// 构造函数,接受 KrItem 对象并初始化 KROutboundItem - KROutboundItem(KrNodeListItem nodeListItem) { + KROutboundItem(this.nodeListItem) { id = nodeListItem.id.toString(); protocol = nodeListItem.protocol; latitude = nodeListItem.latitude; @@ -211,6 +212,11 @@ class KROutboundItem { // 解析配置 } + @override + String toString() { + // 现在它可以正确地访问已保存的 nodeListItem 成员 + return 'KROutboundItem(name: ${nodeListItem.name}, protocol: ${nodeListItem.protocol}, server: ${nodeListItem.serverAddr}, port: ${nodeListItem.port})'; + } /// 构建传输配置 Map _buildTransport(Map json) { final transportType = json["transport"] as String?; 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 0919cfe..9096a48 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 @@ -1,6 +1,5 @@ // hi_node_list_view.dart -import 'dart:math'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -80,8 +79,8 @@ class HINodeListView extends GetView { // 并设置透明背景,让父组件的背景可以透出来 return Material( color: Colors.transparent, - child: _buildSubscribeList(context) - // child: _kr_buildRegionList(context) + // child: _buildSubscribeList(context) + child: _kr_buildRegionList(context) ); } @@ -109,7 +108,7 @@ class HINodeListView extends GetView { onTap: () async { try { final success = - await controller.homeController.kr_performCountrySwitch('auto'); + await controller.homeController.kr_performNodeSwitch('auto'); if (success) { controller.homeController.kr_currentListStatus.value = KRHomeViewsListStatus.kr_none; @@ -193,8 +192,10 @@ class HINodeListView extends GetView { return InkWell( onTap: () async { try { + final success = - await controller.homeController.kr_performCountrySwitch(country.country); + 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; @@ -311,7 +312,23 @@ class HINodeListView extends GetView { ), ...controller.kr_subscribeService.allList.map((item) { return InkWell( - onTap: () => _onNodeSelected(item), + // 🔧 修复:改为 async,等待节点切换完成后再关闭列表 + onTap: () async { + try { + KRLogUtil.kr_i( + '🔄 用户点击节点: ${item.tag}'); + final success = await controller.homeController + .kr_performNodeSwitch(item.tag); + if (success) { + controller.homeController.kr_currentListStatus.value = + KRHomeViewsListStatus.kr_none; + } + } catch (e) { + KRLogUtil.kr_e( + '节点切换异常: $e', + tag: 'NodeListView'); + } + }, child: _kr_buildNodeListItem(context, item: item), ); }).toList(), @@ -524,7 +541,7 @@ class HINodeListView extends GetView { ); }), SizedBox(width: 12.w), - Obx(() => controller.homeController.kr_coutryText.value == country.country + Obx(() => controller.homeController.kr_cutTag.value == '${country.country}-auto' ? KrLocalImage( imageName: 'radio-active-icon', imageType: ImageType.svg, 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 6815201..f2eaed5 100755 --- a/lib/app/modules/kr_home/controllers/kr_home_controller.dart +++ b/lib/app/modules/kr_home/controllers/kr_home_controller.dart @@ -1276,107 +1276,6 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { } } - /// 🌍 执行国家切换(包含UI同步与VPN热重载) - /// 返回 true 表示切换成功,false 表示失败 - Future kr_performCountrySwitch(String countryTag) async { - try { - KRLogUtil.kr_i('🔄 开始切换国家: $countryTag', tag: 'HomeController'); - - // 1. 保存原国家,以备失败恢复 - final originalCountry = kr_coutryText.value; - - // 2. 设置切换中状态 - kr_coutryText.value = countryTag; - kr_currentNodeName.value = countryTag; // UI显示当前国家 - - // 3. 获取该国家的节点列表 - final countryData = kr_subscribeService.countryOutboundList - .firstWhereOrNull((c) => c.country.toUpperCase() == countryTag.toUpperCase()); - - if (countryData == null || countryData.outboundList.isEmpty) { - KRLogUtil.kr_w('⚠️ 未找到国家 [$countryTag] 的节点列表', tag: 'HomeController'); - KRCommonUtil.kr_showToast('该国家暂无可用节点'); - return false; - } - - // 取第一个节点作为默认节点(后续由 sing-box 自动在该组内切换) - final defaultNode = countryData.outboundList.first.tag; - KRLogUtil.kr_i( - '📊 国家 [$countryTag] 包含 ${countryData.outboundList.length} 个节点,默认节点: $defaultNode', - tag: 'HomeController', - ); - - // 4. 如果VPN未连接,只更新UI变量即可 - if (!kr_isConnected.value) { - KRLogUtil.kr_i('📴 VPN未连接,只更新UI变量: $countryTag', tag: 'HomeController'); - kr_cutSeletedTag.value = defaultNode; - kr_cutTag.value = defaultNode; - kr_currentNodeLatency.value = -2; - - // 保存国家选择与默认节点 - await KRSecureStorage().kr_saveData(key: 'SELECTED_COUNTRY_TAG', value: countryTag); - await KRSecureStorage().kr_saveData(key: 'SELECTED_NODE_TAG', value: defaultNode); - - KRLogUtil.kr_i('✅ 已保存国家选择 [$countryTag] (未连接状态)', tag: 'HomeController'); - return true; - } - - // 5. VPN已连接状态,执行完整重载逻辑 - try { - KRLogUtil.kr_i('🔌 VPN已连接,准备切换国家 [$countryTag]', tag: 'HomeController'); - - // 显示连接中 - kr_currentNodeLatency.value = -1; - kr_isLatency.value = true; - - // 保存选择 - await KRSecureStorage().kr_saveData(key: 'SELECTED_COUNTRY_TAG', value: countryTag); - await KRSecureStorage().kr_saveData(key: 'SELECTED_NODE_TAG', value: defaultNode); - - // 🔧 调用 SingBox 层逻辑切换国家分组(稍后在 kr_sing_box_imp.dart 实现) - KRLogUtil.kr_i('🧩 调用 sing-box 切换国家分组逻辑: $countryTag', tag: 'HomeController'); - await KRSingBoxImp.instance.kr_selectCountry(countryTag); - - // 🔁 停止并重启 VPN - await KRSingBoxImp.instance.kr_stop(); - KRLogUtil.kr_i('⏳ 等待VPN停止(1500ms)', tag: 'HomeController'); - await Future.delayed(const Duration(milliseconds: 1500)); - - await KRSingBoxImp.instance.kr_start(); - KRLogUtil.kr_i('⏳ 等待VPN启动(2500ms)', tag: 'HomeController'); - await Future.delayed(const Duration(milliseconds: 2500)); - - // ✅ 切换成功,更新UI - kr_cutSeletedTag.value = defaultNode; - kr_cutTag.value = defaultNode; - kr_updateConnectionInfo(); - - // 更新延迟信息 - _kr_updateLatencyOnConnected(); - - KRLogUtil.kr_i('✅ 国家切换成功: $countryTag', tag: 'HomeController'); - return true; - } catch (switchError) { - KRLogUtil.kr_e('❌ 国家切换失败: $switchError', tag: 'HomeController'); - - // 恢复原国家状态 - kr_coutryText.value = originalCountry; - await KRSecureStorage().kr_saveData(key: 'SELECTED_COUNTRY_TAG', value: originalCountry ?? ''); - - KRCommonUtil.kr_showToast('切换国家失败,已恢复为: ${originalCountry ?? "无"}'); - return false; - } - } catch (e) { - KRLogUtil.kr_e('❌ 国家切换异常: $e', tag: 'HomeController'); - KRCommonUtil.kr_showToast('国家切换异常,请重试'); - return false; - } finally { - kr_isLatency.value = false; - KRLogUtil.kr_i('🔄 国家切换流程完成', tag: 'HomeController'); - } - } - - /// 🔧 修复:简化的 kr_selectNode 方法 /// 现在只是委托给新的 kr_performNodeSwitch 方法 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 7a272ae..94cad42 100755 --- a/lib/app/services/singbox_imp/kr_sing_box_imp.dart +++ b/lib/app/services/singbox_imp/kr_sing_box_imp.dart @@ -16,6 +16,7 @@ import 'package:path/path.dart' as p; import '../../../core/model/directories.dart'; import '../../../singbox/model/singbox_config_option.dart'; import '../../../singbox/model/singbox_outbound.dart'; +import '../../../singbox/model/singbox_proxy_type.dart'; import '../../../singbox/model/singbox_stats.dart'; import '../../../singbox/model/singbox_status.dart'; import '../../utils/kr_country_util.dart'; @@ -23,6 +24,8 @@ import '../../utils/kr_log_util.dart'; import '../../utils/kr_secure_storage.dart'; import '../../common/app_run_data.dart'; import 'package:flutter/foundation.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; + enum KRConnectionType { global, @@ -1557,57 +1560,6 @@ class KRSingBoxImp { } } - /// 🌍 国家级节点选择 - /// 如果传入 'auto',则调用原有 kr_selectOutbound('auto') - /// 否则根据国家名筛选该国节点,自动选择延迟最低的节点 - Future kr_selectCountry(String country) async { - KRLogUtil.kr_i('🌎 [v2.1] 开始选择国家: $country', tag: 'SingBox'); - - if (country == 'auto') { - KRLogUtil.kr_i('🌀 国家为 auto,执行自动节点选择逻辑', tag: 'SingBox'); - 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'); - } - - } - /// 配合文件地址 @@ -1671,19 +1623,30 @@ class KRSingBoxImp { 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 被对端重置) + // 打印每个 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)); + 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 连接性检测,更通用更稳健) + // 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')); + 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'); @@ -1691,23 +1654,177 @@ class KRSingBoxImp { KRLogUtil.kr_e('🔐 HTTPS 204 错误: $e', tag: 'SingBoxTun'); } - // 3) 系统DNS解析(走系统解析器) + // 3️⃣ 系统DNS解析 try { final addrs = await InternetAddress.lookup('google.com'); - KRLogUtil.kr_i('🧭 DNS解析成功: ${addrs.map((a) => a.address).join(", ")}', tag: 'SingBoxTun'); + 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可达性) + // 3️⃣ 系统DNS解析 try { - final sock = await Socket.connect('8.8.8.8', 53, timeout: const Duration(seconds: 3)); + 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(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 kr_probeDoH(List 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'); + } + } + } + }