feat: 保存节点调试进度
This commit is contained in:
parent
46295f4543
commit
670eb7ebc9
@ -28,11 +28,12 @@ class KROutboundItem {
|
|||||||
|
|
||||||
/// URL
|
/// URL
|
||||||
String url = "";
|
String url = "";
|
||||||
|
// ✅ 1. 将传入的 nodeListItem 保存为类的 final 成员变量
|
||||||
|
final KrNodeListItem nodeListItem;
|
||||||
/// 服务器类型
|
/// 服务器类型
|
||||||
|
|
||||||
/// 构造函数,接受 KrItem 对象并初始化 KROutboundItem
|
/// 构造函数,接受 KrItem 对象并初始化 KROutboundItem
|
||||||
KROutboundItem(KrNodeListItem nodeListItem) {
|
KROutboundItem(this.nodeListItem) {
|
||||||
id = nodeListItem.id.toString();
|
id = nodeListItem.id.toString();
|
||||||
protocol = nodeListItem.protocol;
|
protocol = nodeListItem.protocol;
|
||||||
latitude = nodeListItem.latitude;
|
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<String, dynamic> _buildTransport(Map<String, dynamic> json) {
|
Map<String, dynamic> _buildTransport(Map<String, dynamic> json) {
|
||||||
final transportType = json["transport"] as String?;
|
final transportType = json["transport"] as String?;
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
// hi_node_list_view.dart
|
// hi_node_list_view.dart
|
||||||
|
|
||||||
import 'dart:math';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
@ -80,8 +79,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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,7 +108,7 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
onTap: () async {
|
onTap: () async {
|
||||||
try {
|
try {
|
||||||
final success =
|
final success =
|
||||||
await controller.homeController.kr_performCountrySwitch('auto');
|
await controller.homeController.kr_performNodeSwitch('auto');
|
||||||
if (success) {
|
if (success) {
|
||||||
controller.homeController.kr_currentListStatus.value =
|
controller.homeController.kr_currentListStatus.value =
|
||||||
KRHomeViewsListStatus.kr_none;
|
KRHomeViewsListStatus.kr_none;
|
||||||
@ -193,8 +192,10 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
final success =
|
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) {
|
if (success) {
|
||||||
controller.homeController.kr_currentListStatus.value =
|
controller.homeController.kr_currentListStatus.value =
|
||||||
KRHomeViewsListStatus.kr_none;
|
KRHomeViewsListStatus.kr_none;
|
||||||
@ -311,7 +312,23 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
),
|
),
|
||||||
...controller.kr_subscribeService.allList.map((item) {
|
...controller.kr_subscribeService.allList.map((item) {
|
||||||
return InkWell(
|
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),
|
child: _kr_buildNodeListItem(context, item: item),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
@ -524,7 +541,7 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
SizedBox(width: 12.w),
|
SizedBox(width: 12.w),
|
||||||
Obx(() => controller.homeController.kr_coutryText.value == country.country
|
Obx(() => controller.homeController.kr_cutTag.value == '${country.country}-auto'
|
||||||
? KrLocalImage(
|
? KrLocalImage(
|
||||||
imageName: 'radio-active-icon',
|
imageName: 'radio-active-icon',
|
||||||
imageType: ImageType.svg,
|
imageType: ImageType.svg,
|
||||||
|
|||||||
@ -1276,107 +1276,6 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 🌍 执行国家切换(包含UI同步与VPN热重载)
|
|
||||||
/// 返回 true 表示切换成功,false 表示失败
|
|
||||||
Future<bool> 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_selectNode 方法
|
||||||
/// 现在只是委托给新的 kr_performNodeSwitch 方法
|
/// 现在只是委托给新的 kr_performNodeSwitch 方法
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import 'package:path/path.dart' as p;
|
|||||||
import '../../../core/model/directories.dart';
|
import '../../../core/model/directories.dart';
|
||||||
import '../../../singbox/model/singbox_config_option.dart';
|
import '../../../singbox/model/singbox_config_option.dart';
|
||||||
import '../../../singbox/model/singbox_outbound.dart';
|
import '../../../singbox/model/singbox_outbound.dart';
|
||||||
|
import '../../../singbox/model/singbox_proxy_type.dart';
|
||||||
import '../../../singbox/model/singbox_stats.dart';
|
import '../../../singbox/model/singbox_stats.dart';
|
||||||
import '../../../singbox/model/singbox_status.dart';
|
import '../../../singbox/model/singbox_status.dart';
|
||||||
import '../../utils/kr_country_util.dart';
|
import '../../utils/kr_country_util.dart';
|
||||||
@ -23,6 +24,8 @@ import '../../utils/kr_log_util.dart';
|
|||||||
import '../../utils/kr_secure_storage.dart';
|
import '../../utils/kr_secure_storage.dart';
|
||||||
import '../../common/app_run_data.dart';
|
import '../../common/app_run_data.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
|
|
||||||
|
|
||||||
enum KRConnectionType {
|
enum KRConnectionType {
|
||||||
global,
|
global,
|
||||||
@ -1557,57 +1560,6 @@ class KRSingBoxImp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 🌍 国家级节点选择
|
|
||||||
/// 如果传入 'auto',则调用原有 kr_selectOutbound('auto')
|
|
||||||
/// 否则根据国家名筛选该国节点,自动选择延迟最低的节点
|
|
||||||
Future<void> 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(', ');
|
final activeTags = kr_activeGroups.map((g) => g.tag).join(', ');
|
||||||
KRLogUtil.kr_i('🧩 分组: all=${kr_allGroups.length}, active=${kr_activeGroups.length} [$activeTags]', tag: 'SingBoxTun');
|
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 {
|
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();
|
sock.destroy();
|
||||||
KRLogUtil.kr_i('🌐 TCP 443连接成功 (1.1.1.1:443)', tag: 'SingBoxTun');
|
KRLogUtil.kr_i('🌐 TCP 443连接成功 (1.1.1.1:443)', tag: 'SingBoxTun');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
KRLogUtil.kr_e('🌐 TCP 443连接失败: $e', tag: 'SingBoxTun');
|
KRLogUtil.kr_e('🌐 TCP 443连接失败: $e', tag: 'SingBoxTun');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) HTTPS 测试(使用 Google 204 连接性检测,更通用更稳健)
|
// 2️⃣ HTTPS 测试(Google Connectivity)
|
||||||
try {
|
try {
|
||||||
final client = HttpClient()..connectionTimeout = const Duration(seconds: 3);
|
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');
|
req.headers.add('User-Agent', 'kr-debug');
|
||||||
final resp = await req.close();
|
final resp = await req.close();
|
||||||
KRLogUtil.kr_i('🔐 HTTPS 204 状态: ${resp.statusCode}', tag: 'SingBoxTun');
|
KRLogUtil.kr_i('🔐 HTTPS 204 状态: ${resp.statusCode}', tag: 'SingBoxTun');
|
||||||
@ -1691,23 +1654,177 @@ class KRSingBoxImp {
|
|||||||
KRLogUtil.kr_e('🔐 HTTPS 204 错误: $e', tag: 'SingBoxTun');
|
KRLogUtil.kr_e('🔐 HTTPS 204 错误: $e', tag: 'SingBoxTun');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) 系统DNS解析(走系统解析器)
|
// 3️⃣ 系统DNS解析
|
||||||
try {
|
try {
|
||||||
final addrs = await InternetAddress.lookup('google.com');
|
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) {
|
} catch (e) {
|
||||||
KRLogUtil.kr_e('🧭 DNS解析错误: $e', tag: 'SingBoxTun');
|
KRLogUtil.kr_e('🧭 DNS解析错误: $e', tag: 'SingBoxTun');
|
||||||
}
|
}
|
||||||
|
// 3️⃣ 系统DNS解析
|
||||||
// 4) TCP 53检测(公共DNS TCP53可达性)
|
|
||||||
try {
|
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();
|
sock.destroy();
|
||||||
KRLogUtil.kr_i('🛰️ TCP 53连接成功 (8.8.8.8:53)', tag: 'SingBoxTun');
|
KRLogUtil.kr_i('🛰️ TCP 53连接成功 (8.8.8.8:53)', tag: 'SingBoxTun');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
KRLogUtil.kr_w('🛰️ TCP 53连接失败: $e', tag: 'SingBoxTun');
|
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<int>(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');
|
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<void> kr_probeDoH(List<String> 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user