feat: 节点测速调整

This commit is contained in:
2025-11-25 23:06:20 -08:00
parent 1809c11473
commit 909020654f
6 changed files with 112 additions and 263 deletions
+7 -44
View File
@@ -8,7 +8,7 @@ import 'package:flutter/foundation.dart';
/// 表示出站项列表的模型类
class KrOutboundsList {
/// 服务器分组
final List<KRGroupOutboundList> groupOutboundList = []; // 存储服务器分组的列表
@@ -18,20 +18,20 @@ class KrOutboundsList {
/// 全部列表
final List<KROutboundItem> allList = []; // 存储国家分组的列表
// 配置json
final List<Map<String,dynamic>> configJsonList = [];
final List<Map<String,dynamic>> configJsonList = [];
/// 标签列表
final Map<String,KROutboundItem> keyList = {}; // 存储国家分组的列表
/// 处理出站项并将其分组
/// [list] 是要处理的出站项列表
void processOutboundItems(List<KrNodeListItem> list,List<KRNodeGroupListItem> groupList) {
final Map<String, List<KROutboundItem>> tagGroups = {};
final Map<String, List<KROutboundItem>> countryGroups = {};
// 用于追踪已使用的标签
final Map<String, int> 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<String, List<KROutboundItem>> 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()}');
}
}
}
}
@@ -95,6 +95,13 @@ class HINodeListController extends GetxController with WidgetsBindingObserver {
KRLogUtil.kr_i('调试模式已重置', tag: 'HINodeListController');
}
Future<void> kr_handleRefresh() async {
await kr_subscribeService.kr_refreshAll();
if (!homeController.kr_isLatency.value) {
homeController.kr_urlTest();
}
}
@override
void onInit() {
super.onInit();
@@ -23,22 +23,6 @@ class HINodeListView extends GetView<HINodeListController> {
static const Color krModernGreen = Color(0xFF4CAF50);
static const Color krModernGreenLight = Color(0xFF81C784);
// 存储随机延迟值,用于UI展示
static final Map<String, int> _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<HINodeListController> {
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<HINodeListController> {
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<HINodeListController> {
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<HINodeListController> {
),
),
),
...controller.kr_getFilteredNodeList().map((item) {
...controller.kr_subscribeService.allList().map((item) {
return InkWell(
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
onTap: () async {
@@ -423,18 +406,9 @@ class HINodeListView extends GetView<HINodeListController> {
// 极简逻辑:只根据节点类型决定显示方式
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;
@@ -159,8 +159,7 @@ class HINodePageView extends GetView<HINodeListController> {
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<HINodeListController> {
),
);
}
}
}
@@ -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([]);
}
+80 -171
View File
@@ -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<String, dynamic> kr_configOption = {};
List<Map<String, dynamic>> kr_outbounds = [];
List<KRCountryOutboundList> Kr_allOutbounds = [];
/// 首次启动
RxBool kr_isFristStart = false.obs;
@@ -103,6 +99,9 @@ class KRSingBoxImp {
/// 活动的出站分组
RxList<SingboxOutboundGroup> kr_activeGroups = <SingboxOutboundGroup>[].obs;
// 🔒 Windows DNS 优化:标志位,记录 DNS 是否已备份
bool _dnsBackedUp = false;
/// 所有的出站分组
RxList<SingboxOutboundGroup> kr_allGroups = <SingboxOutboundGroup>[].obs;
@@ -115,62 +114,6 @@ class KRSingBoxImp {
/// 初始化进行中共享 Futuresingle-flight 防并发)
Future<void>? _kr_initFuture;
/// 是否为Android模拟器
bool _kr_isAndroidEmulator = false;
/// 检测并缓存模拟器标识
///
/// - 仅在 Android 上进行模拟器特征识别
/// - 通过 brand/model/product/manufacturer 的常见模拟器特征判断
/// 返回:无(结果写入 `_kr_isAndroidEmulator`
Future<void> _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<String> keys) => keys.any((k) => s.contains(k));
final indicators = <bool>[
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<String, dynamic> _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<KRCountryOutboundList> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<void>();
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 秒重新选择一次,确保用户选择不被覆盖