feat: 保存节点调试进度
Build Windows / 编译 libcore (Windows) (20.15.1) (push) Successful in 20m7s
Build Windows / build (push) Has been cancelled

This commit is contained in:
2025-11-13 22:43:27 -08:00
parent 46295f4543
commit 670eb7ebc9
4 changed files with 209 additions and 170 deletions
+177 -60
View File
@@ -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<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(', ');
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<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');
}
// 根据所选国家解析当前实际连接节点(适配 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');
}
}
}
}