From 909020654f93a9f62e68b500fc432faadf92851c Mon Sep 17 00:00:00 2001 From: speakeloudest Date: Tue, 25 Nov 2025 23:06:20 -0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=8A=82=E7=82=B9=E6=B5=8B=E9=80=9F?= =?UTF-8?q?=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/app/model/business/kr_outbounds_list.dart | 51 +--- .../controllers/hi_node_list_controller.dart | 7 + .../hi_node_list/views/hi_node_list_view.dart | 58 ++-- .../hi_node_list/views/hi_page_node_view.dart | 5 +- lib/app/services/kr_subscribe_service.dart | 3 - .../services/singbox_imp/kr_sing_box_imp.dart | 251 ++++++------------ 6 files changed, 112 insertions(+), 263 deletions(-) diff --git a/lib/app/model/business/kr_outbounds_list.dart b/lib/app/model/business/kr_outbounds_list.dart index e0978a7..53a650a 100755 --- a/lib/app/model/business/kr_outbounds_list.dart +++ b/lib/app/model/business/kr_outbounds_list.dart @@ -8,7 +8,7 @@ import 'package:flutter/foundation.dart'; /// 表示出站项列表的模型类 class KrOutboundsList { - + /// 服务器分组 final List groupOutboundList = []; // 存储服务器分组的列表 @@ -18,20 +18,20 @@ class KrOutboundsList { /// 全部列表 final List allList = []; // 存储国家分组的列表 - + // 配置json - final List> configJsonList = []; + final List> configJsonList = []; /// 标签列表 final Map keyList = {}; // 存储国家分组的列表 - + /// 处理出站项并将其分组 /// [list] 是要处理的出站项列表 void processOutboundItems(List list,List groupList) { final Map> tagGroups = {}; final Map> countryGroups = {}; - + // 用于追踪已使用的标签 final Map tagCounter = {}; @@ -88,14 +88,11 @@ class KrOutboundsList { } } - // 生成国家自动选择虚拟节点 - _generateCountryAutoNodes(countryGroups); - // 将标签分组转换为 KRGroupOutboundList 并添加到 groupOutboundList for (var tag in tagGroups.keys) { - final item = KRGroupOutboundList( + final item = KRGroupOutboundList( tag: tag, outboundList: tagGroups[tag]!); - + for (var group in groupList) { if (item.tag == group.name) { item.icon = group.icon; @@ -111,41 +108,7 @@ class KrOutboundsList { country: country, outboundList: countryGroups[country]!)); // 添加国家分组到列表 } - } - /// 生成国家自动选择虚拟节点 - void _generateCountryAutoNodes(Map> countryGroups) { - for (var entry in countryGroups.entries) { - final country = entry.key; - final nodes = entry.value; - final autoTag = '${country}-auto'; - if (kDebugMode) { - print('🤖 生成国家自动选择节点: $autoTag, 包含 ${nodes.length} 个节点'); - } - - // 构建 urltest 配置 - final urltestConfig = { - 'type': 'urltest', - 'tag': autoTag, - 'outbounds': nodes.map((node) => node.tag).toList(), - 'url': 'https://www.google.com/generate_204', - 'interval': '10m', - 'tolerance': 50, - "interrupt_exist_connections": true, - }; - - // 创建虚拟节点 - final virtualNode = KROutboundItem.fromVirtual(autoTag, country, urltestConfig); - - // 添加到各个列表 - allList.add(virtualNode); - keyList[autoTag] = virtualNode; - // configJsonList.add(urltestConfig); - - if (kDebugMode) { - print('✅ 生成虚拟节点: $autoTag, 配置: ${urltestConfig.toString()}'); - } - } } } diff --git a/lib/app/modules/hi_node_list/controllers/hi_node_list_controller.dart b/lib/app/modules/hi_node_list/controllers/hi_node_list_controller.dart index 1927d43..069e082 100644 --- a/lib/app/modules/hi_node_list/controllers/hi_node_list_controller.dart +++ b/lib/app/modules/hi_node_list/controllers/hi_node_list_controller.dart @@ -95,6 +95,13 @@ class HINodeListController extends GetxController with WidgetsBindingObserver { KRLogUtil.kr_i('调试模式已重置', tag: 'HINodeListController'); } + Future kr_handleRefresh() async { + await kr_subscribeService.kr_refreshAll(); + if (!homeController.kr_isLatency.value) { + homeController.kr_urlTest(); + } + } + @override void onInit() { super.onInit(); 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 ae8d008..0648876 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 @@ -23,22 +23,6 @@ class HINodeListView extends GetView { static const Color krModernGreen = Color(0xFF4CAF50); static const Color krModernGreenLight = Color(0xFF81C784); - // 存储随机延迟值,用于UI展示 - static final Map _fakeDelays = {}; - - /// 获取用于显示的延迟值 - int _getDisplayDelay(HINodeListController controller, KROutboundItem item) { - return item.urlTestDelay.value; - // if (controller.homeController.kr_isConnected.value) { - // - // } - // if (!_fakeDelays.containsKey(item.tag)) { - // final random = Random(); - // _fakeDelays[item.tag] = 30 + random.nextInt(71); // 30-100ms - // } - // return _fakeDelays[item.tag] ?? 0; - } - /// 获取分组内最快节点的延迟值(单位:ms) /// 如果列表为空,返回 0 int getFastestNodeDelay( @@ -77,14 +61,18 @@ class HINodeListView extends GetView { Widget build(BuildContext context) { // 1. 使用 Material 作为根组件,确保 InkWell 的水波纹效果正常 // 并设置透明背景,让父组件的背景可以透出来 - return Material( - color: Colors.transparent, - child: _buildSubscribeList(context) - // child: _kr_buildRegionList(context) - ); + return Obx(() { + // Material 保持透明背景,让 InkWell 水波纹正常 + return Material( + color: Colors.transparent, + child: controller.isDebugMode.value + ? _buildSubscribeList(context) + : _kr_buildRegionList(context), + ); + }); } - /// 构建国家/地区分组列表(废弃不使用) + /// 构建国家/地区分组列表 Widget _kr_buildRegionList(BuildContext context) { return Obx(() { return _kr_buildListContainer( @@ -185,10 +173,8 @@ class HINodeListView extends GetView { return InkWell( onTap: () async { try { - final success = - await controller.homeController.kr_performNodeSwitch('${country.country}-auto'); - print('node 点击 ${country.country} 节点数量${country.outboundList.length} 节点详情 ${country.outboundList}'); + await controller.homeController.kr_performNodeSwitch('auto'); if (success) { controller.homeController.kr_currentListStatus.value = KRHomeViewsListStatus.kr_none; @@ -216,10 +202,7 @@ class HINodeListView extends GetView { padding: EdgeInsets.symmetric(vertical: 8.w), // 2. 使用 children 属性,并一次性构建所有列表项 children: [ - if (controller.kr_getFilteredNodeList().isEmpty) - _buildEmptyListPlaceholder(context, - controller.isDebugMode.value ? '调试模式:无节点数据' : AppTranslations.kr_home.noNodes) - else ...[ + ...[ InkWell( // 🔧 修复:改为 async,等待节点切换完成后再关闭列表 onTap: () async { @@ -327,7 +310,7 @@ class HINodeListView extends GetView { ), ), ), - ...controller.kr_getFilteredNodeList().map((item) { + ...controller.kr_subscribeService.allList().map((item) { return InkWell( // 🔧 修复:改为 async,等待节点切换完成后再关闭列表 onTap: () async { @@ -423,18 +406,9 @@ class HINodeListView extends GetView { // 极简逻辑:只根据节点类型决定显示方式 print('🔄 _kr_buildNodeListItem节点: item.tag=${item.tag}, country=${item.country}'); - // 如果节点本身是auto组节点,显示组内最快节点的速度 - if (item.tag.endsWith('-auto')) { - print('🎯 检测到auto组节点,获取最快节点信息'); - final autoNodeInfo = controller.homeController.kr_getCountryAutoSelectedNode(item.country); - print('📊 _kr_buildNodeListItem获取auto最快节点: $autoNodeInfo'); - displayDelay = autoNodeInfo?['delay'] ?? 0; - print('✅ _kr_buildNodeListItem使用auto最快节点延迟: $displayDelay'); - } else { - // 普通节点,直接显示节点自身的速度 - displayDelay = item.urlTestDelay.value; - print('🔄 _kr_buildNodeListItem使用普通节点延迟: $displayDelay'); - } + // 普通节点,直接显示节点自身的速度 + displayDelay = item.urlTestDelay.value; + print('🔄 _kr_buildNodeListItem使用普通节点延迟: $displayDelay'); // 2. 声明文本和颜色变量 String delayText; diff --git a/lib/app/modules/hi_node_list/views/hi_page_node_view.dart b/lib/app/modules/hi_node_list/views/hi_page_node_view.dart index de5540e..e06b0c3 100644 --- a/lib/app/modules/hi_node_list/views/hi_page_node_view.dart +++ b/lib/app/modules/hi_node_list/views/hi_page_node_view.dart @@ -159,8 +159,7 @@ class HINodePageView extends GetView { padding: EdgeInsets.only(left: 60.w, right: 60.w, bottom: 90.w), // 为HIHelpEntrance预留空间 // 2. 在 Padding 内部使用 EasyRefresh child: EasyRefresh( - // 3. 绑定 onRefresh 回调 - onRefresh: controller.kr_subscribeService.kr_refreshAll, + onRefresh: controller.kr_handleRefresh, // 4. 自定义 Header 样式 header: ClassicHeader( dragText: '下拉刷新', @@ -228,4 +227,4 @@ class HINodePageView extends GetView { ), ); } -} \ No newline at end of file +} diff --git a/lib/app/services/kr_subscribe_service.dart b/lib/app/services/kr_subscribe_service.dart index 75d77c3..8b79f63 100755 --- a/lib/app/services/kr_subscribe_service.dart +++ b/lib/app/services/kr_subscribe_service.dart @@ -309,7 +309,6 @@ class KRSubscribeService { // 保存配置 KRSingBoxImp.instance.kr_saveOutbounds(listModel.configJsonList); - KRSingBoxImp.instance.kr_saveAllOutbounds(listModel.countryOutboundList); // 更新试用和订阅状态 _kr_updateSubscribeStatus(); @@ -662,7 +661,6 @@ class KRSubscribeService { // 保存配置 KRSingBoxImp.instance.kr_saveOutbounds(listModel.configJsonList); - KRSingBoxImp.instance.kr_saveAllOutbounds(listModel.countryOutboundList); // 更新试用和订阅状态 _kr_updateSubscribeStatus(); @@ -725,7 +723,6 @@ class KRSubscribeService { // 保存配置 KRSingBoxImp.instance.kr_saveOutbounds([]); - KRSingBoxImp.instance.kr_saveAllOutbounds([]); } 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 b0ff3e3..6c07d54 100755 --- a/lib/app/services/singbox_imp/kr_sing_box_imp.dart +++ b/lib/app/services/singbox_imp/kr_sing_box_imp.dart @@ -12,15 +12,12 @@ import 'package:kaer_with_panels/singbox/service/singbox_service.dart'; import 'package:kaer_with_panels/singbox/service/singbox_service_provider.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as p; -import 'package:device_info_plus/device_info_plus.dart'; import '../../../core/model/directories.dart'; import '../../../singbox/model/singbox_config_option.dart'; import '../../../singbox/model/singbox_outbound.dart'; import '../../../singbox/model/singbox_stats.dart'; import '../../../singbox/model/singbox_status.dart'; -import '../../model/business/kr_group_outbound_list.dart'; -import '../../model/business/kr_outbound_item.dart'; import '../../utils/kr_country_util.dart'; import '../../utils/kr_log_util.dart'; import '../../utils/kr_secure_storage.dart'; @@ -68,7 +65,6 @@ class KRSingBoxImp { Map kr_configOption = {}; List> kr_outbounds = []; - List Kr_allOutbounds = []; /// 首次启动 RxBool kr_isFristStart = false.obs; @@ -103,6 +99,9 @@ class KRSingBoxImp { /// 活动的出站分组 RxList kr_activeGroups = [].obs; + // 🔒 Windows DNS 优化:标志位,记录 DNS 是否已备份 + bool _dnsBackedUp = false; + /// 所有的出站分组 RxList kr_allGroups = [].obs; @@ -115,62 +114,6 @@ class KRSingBoxImp { /// 初始化进行中共享 Future(single-flight 防并发) Future? _kr_initFuture; - /// 是否为Android模拟器 - bool _kr_isAndroidEmulator = false; - - /// 检测并缓存模拟器标识 - /// - /// - 仅在 Android 上进行模拟器特征识别 - /// - 通过 brand/model/product/manufacturer 的常见模拟器特征判断 - /// 返回:无(结果写入 `_kr_isAndroidEmulator`) - Future _kr_detectEmulator() async { - try { - if (!Platform.isAndroid) { - _kr_isAndroidEmulator = false; - return; - } - - final info = await DeviceInfoPlugin().androidInfo; - final brand = (info.brand ?? '').toLowerCase(); - final model = (info.model ?? '').toLowerCase(); - final product = (info.product ?? '').toLowerCase(); - final manufacturer = (info.manufacturer ?? '').toLowerCase(); - final device = (info.device ?? '').toLowerCase(); - - bool containsAny(String s, List keys) => keys.any((k) => s.contains(k)); - - final indicators = [ - containsAny(brand, [ - 'generic', 'unknown', 'google', 'vbox', 'virtualbox', - 'bluestacks', 'nox', 'ldplayer', 'genymotion', 'mumu', 'netease', 'leidian' - ]), - containsAny(model, [ - 'emulator', 'sdk', 'sdk_gphone', 'google_sdk', 'android sdk built for x86', - 'bluestacks', 'genymotion', 'nox', 'ldplayer', 'mumu' - ]), - containsAny(product, [ - 'sdk', 'google_sdk', 'sdk_gphone', 'emulator', 'vbox', 'virtualbox' - ]), - containsAny(manufacturer, [ - 'genymotion', 'unknown', 'bluestacks', 'nox', 'ld', 'netease', 'mumu' - ]), - containsAny(device, [ - 'emulator', 'generic', 'vbox', 'virtualbox', 'sdk' - ]), - ]; - - _kr_isAndroidEmulator = indicators.any((v) => v); - KRLogUtil.kr_i( - '🔍 Android 模拟器检测: ${_kr_isAndroidEmulator ? '是模拟器' : '非模拟器'}' - ' (brand=$brand, model=$model, product=$product, manufacturer=$manufacturer, device=$device)', - tag: 'SingBox', - ); - } catch (e) { - _kr_isAndroidEmulator = false; - KRLogUtil.kr_w('⚠️ 模拟器检测失败,按真机处理: $e', tag: 'SingBox'); - } - } - /// 当前混合代理端口是否就绪 bool get kr_isProxyReady => kr_status.value is SingboxStarted; @@ -226,9 +169,6 @@ class KRSingBoxImp { await KRCountryUtil.kr_init(); KRLogUtil.kr_i('国家工具初始化完成'); - // 设备环境检测(Android模拟器) - await _kr_detectEmulator(); - final oOption = SingboxConfigOption.fromJson(_getConfigOption()); KRLogUtil.kr_i('配置选项初始化完成'); @@ -617,11 +557,9 @@ class KRSingBoxImp { "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": _kr_isAndroidEmulator - ? "local" - : "https://dns.google/dns-query", + "remote-dns-address": "https://dns.google/dns-query", // 使用 Google DoH,避免中转节点 DNS 死锁 "remote-dns-domain-strategy": "prefer_ipv4", - "direct-dns-address": "local", + "direct-dns-address": "local", // 使用系统 DNS,确保中转服务器域名能被解析 "direct-dns-domain-strategy": "prefer_ipv4", "mixed-port": kr_port, "tproxy-port": kr_port, @@ -743,56 +681,9 @@ class KRSingBoxImp { addFromOutbound(g); } - for (final g in Kr_allOutbounds) { - for (final it in g.outboundList) { - addFromOutbound(it.config); - } - } - return set; } - /// 动态构建 DNS 配置 - /// - /// - 真机:使用远程 DoH + 本地系统 DNS,final 指向远程 - /// - 模拟器:仅使用系统 DNS,final 指向直连,避免覆盖系统 DNS - /// 返回:Sing-box `dns` 段配置 Map - Map _kr_buildDnsConfig() { - if (_kr_isAndroidEmulator) { - KRLogUtil.kr_i('🛡️ 模拟器兼容模式:强制使用系统 DNS', tag: 'SingBox'); - return { - "servers": [ - { - "tag": "dns-direct", - "address": "local", - "detour": "direct" - } - ], - "rules": [], - "final": "dns-direct", - "strategy": "prefer_ipv4" - }; - } - - return { - "servers": [ - { - "tag": "dns-remote", - "address": "https://1.1.1.1/dns-query", - "address_resolver": "dns-direct" - }, - { - "tag": "dns-direct", - "address": "local", - "detour": "direct" - } - ], - "rules": _kr_buildDnsRules(), - "final": "dns-remote", - "strategy": "prefer_ipv4" - }; - } - /// 订阅状态变化流 /// 参考 hiddify-app: 监听 libcore 发送的状态事件来自动更新 UI void _kr_subscribeToStatus() { @@ -1206,7 +1097,7 @@ class KRSingBoxImp { await Future.delayed(const Duration(milliseconds: 2000)); final savedNode = await KRSecureStorage().kr_readData(key: _keySelectedNode); - if (savedNode != null && savedNode.isNotEmpty && savedNode != 'auto' && !savedNode.endsWith('-auto')) { + if (savedNode != null && savedNode.isNotEmpty && savedNode != 'auto') { KRLogUtil.kr_i('🔄 恢复用户选择的节点: $savedNode', tag: 'SingBox'); try { @@ -1247,14 +1138,7 @@ class KRSingBoxImp { // print("错误堆栈: $stack"); // } // } - void kr_saveAllOutbounds(List outbounds) { - Kr_allOutbounds = outbounds; - KRLogUtil.kr_i('📊 保存国家分组: ${Kr_allOutbounds.length}', tag: 'SingBox'); - for (int i = 0; i < Kr_allOutbounds.length; i++) { - final c = Kr_allOutbounds[i]; - KRLogUtil.kr_i(' group[$i] country="${c.country}" outbounds=${c.outboundList.length}', tag: 'SingBox'); - } - } + /// 保存配置文件 void kr_saveOutbounds(List> outbounds) async { KRLogUtil.kr_i('💾 开始保存配置文件...', tag: 'SingBox'); @@ -1303,7 +1187,23 @@ class KRSingBoxImp { "level": "debug", "timestamp": true }, - "dns": _kr_buildDnsConfig(), + "dns": { + "servers": [ + { + "tag": "dns-remote", + "address": "https://1.1.1.1/dns-query", + "address_resolver": "dns-direct" + }, + { + "tag": "dns-direct", + "address": "local", + "detour": "direct" + } + ], + "rules": _kr_buildDnsRules(), // ✅ 使用动态构建的 DNS 规则 + "final": "dns-remote", + "strategy": "prefer_ipv4" + }, "inbounds": [ { "type": "tun", @@ -1506,55 +1406,21 @@ class KRSingBoxImp { // 🔧 强制重新生成配置文件(确保最新的路由规则生效) if (kr_outbounds.isNotEmpty) { KRLogUtil.kr_i('🔄 启动前强制重新生成配置文件...', tag: 'SingBox'); - final selectedNode = await KRSecureStorage().kr_readData(key: _keySelectedNode); - KRLogUtil.kr_i('📊 国家分组数量: ${Kr_allOutbounds.length}', tag: 'SingBox'); - for (int i = 0; i < Kr_allOutbounds.length; i++) { - final g = Kr_allOutbounds[i]; - KRLogUtil.kr_i('Kr_allOutbounds[$i] country="${g.country}" outbounds=${g.outboundList.length}', tag: 'SingBox'); - } - List> toSave = []; - if (selectedNode != null && selectedNode.endsWith('-auto')) { - KRLogUtil.kr_i('🤖 检测到自动国家节点: $selectedNode', tag: 'SingBox'); - String selectedCountry = selectedNode.replaceAll(RegExp(r'-auto$'), ''); - final selectedCountryLower = selectedCountry.toLowerCase(); - final matchedGroup = Kr_allOutbounds.firstWhere( - (g) => g.country.toLowerCase() == selectedCountryLower, - orElse: () => KRCountryOutboundList(country: '', outboundList: []), - ); - if (matchedGroup.country.isNotEmpty) { - KRLogUtil.kr_i('🎯 命中分组: ${matchedGroup.country}, 节点数: ${matchedGroup.outboundList.length}', tag: 'SingBox'); - toSave = matchedGroup.outboundList.map((it) => it.config).toList(); - } else { - for (final g in Kr_allOutbounds) { - for (final it in g.outboundList) { - final tagLower = it.tag.toLowerCase(); - if (tagLower == selectedNode.toLowerCase() || tagLower.contains(selectedCountryLower)) { - toSave.add(it.config); - } - } - } - } - KRLogUtil.kr_i('✅ 自动国家过滤: ${toSave.length}', tag: 'SingBox'); - } else { - for (final g in Kr_allOutbounds) { - for (final it in g.outboundList) { - toSave.add(it.config); - } - } - } - kr_saveOutbounds(toSave); + kr_saveOutbounds(kr_outbounds); + // 等待配置文件写入完成 await Future.delayed(const Duration(milliseconds: 100)); } KRLogUtil.kr_i('📁 配置文件路径: $_cutPath', tag: 'SingBox'); KRLogUtil.kr_i('📝 配置名称: $kr_configName', tag: 'SingBox'); - // 🔑 Windows 平台:在启动前备份 DNS 设置 - if (Platform.isWindows) { - KRLogUtil.kr_i('🪟 Windows 平台,备份 DNS 设置...', tag: 'SingBox'); + // 🔑 Windows 平台:仅在首次启动时备份 DNS 设置(优化:避免重复备份) + if (Platform.isWindows && !_dnsBackedUp) { + KRLogUtil.kr_i('🪟 Windows 平台,首次启动备份 DNS 设置...', tag: 'SingBox'); try { final backupSuccess = await KRWindowsDnsUtil.instance.kr_backupDnsSettings(); if (backupSuccess) { + _dnsBackedUp = true; // 标记已备份 KRLogUtil.kr_i('✅ Windows DNS 备份成功', tag: 'SingBox'); } else { KRLogUtil.kr_w('⚠️ Windows DNS 备份失败,将在停止时使用兜底恢复', tag: 'SingBox'); @@ -1562,6 +1428,8 @@ class KRSingBoxImp { } catch (e) { KRLogUtil.kr_w('⚠️ Windows DNS 备份异常: $e,将在停止时使用兜底恢复', tag: 'SingBox'); } + } else if (Platform.isWindows && _dnsBackedUp) { + KRLogUtil.kr_i('⏭️ Windows 平台,DNS 已备份,跳过重复备份(节点切换优化)', tag: 'SingBox'); } // 🔑 先尝试停止旧实例,避免 command.sock 冲突 @@ -1731,12 +1599,42 @@ class KRSingBoxImp { // 继续执行清理操作 } - // 🔑 Windows 平台:停止后立即恢复 DNS 设置 - if (Platform.isWindows) { - KRLogUtil.kr_i('🪟 Windows 平台,开始恢复 DNS 设置...', tag: 'SingBox'); + // 🔑 Windows 平台:仅在有备份时恢复 DNS 设置 + if (Platform.isWindows && _dnsBackedUp) { + KRLogUtil.kr_i('🪟 Windows 平台,等待 sing-box 完全停止...', tag: 'SingBox'); - // 等待 sing-box 完全停止 - await Future.delayed(const Duration(milliseconds: 1000)); + // 🔧 P3优化: 监听状态而非固定延迟,确保 sing-box 真正停止后再恢复 DNS + try { + // 如果当前已经是停止状态,直接继续 + if (kr_status.value is SingboxStopped) { + KRLogUtil.kr_i('✅ sing-box 已经是停止状态,立即恢复 DNS', tag: 'SingBox'); + } else { + // 等待状态变为停止,最多等待3秒 + final completer = Completer(); + late final Worker worker; + + worker = ever(kr_status, (status) { + if (status is SingboxStopped) { + if (!completer.isCompleted) { + completer.complete(); + worker.dispose(); + } + } + }); + + await completer.future.timeout( + const Duration(seconds: 3), + onTimeout: () { + KRLogUtil.kr_w('⏱️ 等待停止状态超时,继续执行 DNS 恢复', tag: 'SingBox'); + worker.dispose(); + }, + ); + + KRLogUtil.kr_i('✅ sing-box 已完全停止,开始恢复 DNS...', tag: 'SingBox'); + } + } catch (e) { + KRLogUtil.kr_w('⚠️ 状态监听异常: $e,继续执行 DNS 恢复', tag: 'SingBox'); + } try { // 尝试恢复 DNS @@ -1749,7 +1647,14 @@ class KRSingBoxImp { } catch (e) { KRLogUtil.kr_e('❌ Windows DNS 恢复异常: $e', tag: 'SingBox'); // 异常时也会在工具类内部执行兜底恢复 + } finally { + // 🔧 P0修复2: 使用 finally 确保标志位始终被重置,避免下次启动跳过备份 + _dnsBackedUp = false; + KRLogUtil.kr_i('🔄 重置 DNS 备份标志位', tag: 'SingBox'); } + } else if (Platform.isWindows && !_dnsBackedUp) { + KRLogUtil.kr_i('⏭️ Windows 平台,DNS 未备份,跳过恢复', tag: 'SingBox'); + await Future.delayed(const Duration(milliseconds: 500)); } else { // 非 Windows 平台正常等待 await Future.delayed(const Duration(milliseconds: 500)); @@ -1777,12 +1682,16 @@ class KRSingBoxImp { KRLogUtil.kr_e('错误堆栈: $stackTrace'); // 🔑 即使出错,也要尝试恢复 Windows DNS - if (Platform.isWindows) { + if (Platform.isWindows && _dnsBackedUp) { KRLogUtil.kr_w('⚠️ 停止异常,强制执行 DNS 恢复', tag: 'SingBox'); try { await KRWindowsDnsUtil.instance.kr_restoreDnsSettings(); } catch (dnsError) { KRLogUtil.kr_e('❌ 强制 DNS 恢复失败: $dnsError', tag: 'SingBox'); + } finally { + // 🔧 P0修复2: 异常路径也要重置标志位 + _dnsBackedUp = false; + KRLogUtil.kr_i('🔄 异常后重置 DNS 备份标志位', tag: 'SingBox'); } } @@ -1989,7 +1898,7 @@ class KRSingBoxImp { // 🔄 如果用户选择了具体节点(不是 auto),启动定期检查和重新选择 // 这是为了防止 urltest 自动覆盖用户的手动选择 _nodeSelectionTimer?.cancel(); - if (tag != 'auto' && !tag.endsWith('-auto')) { + if (tag != 'auto') { KRLogUtil.kr_i('🔁 启动节点选择监控,防止被 auto 覆盖', tag: 'SingBox'); _nodeSelectionTimer = Timer.periodic(const Duration(seconds: 20), (timer) { // 每 20 秒重新选择一次,确保用户选择不被覆盖