feat: bugfix

This commit is contained in:
2025-12-01 06:54:18 -08:00
parent 1b1b38aba5
commit 86687cac58
22 changed files with 827 additions and 493 deletions
+258 -161
View File
@@ -185,14 +185,14 @@ class KRSingBoxImp {
KRLogUtil.kr_i('iOS 路径获取完成: $paths');
kr_configDics = (
baseDir: Directory(paths?["base"]! as String),
workingDir: Directory(paths?["working"]! as String),
tempDir: Directory(paths?["temp"]! as String),
baseDir: Directory(paths?["base"]! as String),
workingDir: Directory(paths?["working"]! as String),
tempDir: Directory(paths?["temp"]! as String),
);
} else {
final baseDir = await getApplicationSupportDirectory();
final workingDir =
Platform.isAndroid ? await getExternalStorageDirectory() : baseDir;
Platform.isAndroid ? await getExternalStorageDirectory() : baseDir;
final tempDir = await getTemporaryDirectory();
// Windows 路径规范化:确保使用正确的路径分隔符
@@ -200,7 +200,8 @@ class KRSingBoxImp {
if (Platform.isWindows) {
final normalized = dir.path.replaceAll('/', '\\');
if (normalized != dir.path) {
KRLogUtil.kr_i('路径规范化: ${dir.path} -> $normalized', tag: 'SingBox');
KRLogUtil.kr_i('路径规范化: ${dir.path} -> $normalized',
tag: 'SingBox');
return Directory(normalized);
}
}
@@ -208,16 +209,17 @@ class KRSingBoxImp {
}
kr_configDics = (
baseDir: normalizePath(baseDir),
workingDir: normalizePath(workingDir!),
tempDir: normalizePath(tempDir),
baseDir: normalizePath(baseDir),
workingDir: normalizePath(workingDir!),
tempDir: normalizePath(tempDir),
);
KRLogUtil.kr_i('其他平台路径初始化完成');
}
KRLogUtil.kr_i('开始创建目录');
KRLogUtil.kr_i('baseDir: ${kr_configDics.baseDir.path}', tag: 'SingBox');
KRLogUtil.kr_i('workingDir: ${kr_configDics.workingDir.path}', tag: 'SingBox');
KRLogUtil.kr_i('workingDir: ${kr_configDics.workingDir.path}',
tag: 'SingBox');
KRLogUtil.kr_i('tempDir: ${kr_configDics.tempDir.path}', tag: 'SingBox');
// 确保所有目录都存在
@@ -253,12 +255,14 @@ class KRSingBoxImp {
break;
} else {
retryCount++;
KRLogUtil.kr_i('⚠️ data 目录创建后验证失败,重试 $retryCount/$maxRetries', tag: 'SingBox');
KRLogUtil.kr_i('⚠️ data 目录创建后验证失败,重试 $retryCount/$maxRetries',
tag: 'SingBox');
await Future.delayed(const Duration(milliseconds: 200));
}
} catch (e) {
retryCount++;
KRLogUtil.kr_e('❌ 创建 data 目录失败 (尝试 $retryCount/$maxRetries): $e', tag: 'SingBox');
KRLogUtil.kr_e('❌ 创建 data 目录失败 (尝试 $retryCount/$maxRetries): $e',
tag: 'SingBox');
if (retryCount >= maxRetries) {
throw Exception('无法创建 libcore 数据库目录: ${dataDir.path},错误: $e');
}
@@ -294,32 +298,40 @@ class KRSingBoxImp {
// libcore 的 Setup() 会调用 os.Chdir(workingPath),然后使用相对路径 "./data"
// 如果 os.Chdir() 失败(路径不存在或权限问题),后续的相对路径访问会失败
if (!kr_configDics.workingDir.existsSync()) {
final error = '❌ workingDir 不存在,无法调用 setup(): ${kr_configDics.workingDir.path}';
final error =
'❌ workingDir 不存在,无法调用 setup(): ${kr_configDics.workingDir.path}';
KRLogUtil.kr_e(error, tag: 'SingBox');
throw Exception(error);
}
// 验证 workingDir 可读可写
try {
final testWorkingFile = File(p.join(kr_configDics.workingDir.path, '.test_working_dir'));
final testWorkingFile =
File(p.join(kr_configDics.workingDir.path, '.test_working_dir'));
await testWorkingFile.writeAsString('test');
await testWorkingFile.delete();
KRLogUtil.kr_i('✅ workingDir 写入权限验证通过', tag: 'SingBox');
} catch (e) {
final error = '❌ workingDir 无写入权限: ${kr_configDics.workingDir.path}, 错误: $e';
final error =
'❌ workingDir 无写入权限: ${kr_configDics.workingDir.path}, 错误: $e';
KRLogUtil.kr_e(error, tag: 'SingBox');
throw Exception(error);
}
final finalDataDir = Directory(p.join(kr_configDics.workingDir.path, 'data'));
final finalDataDir =
Directory(p.join(kr_configDics.workingDir.path, 'data'));
if (!finalDataDir.existsSync()) {
KRLogUtil.kr_e('❌ 最终验证失败:data 目录不存在', tag: 'SingBox');
KRLogUtil.kr_e('路径: ${finalDataDir.path}', tag: 'SingBox');
KRLogUtil.kr_e('workingDir 是否存在: ${kr_configDics.workingDir.existsSync()}', tag: 'SingBox');
KRLogUtil.kr_e(
'workingDir 是否存在: ${kr_configDics.workingDir.existsSync()}',
tag: 'SingBox');
if (kr_configDics.workingDir.existsSync()) {
try {
final workingDirContents = kr_configDics.workingDir.listSync();
KRLogUtil.kr_e('workingDir 内容: ${workingDirContents.map((e) => e.path.split(Platform.pathSeparator).last).join(", ")}', tag: 'SingBox');
KRLogUtil.kr_e(
'workingDir 内容: ${workingDirContents.map((e) => e.path.split(Platform.pathSeparator).last).join(", ")}',
tag: 'SingBox');
} catch (e) {
KRLogUtil.kr_e('无法列出 workingDir 内容: $e', tag: 'SingBox');
}
@@ -338,11 +350,13 @@ class KRSingBoxImp {
// 不抛出异常,让 setup() 自己处理
}
final configsDir = Directory(p.join(kr_configDics.workingDir.path, "configs"));
final configsDir =
Directory(p.join(kr_configDics.workingDir.path, "configs"));
if (!configsDir.existsSync()) {
try {
await configsDir.create(recursive: true);
KRLogUtil.kr_i('✅ 已创建 configs 目录: ${configsDir.path}', tag: 'SingBox');
KRLogUtil.kr_i('✅ 已创建 configs 目录: ${configsDir.path}',
tag: 'SingBox');
} catch (e) {
KRLogUtil.kr_e('⚠️ configs 目录创建失败: $e', tag: 'SingBox');
// 不抛出异常,继续初始化
@@ -354,8 +368,10 @@ class KRSingBoxImp {
// 特别处理 extensionData.db 文件 (Windows特定)
if (Platform.isWindows) {
try {
final extensionDataDbPath = p.join(finalDataDir.path, 'extensionData.db');
KRLogUtil.kr_i('👉 准备处理 extensionData.db 路径: $extensionDataDbPath', tag: 'SingBox');
final extensionDataDbPath =
p.join(finalDataDir.path, 'extensionData.db');
KRLogUtil.kr_i('👉 准备处理 extensionData.db 路径: $extensionDataDbPath',
tag: 'SingBox');
// 确保 extensionData.db 的父目录存在
final extensionDataParent = Directory(p.dirname(extensionDataDbPath));
@@ -365,7 +381,8 @@ class KRSingBoxImp {
}
// 测试文件创建权限
final testFile = File(p.join(extensionDataParent.path, '.test_extension'));
final testFile =
File(p.join(extensionDataParent.path, '.test_extension'));
await testFile.writeAsString('test');
await testFile.delete();
KRLogUtil.kr_i('✅ extensionData 目录权限验证通过', tag: 'SingBox');
@@ -377,18 +394,26 @@ class KRSingBoxImp {
KRLogUtil.kr_i('✅ 目录创建完成', tag: 'SingBox');
KRLogUtil.kr_i('开始设置 SingBox', tag: 'SingBox');
KRLogUtil.kr_i(' - baseDir: ${kr_configDics.baseDir.path}', tag: 'SingBox');
KRLogUtil.kr_i(' - workingDir: ${kr_configDics.workingDir.path}', tag: 'SingBox');
KRLogUtil.kr_i(' - tempDir: ${kr_configDics.tempDir.path}', tag: 'SingBox');
KRLogUtil.kr_i(' - data 目录: ${p.join(kr_configDics.workingDir.path, "data")}', tag: 'SingBox');
KRLogUtil.kr_i(' - data 目录存在: ${finalDataDir.existsSync()}', tag: 'SingBox');
KRLogUtil.kr_i(' - baseDir: ${kr_configDics.baseDir.path}',
tag: 'SingBox');
KRLogUtil.kr_i(' - workingDir: ${kr_configDics.workingDir.path}',
tag: 'SingBox');
KRLogUtil.kr_i(' - tempDir: ${kr_configDics.tempDir.path}',
tag: 'SingBox');
KRLogUtil.kr_i(
' - data 目录: ${p.join(kr_configDics.workingDir.path, "data")}',
tag: 'SingBox');
KRLogUtil.kr_i(' - data 目录存在: ${finalDataDir.existsSync()}',
tag: 'SingBox');
// 在 Windows 上,列出 data 目录内容(如果有文件)
if (Platform.isWindows && finalDataDir.existsSync()) {
try {
final dataContents = finalDataDir.listSync();
if (dataContents.isNotEmpty) {
KRLogUtil.kr_i(' - data 目录现有文件: ${dataContents.map((e) => e.path.split(Platform.pathSeparator).last).join(", ")}', tag: 'SingBox');
KRLogUtil.kr_i(
' - data 目录现有文件: ${dataContents.map((e) => e.path.split(Platform.pathSeparator).last).join(", ")}',
tag: 'SingBox');
} else {
KRLogUtil.kr_i(' - data 目录为空', tag: 'SingBox');
}
@@ -397,19 +422,23 @@ class KRSingBoxImp {
}
}
KRLogUtil.kr_i(' - libcore 将通过 os.Chdir() 切换到: ${kr_configDics.workingDir.path}', tag: 'SingBox');
KRLogUtil.kr_i(
' - libcore 将通过 os.Chdir() 切换到: ${kr_configDics.workingDir.path}',
tag: 'SingBox');
KRLogUtil.kr_i(' - 然后使用相对路径 "./data" 访问数据库', tag: 'SingBox');
// Windows 特定:验证路径格式是否正确
if (Platform.isWindows) {
final workingPath = kr_configDics.workingDir.path;
if (workingPath.contains('/')) {
KRLogUtil.kr_e('⚠️ 警告:Windows 路径包含正斜杠,可能导致问题: $workingPath', tag: 'SingBox');
KRLogUtil.kr_e('⚠️ 警告:Windows 路径包含正斜杠,可能导致问题: $workingPath',
tag: 'SingBox');
}
// 确保路径使用反斜杠(Windows 标准)
final normalizedPath = workingPath.replaceAll('/', '\\');
if (normalizedPath != workingPath) {
KRLogUtil.kr_e('⚠️ 路径格式可能需要规范化: $workingPath -> $normalizedPath', tag: 'SingBox');
KRLogUtil.kr_e('⚠️ 路径格式可能需要规范化: $workingPath -> $normalizedPath',
tag: 'SingBox');
}
}
@@ -418,11 +447,11 @@ class KRSingBoxImp {
KRLogUtil.kr_i('📡 开始调用 setup() 注册 FFI 端口', tag: 'SingBox');
final setupResult = await kr_singBox.setup(kr_configDics, false).run();
setupResult.match(
(error) {
(error) {
KRLogUtil.kr_e('❌ setup() 失败: $error', tag: 'SingBox');
throw Exception('FFI setup 失败: $error');
},
(_) {
(_) {
KRLogUtil.kr_i('✅ setup() 成功,FFI 端口已注册', tag: 'SingBox');
},
);
@@ -457,19 +486,25 @@ class KRSingBoxImp {
KRLogUtil.kr_e('🔍 Windows 路径诊断信息:', tag: 'SingBox');
KRLogUtil.kr_e(' - workingDir: ${workingDir.path}', tag: 'SingBox');
KRLogUtil.kr_e(' - workingDir 存在: ${workingDir.existsSync()}', tag: 'SingBox');
KRLogUtil.kr_e(' - workingDir 存在: ${workingDir.existsSync()}',
tag: 'SingBox');
KRLogUtil.kr_e(' - data 目录: ${dataDir.path}', tag: 'SingBox');
KRLogUtil.kr_e(' - data 目录存在: ${dataDir.existsSync()}', tag: 'SingBox');
KRLogUtil.kr_e(' - data 目录存在: ${dataDir.existsSync()}',
tag: 'SingBox');
KRLogUtil.kr_e(' - configs 目录: ${configsDir.path}', tag: 'SingBox');
KRLogUtil.kr_e(' - configs 目录存在: ${configsDir.existsSync()}', tag: 'SingBox');
KRLogUtil.kr_e(' - configs 目录存在: ${configsDir.existsSync()}',
tag: 'SingBox');
// 检查父目录内容
if (workingDir.existsSync()) {
try {
final contents = workingDir.listSync();
KRLogUtil.kr_e(' - workingDir 内容: ${contents.map((e) => "${e.path.split(Platform.pathSeparator).last}${e is Directory ? "/" : ""}").join(", ")}', tag: 'SingBox');
KRLogUtil.kr_e(
' - workingDir 内容: ${contents.map((e) => "${e.path.split(Platform.pathSeparator).last}${e is Directory ? "/" : ""}").join(", ")}',
tag: 'SingBox');
} catch (listErr) {
KRLogUtil.kr_e(' - 无法列出 workingDir 内容: $listErr', tag: 'SingBox');
KRLogUtil.kr_e(' - 无法列出 workingDir 内容: $listErr',
tag: 'SingBox');
}
}
} catch (diagErr) {
@@ -492,7 +527,8 @@ class KRSingBoxImp {
Future<void> _kr_extractGeositeFiles() async {
try {
// 创建 geosite 目录
final geositeDir = Directory(p.join(kr_configDics.workingDir.path, 'geosite'));
final geositeDir =
Directory(p.join(kr_configDics.workingDir.path, 'geosite'));
if (!geositeDir.existsSync()) {
await geositeDir.create(recursive: true);
KRLogUtil.kr_i('✅ 已创建 geosite 目录: ${geositeDir.path}', tag: 'SingBox');
@@ -509,7 +545,8 @@ class KRSingBoxImp {
// 检查文件是否已存在
if (targetFile.existsSync()) {
final fileSize = await targetFile.length();
KRLogUtil.kr_i('📄 $filename 已存在 (${fileSize} bytes), 跳过', tag: 'SingBox');
KRLogUtil.kr_i('📄 $filename 已存在 (${fileSize} bytes), 跳过',
tag: 'SingBox');
continue;
}
@@ -522,7 +559,8 @@ class KRSingBoxImp {
await targetFile.writeAsBytes(bytes);
final writtenSize = await targetFile.length();
KRLogUtil.kr_i('✅ 提取成功: $filename (${writtenSize} bytes)', tag: 'SingBox');
KRLogUtil.kr_i('✅ 提取成功: $filename (${writtenSize} bytes)',
tag: 'SingBox');
}
KRLogUtil.kr_i('🎉 所有 geosite 文件提取完成', tag: 'SingBox');
@@ -542,24 +580,27 @@ class KRSingBoxImp {
// 🔧 关键修复:全局代理模式下,region 强制设为 'other'libcore 就不会生成国家直连规则
final String effectiveRegion;
if (kr_connectionType.value == KRConnectionType.global) {
effectiveRegion = 'other'; // 全局代理:不添加任何国家规则
effectiveRegion = 'other'; // 全局代理:不添加任何国家规则
KRLogUtil.kr_i('🌐 [全局代理模式] region 设为 other,所有流量走代理', tag: 'SingBox');
} else {
effectiveRegion = KRCountryUtil.kr_getCurrentCountryCode(); // 智能代理:使用用户选择的国家
effectiveRegion =
KRCountryUtil.kr_getCurrentCountryCode(); // 智能代理:使用用户选择的国家
KRLogUtil.kr_i('✅ [智能代理模式] region 设为 $effectiveRegion', tag: 'SingBox');
}
final op = {
"region": effectiveRegion, // 🔧 修复:根据出站模式动态设置 region
"block-ads": false, // 参考 hiddify-app: 默认关闭广告拦截
"region": effectiveRegion, // 🔧 修复:根据出站模式动态设置 region
"block-ads": false, // 参考 hiddify-app: 默认关闭广告拦截
"use-xray-core-when-possible": false,
"execute-config-as-is": true, // 🔧 修复:使用我们生成的完整配置,不让 libcore 重写
"log-level": "info", // 调试阶段使用 info,生产环境改为 warn
"execute-config-as-is": true, // 🔧 修复:使用我们生成的完整配置,不让 libcore 重写
"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": "https://dns.google/dns-query", // 使用 Google DoH,避免中转节点 DNS 死锁
"ipv6-mode":
"ipv4_only", // 参考 hiddify-app: 仅使用 IPv4 (有效值: ipv4_only, prefer_ipv4, prefer_ipv6, ipv6_only)
"remote-dns-address":
"https://dns.google/dns-query", // 使用 Google DoH,避免中转节点 DNS 死锁
"remote-dns-domain-strategy": "prefer_ipv4",
"direct-dns-address": "local", // 使用系统 DNS,确保中转服务器域名能被解析
"direct-dns-address": "local", // 使用系统 DNS,确保中转服务器域名能被解析
"direct-dns-domain-strategy": "prefer_ipv4",
"mixed-port": kr_port,
"tproxy-port": kr_port,
@@ -567,14 +608,15 @@ class KRSingBoxImp {
"tun-implementation": "gvisor",
"mtu": 9000,
"strict-route": true,
"connection-test-url": "http://cp.cloudflare.com", // 参考 hiddify-app: 使用 Cloudflare 测试端点
"connection-test-url":
"http://cp.cloudflare.com", // 参考 hiddify-app: 使用 Cloudflare 测试端点
"url-test-interval": 30,
"enable-clash-api": true,
"clash-api-port": 36756,
"enable-tun": Platform.isIOS || Platform.isAndroid,
"enable-tun-service": false,
"set-system-proxy":
Platform.isWindows || Platform.isLinux || Platform.isMacOS,
Platform.isWindows || Platform.isLinux || Platform.isMacOS,
"bypass-lan": false,
"allow-connection-from-lan": false,
"enable-fake-dns": false,
@@ -628,11 +670,14 @@ class KRSingBoxImp {
// 🔧 调试日志:确认自定义规则已添加
final rules = op["rules"] as List?;
KRLogUtil.kr_i('✅ HiddifyOptions 已生成,包含 ${rules?.length ?? 0} 条自定义路由规则', tag: 'SingBox');
KRLogUtil.kr_i('✅ HiddifyOptions 已生成,包含 ${rules?.length ?? 0} 条自定义路由规则',
tag: 'SingBox');
if (rules != null && rules.isNotEmpty) {
for (var rule in rules) {
final ruleMap = rule as Map<String, dynamic>;
KRLogUtil.kr_i(' - 规则: domains=${ruleMap["domains"]}, outbound=${ruleMap["outbound"]}', tag: 'SingBox');
KRLogUtil.kr_i(
' - 规则: domains=${ruleMap["domains"]}, outbound=${ruleMap["outbound"]}',
tag: 'SingBox');
}
}
@@ -641,21 +686,16 @@ class KRSingBoxImp {
List<Map<String, dynamic>> _kr_buildHiddifyRules() {
final rules = <Map<String, dynamic>>[];
rules.add({
"domains": "domain:api.hifast.biz",
"outbound": "bypass"
});
rules.add({"domains": "domain:api.hifast.biz", "outbound": "bypass"});
final nodeDomains = _kr_collectNodeDomains();
for (final d in nodeDomains) {
rules.add({
"domains": "domain:$d",
"outbound": "bypass"
});
rules.add({"domains": "domain:$d", "outbound": "bypass"});
}
KRLogUtil.kr_i('✅ 节点域名白名单数量: ${nodeDomains.length}', tag: 'SingBox');
KRLogUtil.kr_i('✅ 节点域名白名单集合: ${jsonEncode(nodeDomains.toList())}', tag: 'SingBox');
KRLogUtil.kr_i('✅ 节点域名白名单集合: ${jsonEncode(nodeDomains.toList())}',
tag: 'SingBox');
return rules;
}
@@ -664,21 +704,25 @@ class KRSingBoxImp {
void addFromOutbound(Map<String, dynamic> o) {
final server = o['server']?.toString();
if (server != null && server.isNotEmpty && InternetAddress.tryParse(server) == null) {
if (server != null &&
server.isNotEmpty &&
InternetAddress.tryParse(server) == null) {
set.add(server.toLowerCase());
}
final tls = o['tls'];
if (tls is Map) {
final sni = tls['server_name']?.toString();
if (sni != null && sni.isNotEmpty && InternetAddress.tryParse(sni) == null) {
if (sni != null &&
sni.isNotEmpty &&
InternetAddress.tryParse(sni) == null) {
set.add(sni.toLowerCase());
}
}
}
for (final g in kr_outbounds) {
addFromOutbound(g);
addFromOutbound(g);
}
return set;
@@ -706,7 +750,7 @@ class KRSingBoxImp {
_kr_subscriptions.add(
kr_singBox.watchStatus().listen(
(status) {
(status) {
if (kDebugMode) {
print('🔵 收到 Native 状态更新: ${status.runtimeType}');
}
@@ -744,7 +788,7 @@ class KRSingBoxImp {
// 所以外层必须有 try-catch
final stream = kr_singBox.watchStats();
final subscription = stream.listen(
(stats) {
(stats) {
kr_stats.value = stats;
},
onError: (error) {
@@ -769,7 +813,7 @@ class KRSingBoxImp {
_kr_subscriptions.add(
kr_singBox.watchActiveGroups().listen(
(groups) {
(groups) {
print('[watchActiveGroups] 📡 收到活动组更新,数量: ${groups.length}');
KRLogUtil.kr_i('📡 收到活动组更新,数量: ${groups.length}', tag: 'SingBox');
kr_activeGroups.value = groups;
@@ -777,10 +821,14 @@ class KRSingBoxImp {
// 详细打印每个组的信息
for (int i = 0; i < groups.length; i++) {
final group = groups[i];
KRLogUtil.kr_i('📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'SingBox');
KRLogUtil.kr_i(
'📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}',
tag: 'SingBox');
for (int j = 0; j < group.items.length; j++) {
final item = group.items[j];
KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox');
KRLogUtil.kr_i(
' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}',
tag: 'SingBox');
}
}
@@ -796,13 +844,14 @@ class KRSingBoxImp {
_kr_subscriptions.add(
kr_singBox.watchGroups().listen(
(groups) {
(groups) {
print('[watchGroups] 📡 收到所有组更新,数量: ${groups.length}');
kr_allGroups.value = groups;
// 打印每个组的基本信息
for (int i = 0; i < groups.length; i++) {
final group = groups[i];
print('[watchGroups] 组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}');
print(
'[watchGroups] 组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}');
}
},
onError: (error) {
@@ -812,7 +861,8 @@ class KRSingBoxImp {
cancelOnError: false,
),
);
print('[_kr_subscribeToGroups] ✅ 分组数据流订阅完成,当前订阅数: ${_kr_subscriptions.length}');
print(
'[_kr_subscribeToGroups] ✅ 分组数据流订阅完成,当前订阅数: ${_kr_subscriptions.length}');
}
/// 验证节点选择是否生效
@@ -824,7 +874,7 @@ class KRSingBoxImp {
// 查找 "select" 组
final selectGroup = kr_activeGroups.firstWhere(
(group) => group.tag == 'select',
(group) => group.tag == 'select',
orElse: () => throw Exception('未找到 "select" 选择器组'),
);
@@ -853,7 +903,8 @@ class KRSingBoxImp {
throw Exception('节点切换失败:实际选中 ${selectGroup.selected},期望 $targetTag');
}
KRLogUtil.kr_i('✅ 节点切换验证成功: ${selectGroup.selected} == $targetTag', tag: 'SingBox');
KRLogUtil.kr_i('✅ 节点切换验证成功: ${selectGroup.selected} == $targetTag',
tag: 'SingBox');
} catch (e) {
KRLogUtil.kr_e('❌ 节点验证异常: $e', tag: 'SingBox');
// 不抛出异常,只记录日志,避免阻塞流程
@@ -864,11 +915,11 @@ class KRSingBoxImp {
///
/// 确保 command.sock 准备好后再执行节点选择
Future<void> _kr_selectOutboundWithRetry(
String groupTag,
String outboundTag, {
int maxAttempts = 3,
int initialDelay = 100,
}) async {
String groupTag,
String outboundTag, {
int maxAttempts = 3,
int initialDelay = 100,
}) async {
int attempt = 0;
int delay = initialDelay;
@@ -931,7 +982,8 @@ class KRSingBoxImp {
try {
// 先验证 command.sock 是否可访问
final socketFile = File(p.join(kr_configDics.baseDir.path, 'command.sock'));
final socketFile =
File(p.join(kr_configDics.baseDir.path, 'command.sock'));
KRLogUtil.kr_i('🔍 检查 socket 文件: ${socketFile.path}', tag: 'SingBox');
if (!socketFile.existsSync()) {
@@ -949,7 +1001,7 @@ class KRSingBoxImp {
// 这样可以避免阻塞当前的异步执行
try {
KRLogUtil.kr_i('📊 订阅统计数据流...', tag: 'SingBox');
await Future.delayed(Duration.zero); // 让出 UI 线程
await Future.delayed(Duration.zero); // 让出 UI 线程
_kr_subscribeToStats();
statsSubscribed = true;
KRLogUtil.kr_i('✅ 统计数据流订阅成功', tag: 'SingBox');
@@ -959,7 +1011,7 @@ class KRSingBoxImp {
try {
KRLogUtil.kr_i('📋 订阅分组数据流...', tag: 'SingBox');
await Future.delayed(Duration.zero); // 让出 UI 线程
await Future.delayed(Duration.zero); // 让出 UI 线程
_kr_subscribeToGroups();
groupsSubscribed = true;
KRLogUtil.kr_i('✅ 分组数据流订阅成功', tag: 'SingBox');
@@ -981,7 +1033,8 @@ class KRSingBoxImp {
throw Exception('订阅列表为空,command client 未成功连接');
}
KRLogUtil.kr_i('✅ 命令客户端初始化成功,活跃订阅数: ${_kr_subscriptions.length}', tag: 'SingBox');
KRLogUtil.kr_i('✅ 命令客户端初始化成功,活跃订阅数: ${_kr_subscriptions.length}',
tag: 'SingBox');
return;
} catch (e, stackTrace) {
// 详细记录失败原因和堆栈信息
@@ -1024,7 +1077,8 @@ class KRSingBoxImp {
await Future.delayed(const Duration(milliseconds: 1500));
// 检查 socket 文件
final socketFile = File(p.join(kr_configDics.baseDir.path, 'command.sock'));
final socketFile =
File(p.join(kr_configDics.baseDir.path, 'command.sock'));
if (!socketFile.existsSync()) {
KRLogUtil.kr_w('⚠️ command.sock 尚未创建,预连接取消', tag: 'SingBox');
return;
@@ -1047,7 +1101,6 @@ class KRSingBoxImp {
} catch (e) {
KRLogUtil.kr_w('⚠️ 分组流预订阅失败(正常,UI 调用时会重试): $e', tag: 'SingBox');
}
} catch (e) {
// 静默失败,不影响主流程
KRLogUtil.kr_w('⚠️ 后台预连接任务失败(不影响正常使用): $e', tag: 'SingBox');
@@ -1061,7 +1114,8 @@ class KRSingBoxImp {
Future<void> _kr_ensureCommandClientInitialized() async {
// 如果已经有订阅,说明 command client 已初始化
if (_kr_subscriptions.isNotEmpty) {
KRLogUtil.kr_i('✅ Command client 已初始化(订阅数: ${_kr_subscriptions.length}', tag: 'SingBox');
KRLogUtil.kr_i('✅ Command client 已初始化(订阅数: ${_kr_subscriptions.length}',
tag: 'SingBox');
return;
}
@@ -1096,7 +1150,8 @@ class KRSingBoxImp {
// 等待 command client 初始化
await Future.delayed(const Duration(milliseconds: 2000));
final savedNode = await KRSecureStorage().kr_readData(key: _keySelectedNode);
final savedNode =
await KRSecureStorage().kr_readData(key: _keySelectedNode);
if (savedNode != null && savedNode.isNotEmpty && savedNode != 'auto') {
KRLogUtil.kr_i('🔄 恢复用户选择的节点: $savedNode', tag: 'SingBox');
@@ -1104,7 +1159,8 @@ class KRSingBoxImp {
await _kr_selectOutboundWithRetry("select", savedNode);
KRLogUtil.kr_i('✅ 节点恢复完成', tag: 'SingBox');
} catch (e) {
KRLogUtil.kr_w('⚠️ 节点恢复失败(可能 command client 未就绪): $e', tag: 'SingBox');
KRLogUtil.kr_w('⚠️ 节点恢复失败(可能 command client 未就绪): $e',
tag: 'SingBox');
}
} else {
KRLogUtil.kr_i('️ 使用默认节点选择 (auto)', tag: 'SingBox');
@@ -1151,7 +1207,8 @@ class KRSingBoxImp {
KRLogUtil.kr_i(' - type: ${outbound['type']}', tag: 'SingBox');
KRLogUtil.kr_i(' - tag: ${outbound['tag']}', tag: 'SingBox');
KRLogUtil.kr_i(' - server: ${outbound['server']}', tag: 'SingBox');
KRLogUtil.kr_i(' - server_port: ${outbound['server_port']}', tag: 'SingBox');
KRLogUtil.kr_i(' - server_port: ${outbound['server_port']}',
tag: 'SingBox');
if (outbound['method'] != null) {
KRLogUtil.kr_i(' - method: ${outbound['method']}', tag: 'SingBox');
}
@@ -1159,10 +1216,14 @@ class KRSingBoxImp {
KRLogUtil.kr_i(' - interval: ${outbound['interval']}', tag: 'SingBox');
}
if (outbound['password'] != null) {
KRLogUtil.kr_i(' - password: ${outbound['password']?.toString().substring(0, 8)}...', tag: 'SingBox');
KRLogUtil.kr_i(
' - password: ${outbound['password']?.toString().substring(0, 8)}...',
tag: 'SingBox');
}
if (outbound['uuid'] != null) {
KRLogUtil.kr_i(' - uuid: ${outbound['uuid']?.toString().substring(0, 8)}...', tag: 'SingBox');
KRLogUtil.kr_i(
' - uuid: ${outbound['uuid']?.toString().substring(0, 8)}...',
tag: 'SingBox');
}
KRLogUtil.kr_i(' - 完整配置: ${jsonEncode(outbound)}', tag: 'SingBox');
}
@@ -1171,22 +1232,21 @@ class KRSingBoxImp {
kr_outbounds = outbounds.where((outbound) {
final type = outbound['type'];
if (type == 'hysteria2' || type == 'hysteria') {
KRLogUtil.kr_w('⚠️ 跳过 Hysteria2 节点: ${outbound['tag']} (libcore bug)', tag: 'SingBox');
KRLogUtil.kr_w('⚠️ 跳过 Hysteria2 节点: ${outbound['tag']} (libcore bug)',
tag: 'SingBox');
return false;
}
return true;
}).toList();
KRLogUtil.kr_i('✅ 过滤后节点数量: ${kr_outbounds.length}/${outbounds.length}', tag: 'SingBox');
KRLogUtil.kr_i('✅ 过滤后节点数量: ${kr_outbounds.length}/${outbounds.length}',
tag: 'SingBox');
// 🔧 修复:生成完整的 SingBox 配置
// 之前只保存 {"outbounds": [...]}, 导致 libcore 无法正确处理
// 现在保存完整配置,包含所有必需字段
final Map<String, dynamic> fullConfig = {
"log": {
"level": "debug",
"timestamp": true
},
"log": {"level": "debug", "timestamp": true},
"dns": {
"servers": [
{
@@ -1194,13 +1254,9 @@ class KRSingBoxImp {
"address": "https://1.1.1.1/dns-query",
"address_resolver": "dns-direct"
},
{
"tag": "dns-direct",
"address": "local",
"detour": "direct"
}
{"tag": "dns-direct", "address": "local", "detour": "direct"}
],
"rules": _kr_buildDnsRules(), // ✅ 使用动态构建的 DNS 规则
"rules": _kr_buildDnsRules(), // ✅ 使用动态构建的 DNS 规则
"final": "dns-remote",
"strategy": "prefer_ipv4"
},
@@ -1222,26 +1278,18 @@ class KRSingBoxImp {
"type": "selector",
"tag": "proxy",
"outbounds": kr_outbounds.map((o) => o['tag'] as String).toList(),
"default": kr_outbounds.isNotEmpty ? kr_outbounds[0]['tag'] : "direct",
"default":
kr_outbounds.isNotEmpty ? kr_outbounds[0]['tag'] : "direct",
},
...kr_outbounds,
{
"type": "direct",
"tag": "direct"
},
{
"type": "block",
"tag": "block"
},
{
"type": "dns",
"tag": "dns-out"
}
{"type": "direct", "tag": "direct"},
{"type": "block", "tag": "block"},
{"type": "dns", "tag": "dns-out"}
],
"route": {
"rules": _kr_buildRouteRules(), // ✅ 使用动态构建的路由规则
"rule_set": _kr_buildRuleSets(), // ✅ 使用动态构建的规则集
"final": "proxy", // 🔧 修复:使用 selector 组作为默认出站
"rules": _kr_buildRouteRules(), // ✅ 使用动态构建的路由规则
"rule_set": _kr_buildRuleSets(), // ✅ 使用动态构建的规则集
"final": "proxy", // 🔧 修复:使用 selector 组作为默认出站
"auto_detect_interface": true
}
};
@@ -1252,7 +1300,9 @@ class KRSingBoxImp {
KRLogUtil.kr_i('📄 完整配置文件长度: ${mapStr.length}', tag: 'SingBox');
KRLogUtil.kr_i('📄 Outbounds 数量: ${kr_outbounds.length}', tag: 'SingBox');
KRLogUtil.kr_i('📄 配置前800字符:\n${mapStr.substring(0, mapStr.length > 800 ? 800 : mapStr.length)}', tag: 'SingBox');
KRLogUtil.kr_i(
'📄 配置前800字符:\n${mapStr.substring(0, mapStr.length > 800 ? 800 : mapStr.length)}',
tag: 'SingBox');
await file.writeAsString(mapStr);
await temp.writeAsString(mapStr);
@@ -1276,7 +1326,8 @@ class KRSingBoxImp {
// 🔧 关键修复:只有在"智能代理"模式下,才根据国家添加直连规则
// 如果是"全局代理"模式,即使选择了国家也不添加直连规则
if (kr_connectionType.value == KRConnectionType.rule && currentCountryCode != 'other') {
if (kr_connectionType.value == KRConnectionType.rule &&
currentCountryCode != 'other') {
rules.add({
"rule_set": [
"geoip-$currentCountryCode",
@@ -1285,7 +1336,8 @@ class KRSingBoxImp {
"server": "dns-direct"
});
KRLogUtil.kr_i('✅ [智能代理模式] 添加 DNS 规则: $currentCountryCode 域名使用直连 DNS', tag: 'SingBox');
KRLogUtil.kr_i('✅ [智能代理模式] 添加 DNS 规则: $currentCountryCode 域名使用直连 DNS',
tag: 'SingBox');
} else if (kr_connectionType.value == KRConnectionType.global) {
KRLogUtil.kr_i('🌐 [全局代理模式] 跳过国家 DNS 规则,所有DNS查询走代理', tag: 'SingBox');
}
@@ -1299,10 +1351,7 @@ class KRSingBoxImp {
final rules = <Map<String, dynamic>>[];
// 基础规则: DNS 查询走 dns-out
rules.add({
"protocol": "dns",
"outbound": "dns-out"
});
rules.add({"protocol": "dns", "outbound": "dns-out"});
// ✅ 自定义域名直连规则(优先级高,放在前面)
// rules.add({
@@ -1314,7 +1363,8 @@ class KRSingBoxImp {
// 🔧 关键修复:只有在"智能代理"模式下,才根据国家添加直连规则
// 如果是"全局代理"模式,即使选择了国家也不添加直连规则
if (kr_connectionType.value == KRConnectionType.rule && currentCountryCode != 'other') {
if (kr_connectionType.value == KRConnectionType.rule &&
currentCountryCode != 'other') {
rules.add({
"rule_set": [
"geoip-$currentCountryCode",
@@ -1323,7 +1373,8 @@ class KRSingBoxImp {
"outbound": "direct"
});
KRLogUtil.kr_i('✅ [智能代理模式] 添加路由规则: $currentCountryCode IP/域名直连', tag: 'SingBox');
KRLogUtil.kr_i('✅ [智能代理模式] 添加路由规则: $currentCountryCode IP/域名直连',
tag: 'SingBox');
} else if (kr_connectionType.value == KRConnectionType.global) {
KRLogUtil.kr_i('🌐 [全局代理模式] 跳过国家路由规则,所有流量走代理', tag: 'SingBox');
}
@@ -1338,10 +1389,13 @@ class KRSingBoxImp {
// 🔧 关键修复:只有在"智能代理"模式下,才加载国家规则集
// 如果是"全局代理"模式,即使选择了国家也不加载规则集
if (kr_connectionType.value == KRConnectionType.rule && currentCountryCode != 'other') {
if (kr_connectionType.value == KRConnectionType.rule &&
currentCountryCode != 'other') {
// 检查本地文件是否存在
final geoipFile = File(p.join(kr_configDics.workingDir.path, 'geosite', 'geoip-$currentCountryCode.srs'));
final geositeFile = File(p.join(kr_configDics.workingDir.path, 'geosite', 'geosite-$currentCountryCode.srs'));
final geoipFile = File(p.join(kr_configDics.workingDir.path, 'geosite',
'geoip-$currentCountryCode.srs'));
final geositeFile = File(p.join(kr_configDics.workingDir.path, 'geosite',
'geosite-$currentCountryCode.srs'));
if (geoipFile.existsSync() && geositeFile.existsSync()) {
// ✅ 使用本地文件
@@ -1349,7 +1403,7 @@ class KRSingBoxImp {
"type": "local",
"tag": "geoip-$currentCountryCode",
"format": "binary",
"path": "./geosite/geoip-$currentCountryCode.srs" // 相对于 workingDir
"path": "./geosite/geoip-$currentCountryCode.srs" // 相对于 workingDir
});
ruleSets.add({
@@ -1360,19 +1414,25 @@ class KRSingBoxImp {
});
KRLogUtil.kr_i('✅ 使用本地规则集: $currentCountryCode', tag: 'SingBox');
KRLogUtil.kr_i(' - geoip: ./geosite/geoip-$currentCountryCode.srs', tag: 'SingBox');
KRLogUtil.kr_i(' - geosite: ./geosite/geosite-$currentCountryCode.srs', tag: 'SingBox');
KRLogUtil.kr_i(' - geoip: ./geosite/geoip-$currentCountryCode.srs',
tag: 'SingBox');
KRLogUtil.kr_i(
' - geosite: ./geosite/geosite-$currentCountryCode.srs',
tag: 'SingBox');
} else {
// ❌ 本地文件不存在,使用远程规则集作为后备
KRLogUtil.kr_w('⚠️ 本地规则集不存在,使用远程规则集', tag: 'SingBox');
KRLogUtil.kr_w(' - geoip 文件存在: ${geoipFile.existsSync()}', tag: 'SingBox');
KRLogUtil.kr_w(' - geosite 文件存在: ${geositeFile.existsSync()}', tag: 'SingBox');
KRLogUtil.kr_w(' - geoip 文件存在: ${geoipFile.existsSync()}',
tag: 'SingBox');
KRLogUtil.kr_w(' - geosite 文件存在: ${geositeFile.existsSync()}',
tag: 'SingBox');
ruleSets.add({
"type": "remote",
"tag": "geoip-$currentCountryCode",
"format": "binary",
"url": "https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/country/geoip-$currentCountryCode.srs",
"url":
"https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/country/geoip-$currentCountryCode.srs",
"download_detour": "direct",
"update_interval": "7d"
});
@@ -1381,7 +1441,8 @@ class KRSingBoxImp {
"type": "remote",
"tag": "geosite-$currentCountryCode",
"format": "binary",
"url": "https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/country/geosite-$currentCountryCode.srs",
"url":
"https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/country/geosite-$currentCountryCode.srs",
"download_detour": "direct",
"update_interval": "7d"
});
@@ -1418,7 +1479,8 @@ class KRSingBoxImp {
if (Platform.isWindows && !_dnsBackedUp) {
KRLogUtil.kr_i('🪟 Windows 平台,首次启动备份 DNS 设置...', tag: 'SingBox');
try {
final backupSuccess = await KRWindowsDnsUtil.instance.kr_backupDnsSettings();
final backupSuccess =
await KRWindowsDnsUtil.instance.kr_backupDnsSettings();
if (backupSuccess) {
_dnsBackedUp = true; // 标记已备份
KRLogUtil.kr_i('✅ Windows DNS 备份成功', tag: 'SingBox');
@@ -1449,11 +1511,11 @@ class KRSingBoxImp {
final oOption = SingboxConfigOption.fromJson(_getConfigOption());
final changeResult = await kr_singBox.changeOptions(oOption).run();
changeResult.match(
(error) {
(error) {
KRLogUtil.kr_e('❌ changeOptions() 失败: $error', tag: 'SingBox');
throw Exception('初始化 HiddifyOptions 失败: $error');
},
(_) {
(_) {
KRLogUtil.kr_i('✅ HiddifyOptions 初始化成功', tag: 'SingBox');
},
);
@@ -1463,7 +1525,9 @@ class KRSingBoxImp {
if (await configFile.exists()) {
final configContent = await configFile.readAsString();
KRLogUtil.kr_i('📄 配置文件内容长度: ${configContent.length}', tag: 'SingBox');
KRLogUtil.kr_i('📄 配置文件前500字符: ${configContent.substring(0, configContent.length > 500 ? 500 : configContent.length)}', tag: 'SingBox');
KRLogUtil.kr_i(
'📄 配置文件前500字符: ${configContent.substring(0, configContent.length > 500 ? 500 : configContent.length)}',
tag: 'SingBox');
} else {
KRLogUtil.kr_w('⚠️ 配置文件不存在: $_cutPath', tag: 'SingBox');
}
@@ -1472,7 +1536,7 @@ class KRSingBoxImp {
_kr_subscribeToStatus();
await kr_singBox.start(_cutPath, kr_configName, false).map(
(r) {
(r) {
KRLogUtil.kr_i('✅ SingBox 启动成功', tag: 'SingBox');
},
).mapLeft((err) {
@@ -1528,7 +1592,8 @@ class KRSingBoxImp {
// 🔧 关键修复:恢复用户选择的节点
try {
final selectedNode = await KRSecureStorage().kr_readData(key: _keySelectedNode);
final selectedNode =
await KRSecureStorage().kr_readData(key: _keySelectedNode);
if (selectedNode != null && selectedNode.isNotEmpty) {
KRLogUtil.kr_i('🎯 恢复用户选择的节点: $selectedNode', tag: 'SingBox');
if (kDebugMode) {
@@ -1588,7 +1653,7 @@ class KRSingBoxImp {
// 🔧 延长超时时间到 10 秒,给 Windows DNS 清理足够时间
try {
await kr_singBox.stop().run().timeout(
const Duration(seconds: 10), // ← 从 3 秒延长到 10 秒
const Duration(seconds: 10), // ← 从 3 秒延长到 10 秒
onTimeout: () {
KRLogUtil.kr_w('⚠️ 停止操作超时(10秒),强制继续', tag: 'SingBox');
return const Left('timeout');
@@ -1638,7 +1703,8 @@ class KRSingBoxImp {
try {
// 尝试恢复 DNS
final restoreSuccess = await KRWindowsDnsUtil.instance.kr_restoreDnsSettings();
final restoreSuccess =
await KRWindowsDnsUtil.instance.kr_restoreDnsSettings();
if (restoreSuccess) {
KRLogUtil.kr_i('✅ Windows DNS 恢复成功', tag: 'SingBox');
} else {
@@ -1765,9 +1831,9 @@ class KRSingBoxImp {
mode = KRCountryUtil.kr_getCurrentCountryCode();
KRLogUtil.kr_i('🎯 切换到规则代理模式: $mode', tag: 'SingBox');
break;
// case KRConnectionType.direct:
// mode = "direct";
// break;
// case KRConnectionType.direct:
// mode = "direct";
// break;
}
oOption["region"] = mode;
KRLogUtil.kr_i('📝 更新 region 配置: $mode', tag: 'SingBox');
@@ -1842,6 +1908,25 @@ class KRSingBoxImp {
KRLogUtil.kr_i('🎯 [v2.1] 开始选择出站节点: $tag', tag: 'SingBox');
KRLogUtil.kr_i('📊 当前活动组数量: ${kr_activeGroups.length}', tag: 'SingBox');
// 如果活动组尚未就绪,等待一段时间;仍为空则仅保存选择,稍后恢复
if (kr_activeGroups.isEmpty) {
for (int i = 0; i < 20; i++) {
await Future.delayed(const Duration(milliseconds: 100));
if (kr_activeGroups.isNotEmpty) break;
}
if (kr_activeGroups.isEmpty) {
KRLogUtil.kr_w('⚠️ 活动组为空,跳过即时切换,仅保存选择: $tag', tag: 'SingBox');
try {
await KRSecureStorage()
.kr_saveData(key: _keySelectedNode, value: tag);
KRLogUtil.kr_i('✅ 节点选择已保存: $tag', tag: 'SingBox');
} catch (e) {
KRLogUtil.kr_e('❌ 保存节点选择失败: $e', tag: 'SingBox');
}
return;
}
}
// 🔧 诊断:打印所有活动组的节点,确保目标节点存在
KRLogUtil.kr_i('🔍 搜索目标节点 "$tag" 在活动组中...', tag: 'SingBox');
bool foundNode = false;
@@ -1880,9 +1965,12 @@ class KRSingBoxImp {
// 🔧 关键修复:使用正确的 group tag
// libcore 生成的selector组的tag是"proxy"而不是"select"
final selectorGroupTag = kr_activeGroups.any((g) => g.tag == 'select') ? 'select' : 'proxy';
KRLogUtil.kr_i('⏳ 调用 selectOutbound("$selectorGroupTag", "$tag")...', tag: 'SingBox');
await _kr_selectOutboundWithRetry(selectorGroupTag, tag, maxAttempts: 3, initialDelay: 50);
final selectorGroupTag =
kr_activeGroups.any((g) => g.tag == 'select') ? 'select' : 'proxy';
KRLogUtil.kr_i('⏳ 调用 selectOutbound("$selectorGroupTag", "$tag")...',
tag: 'SingBox');
await _kr_selectOutboundWithRetry(selectorGroupTag, tag,
maxAttempts: 3, initialDelay: 50);
KRLogUtil.kr_i('✅ 节点切换API调用完成: $tag', tag: 'SingBox');
// 🔧 新增:验证节点切换是否生效
@@ -1900,13 +1988,14 @@ class KRSingBoxImp {
_nodeSelectionTimer?.cancel();
if (tag != 'auto') {
KRLogUtil.kr_i('🔁 启动节点选择监控,防止被 auto 覆盖', tag: 'SingBox');
_nodeSelectionTimer = Timer.periodic(const Duration(seconds: 20), (timer) {
_nodeSelectionTimer =
Timer.periodic(const Duration(seconds: 20), (timer) {
// 每 20 秒重新选择一次,确保用户选择不被覆盖
// 使用 then/catchError 避免异常导致 UI 阻塞
kr_singBox.selectOutbound("select", tag).run().then((result) {
result.match(
(error) => KRLogUtil.kr_w('🔁 定时器重选节点失败: $error', tag: 'SingBox'),
(_) => KRLogUtil.kr_d('🔁 定时器重选节点成功', tag: 'SingBox'),
(error) => KRLogUtil.kr_w('🔁 定时器重选节点失败: $error', tag: 'SingBox'),
(_) => KRLogUtil.kr_d('🔁 定时器重选节点成功', tag: 'SingBox'),
);
}).catchError((error) {
KRLogUtil.kr_w('🔁 定时器重选节点异常: $error', tag: 'SingBox');
@@ -1935,10 +2024,14 @@ class KRSingBoxImp {
// 打印所有活动组信息
for (int i = 0; i < kr_activeGroups.length; i++) {
final group = kr_activeGroups[i];
KRLogUtil.kr_i('📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'SingBox');
KRLogUtil.kr_i(
'📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}',
tag: 'SingBox');
for (int j = 0; j < group.items.length; j++) {
final item = group.items[j];
KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox');
KRLogUtil.kr_i(
' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}',
tag: 'SingBox');
}
}
@@ -1954,10 +2047,14 @@ class KRSingBoxImp {
KRLogUtil.kr_i('🔄 测试后活动组状态检查:', tag: 'SingBox');
for (int i = 0; i < kr_activeGroups.length; i++) {
final group = kr_activeGroups[i];
KRLogUtil.kr_i('📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'SingBox');
KRLogUtil.kr_i(
'📋 活动组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}',
tag: 'SingBox');
for (int j = 0; j < group.items.length; j++) {
final item = group.items[j];
KRLogUtil.kr_i(' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}', tag: 'SingBox');
KRLogUtil.kr_i(
' └─ 节点[$j]: tag=${item.tag}, type=${item.type}, delay=${item.urlTestDelay}',
tag: 'SingBox');
}
}
} catch (e) {