import 'dart:convert'; import 'dart:io'; import 'dart:async'; import 'package:flutter/services.dart'; import 'package:fpdart/fpdart.dart'; import 'package:get/get.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; 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 '../../../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 '../../utils/kr_country_util.dart'; import '../../utils/kr_log_util.dart'; import '../../utils/kr_secure_storage.dart'; import '../../utils/kr_windows_dns_util.dart'; import '../../common/app_run_data.dart'; import 'package:flutter/foundation.dart'; enum KRConnectionType { global, rule, // direct, } class KRSingBoxImp { /// 私有构造函数 KRSingBoxImp._(); /// 单例实例 static final KRSingBoxImp _instance = KRSingBoxImp._(); /// 工厂构造函数 factory KRSingBoxImp() => _instance; /// 获取实例的静态方法 static KRSingBoxImp get instance => _instance; /// 配置文件目录 late Directories kr_configDics; /// 配置文件名称 String kr_configName = "hiFastVPN"; /// 存储键:用户选择的节点 static const String _keySelectedNode = 'SELECTED_NODE_TAG'; /// 通道方法 final _kr_methodChannel = const MethodChannel("com.hi.app/platform"); final _kr_container = ProviderContainer(); /// 核心服务 late SingboxService kr_singBox; /// more配置 Map kr_configOption = {}; List> kr_outbounds = []; /// 首次启动 RxBool kr_isFristStart = false.obs; /// 状态 final kr_status = SingboxStatus.stopped().obs; /// 拦截广告 final kr_blockAds = false.obs; /// 是否自动自动选择线路 final kr_isAutoOutbound = false.obs; /// 连接类型 final kr_connectionType = KRConnectionType.rule.obs; String _cutPath = ""; /// 端口 int kr_port = 51213; /// 统计 final kr_stats = SingboxStats( connectionsIn: 0, connectionsOut: 0, uplink: 0, downlink: 0, uplinkTotal: 0, downlinkTotal: 0, ).obs; /// 活动的出站分组 RxList kr_activeGroups = [].obs; // 🔒 Windows DNS 优化:标志位,记录 DNS 是否已备份 bool _dnsBackedUp = false; /// 所有的出站分组 RxList kr_allGroups = [].obs; /// Stream 订阅管理器 final List> _kr_subscriptions = []; /// 初始化标志,防止重复初始化 bool _kr_isInitialized = false; /// 初始化进行中共享 Future(single-flight 防并发) Future? _kr_initFuture; /// 当前混合代理端口是否就绪 bool get kr_isProxyReady => kr_status.value is SingboxStarted; String? _lastProxyRule; /// 构建 Dart HttpClient 可识别的代理规则字符串 /// /// 当 sing-box 尚未启动时返回 `DIRECT`,启动后返回 /// `PROXY 127.0.0.1:; DIRECT`,以便在代理不可用时自动回落。 String kr_buildProxyRule({bool includeDirectFallback = true}) { if (!kr_isProxyReady) { const directRule = 'DIRECT'; if (_lastProxyRule != directRule) { KRLogUtil.kr_i('⏳ sing-box 未就绪,使用 DIRECT 直连', tag: 'SingBox'); _lastProxyRule = directRule; } return directRule; } final proxyRule = StringBuffer('PROXY 127.0.0.1:$kr_port'); if (includeDirectFallback) { proxyRule.write('; DIRECT'); } final ruleString = proxyRule.toString(); if (_lastProxyRule != ruleString) { KRLogUtil.kr_i('🛠️ 使用代理规则: $ruleString', tag: 'SingBox'); _lastProxyRule = ruleString; } return ruleString; } /// 初始化 Future init() async { // 防止重复初始化(已完成) if (_kr_isInitialized) { KRLogUtil.kr_i('SingBox 已经初始化,跳过重复初始化', tag: 'SingBox'); return; } // 防止并发重复初始化(进行中复用同一个 Future) if (_kr_initFuture != null) { KRLogUtil.kr_i('SingBox 初始化进行中,等待完成(single-flight)', tag: 'SingBox'); await _kr_initFuture; return; } // 建立 single-flight 共享 Future final completer = Completer(); _kr_initFuture = completer.future; try { KRLogUtil.kr_i('开始初始化 SingBox'); // 在应用启动时初始化 await KRCountryUtil.kr_init(); KRLogUtil.kr_i('国家工具初始化完成'); final oOption = SingboxConfigOption.fromJson(_getConfigOption()); KRLogUtil.kr_i('配置选项初始化完成'); KRLogUtil.kr_i('开始初始化 SingBox 服务'); kr_singBox = await _kr_container.read(singboxServiceProvider); await _kr_container.read(singboxServiceProvider).init(); KRLogUtil.kr_i('SingBox 服务初始化完成'); KRLogUtil.kr_i('开始初始化目录'); /// 初始化目录 if (Platform.isIOS) { final paths = await _kr_methodChannel.invokeMethod("get_paths"); 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), ); } else { final baseDir = await getApplicationSupportDirectory(); final workingDir = Platform.isAndroid ? await getExternalStorageDirectory() : baseDir; final tempDir = await getTemporaryDirectory(); // Windows 路径规范化:确保使用正确的路径分隔符 Directory normalizePath(Directory dir) { if (Platform.isWindows) { final normalized = dir.path.replaceAll('/', '\\'); if (normalized != dir.path) { KRLogUtil.kr_i('路径规范化: ${dir.path} -> $normalized', tag: 'SingBox'); return Directory(normalized); } } return dir; } kr_configDics = ( 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('tempDir: ${kr_configDics.tempDir.path}', tag: 'SingBox'); // 确保所有目录都存在 if (!kr_configDics.baseDir.existsSync()) { await kr_configDics.baseDir.create(recursive: true); KRLogUtil.kr_i('已创建 baseDir', tag: 'SingBox'); } if (!kr_configDics.workingDir.existsSync()) { await kr_configDics.workingDir.create(recursive: true); KRLogUtil.kr_i('已创建 workingDir', tag: 'SingBox'); } if (!kr_configDics.tempDir.existsSync()) { await kr_configDics.tempDir.create(recursive: true); KRLogUtil.kr_i('已创建 tempDir', tag: 'SingBox'); } // 创建 libcore 数据库所需的 data 目录(在 workingDir 下) // 注意:libcore 的 Setup 会调用 os.Chdir(workingPath),所以 data 目录必须在 workingDir 下 final dataDir = Directory(p.join(kr_configDics.workingDir.path, 'data')); // 强制确保 data 目录存在(Windows 可能需要多次尝试) int retryCount = 0; const maxRetries = 5; while (!dataDir.existsSync() && retryCount < maxRetries) { try { await dataDir.create(recursive: true); // 等待文件系统同步(Windows 上可能需要一点时间) await Future.delayed(const Duration(milliseconds: 100)); // 验证目录确实创建成功 if (dataDir.existsSync()) { KRLogUtil.kr_i('✅ 已创建 data 目录: ${dataDir.path}', tag: 'SingBox'); break; } else { retryCount++; 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'); if (retryCount >= maxRetries) { throw Exception('无法创建 libcore 数据库目录: ${dataDir.path},错误: $e'); } final delayMs = 200 * retryCount; await Future.delayed(Duration(milliseconds: delayMs)); } } if (!dataDir.existsSync()) { final error = 'data 目录不存在: ${dataDir.path}'; KRLogUtil.kr_e('❌ $error', tag: 'SingBox'); throw Exception(error); } // 验证目录权限(尝试创建一个测试文件) try { final testFile = File(p.join(dataDir.path, '.test_write')); await testFile.writeAsString('test'); await testFile.delete(); KRLogUtil.kr_i('✅ data 目录写入权限验证通过', tag: 'SingBox'); } catch (e) { KRLogUtil.kr_e('⚠️ data 目录写入权限验证失败: $e', tag: 'SingBox'); // 不抛出异常,让 libcore 自己处理 } // 在 Windows 上额外等待,确保文件系统操作完成 if (Platform.isWindows) { await Future.delayed(const Duration(milliseconds: 300)); KRLogUtil.kr_i('⏳ Windows 文件系统同步等待完成', tag: 'SingBox'); } // 最终验证:在 setup() 之前再次确认 workingDir 和 data 目录都存在且可访问 // libcore 的 Setup() 会调用 os.Chdir(workingPath),然后使用相对路径 "./data" // 如果 os.Chdir() 失败(路径不存在或权限问题),后续的相对路径访问会失败 if (!kr_configDics.workingDir.existsSync()) { 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')); await testWorkingFile.writeAsString('test'); await testWorkingFile.delete(); KRLogUtil.kr_i('✅ workingDir 写入权限验证通过', tag: 'SingBox'); } catch (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')); 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'); 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'); } catch (e) { KRLogUtil.kr_e('无法列出 workingDir 内容: $e', tag: 'SingBox'); } } throw Exception('data 目录在 setup() 前验证失败: ${finalDataDir.path}'); } // 再次尝试写入测试,确保目录确实可用 try { final verifyFile = File(p.join(finalDataDir.path, '.verify_setup')); await verifyFile.writeAsString('verify'); await verifyFile.delete(); KRLogUtil.kr_i('✅ setup() 前最终验证通过', tag: 'SingBox'); } catch (e) { KRLogUtil.kr_e('❌ setup() 前最终验证失败: $e', tag: 'SingBox'); // 不抛出异常,让 setup() 自己处理 } 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'); } catch (e) { KRLogUtil.kr_e('⚠️ configs 目录创建失败: $e', tag: 'SingBox'); // 不抛出异常,继续初始化 } } else { KRLogUtil.kr_i('✅ configs 目录已存在: ${configsDir.path}', tag: 'SingBox'); } // 特别处理 extensionData.db 文件 (Windows特定) if (Platform.isWindows) { try { final extensionDataDbPath = p.join(finalDataDir.path, 'extensionData.db'); KRLogUtil.kr_i('👉 准备处理 extensionData.db 路径: $extensionDataDbPath', tag: 'SingBox'); // 确保 extensionData.db 的父目录存在 final extensionDataParent = Directory(p.dirname(extensionDataDbPath)); if (!extensionDataParent.existsSync()) { await extensionDataParent.create(recursive: true); KRLogUtil.kr_i('✅ 已创建 extensionData.db 父目录', tag: 'SingBox'); } // 测试文件创建权限 final testFile = File(p.join(extensionDataParent.path, '.test_extension')); await testFile.writeAsString('test'); await testFile.delete(); KRLogUtil.kr_i('✅ extensionData 目录权限验证通过', tag: 'SingBox'); } catch (e) { KRLogUtil.kr_e('⚠️ extensionData 目录处理失败: $e', tag: 'SingBox'); // 不抛出异常,继续初始化 } } 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'); // 在 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'); } else { KRLogUtil.kr_i(' - data 目录为空', tag: 'SingBox'); } } catch (e) { KRLogUtil.kr_e(' - 无法列出 data 目录内容: $e', 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'); } // 确保路径使用反斜杠(Windows 标准) final normalizedPath = workingPath.replaceAll('/', '\\'); if (normalizedPath != workingPath) { KRLogUtil.kr_e('⚠️ 路径格式可能需要规范化: $workingPath -> $normalizedPath', tag: 'SingBox'); } } // 🔑 关键步骤:调用 setup() 将 NativePort 传递给 libcore // 这样 libcore 才能通过 GoDart_PostCObject() 向 Dart 发送消息 KRLogUtil.kr_i('📡 开始调用 setup() 注册 FFI 端口', tag: 'SingBox'); final setupResult = await kr_singBox.setup(kr_configDics, false).run(); setupResult.match( (error) { KRLogUtil.kr_e('❌ setup() 失败: $error', tag: 'SingBox'); throw Exception('FFI setup 失败: $error'); }, (_) { KRLogUtil.kr_i('✅ setup() 成功,FFI 端口已注册', tag: 'SingBox'); }, ); KRLogUtil.kr_i('✅ SingBox 初始化完成'); // 🔧 提取 geosite 文件到 workingDir KRLogUtil.kr_i('📦 开始提取 geosite 文件...', tag: 'SingBox'); await _kr_extractGeositeFiles(); KRLogUtil.kr_i('✅ geosite 文件提取完成', tag: 'SingBox'); _kr_isInitialized = true; // 🔑 关键:在初始化完成后立即订阅状态变化流 // 这样可以确保 UI 始终与 libcore 的实际状态同步 _kr_subscribeToStatus(); KRLogUtil.kr_i('✅ 状态订阅已设置', tag: 'SingBox'); // 完成 single-flight completer.complete(); _kr_initFuture = null; } catch (e, stackTrace) { KRLogUtil.kr_e('❌ SingBox 初始化失败: $e'); KRLogUtil.kr_e('📚 错误堆栈: $stackTrace'); // 添加额外的诊断信息 if (Platform.isWindows) { try { final workingDir = kr_configDics.workingDir; final dataDir = Directory(p.join(workingDir.path, 'data')); final configsDir = Directory(p.join(workingDir.path, 'configs')); 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(' - data 目录: ${dataDir.path}', 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'); // 检查父目录内容 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'); } catch (listErr) { KRLogUtil.kr_e(' - 无法列出 workingDir 内容: $listErr', tag: 'SingBox'); } } } catch (diagErr) { KRLogUtil.kr_e(' - 诊断信息收集失败: $diagErr', tag: 'SingBox'); } } // 如果初始化失败,允许下次重试 _kr_isInitialized = false; // 失败时通知等待者并清理 single-flight if (!completer.isCompleted) { completer.completeError(e, stackTrace); } _kr_initFuture = null; rethrow; } } /// 提取 geosite 文件到 workingDir Future _kr_extractGeositeFiles() async { try { // 创建 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'); } // 需要提取的文件列表 final files = ['geoip-cn.srs', 'geosite-cn.srs']; for (final filename in files) { final assetPath = 'assets/geosite/$filename'; final targetPath = p.join(geositeDir.path, filename); final targetFile = File(targetPath); // 检查文件是否已存在 if (targetFile.existsSync()) { final fileSize = await targetFile.length(); KRLogUtil.kr_i('📄 $filename 已存在 (${fileSize} bytes), 跳过', tag: 'SingBox'); continue; } // 从 assets 加载文件 KRLogUtil.kr_i('📥 正在提取 $filename...', tag: 'SingBox'); final byteData = await rootBundle.load(assetPath); final bytes = byteData.buffer.asUint8List(); // 写入文件系统 await targetFile.writeAsBytes(bytes); final writtenSize = await targetFile.length(); KRLogUtil.kr_i('✅ 提取成功: $filename (${writtenSize} bytes)', tag: 'SingBox'); } KRLogUtil.kr_i('🎉 所有 geosite 文件提取完成', tag: 'SingBox'); } catch (e, stackTrace) { KRLogUtil.kr_e('❌ 提取 geosite 文件失败: $e', tag: 'SingBox'); KRLogUtil.kr_e('堆栈: $stackTrace', tag: 'SingBox'); // 不抛出异常,让应用继续运行(使用远程规则集作为后备) } } Map _getConfigOption() { // 不使用缓存,每次都重新生成配置 // if (kr_configOption.isNotEmpty) { // return kr_configOption; // } // 🔧 关键修复:全局代理模式下,region 强制设为 'other',libcore 就不会生成国家直连规则 final String effectiveRegion; if (kr_connectionType.value == KRConnectionType.global) { effectiveRegion = 'other'; // 全局代理:不添加任何国家规则 KRLogUtil.kr_i('🌐 [全局代理模式] region 设为 other,所有流量走代理', tag: 'SingBox'); } else { effectiveRegion = KRCountryUtil.kr_getCurrentCountryCode(); // 智能代理:使用用户选择的国家 KRLogUtil.kr_i('✅ [智能代理模式] region 设为 $effectiveRegion', tag: 'SingBox'); } final op = { "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 "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 死锁 "remote-dns-domain-strategy": "prefer_ipv4", "direct-dns-address": "local", // 使用系统 DNS,确保中转服务器域名能被解析 "direct-dns-domain-strategy": "prefer_ipv4", "mixed-port": kr_port, "tproxy-port": kr_port, "local-dns-port": 36450, "tun-implementation": "gvisor", "mtu": 9000, "strict-route": true, "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, "bypass-lan": false, "allow-connection-from-lan": false, "enable-fake-dns": false, "enable-dns-routing": true, "independent-dns-cache": true, "rules": _kr_buildHiddifyRules(), "mux": { "enable": false, "padding": false, "max-streams": 8, "protocol": "h2mux" }, "tls-tricks": { "enable-fragment": false, "fragment-size": "10-30", "fragment-sleep": "2-8", "mixed-sni-case": false, "enable-padding": false, "padding-size": "1-1500" }, "warp": { "enable": false, "mode": "proxy_over_warp", "wireguard-config": "", "license-key": "", "account-id": "", "access-token": "", "clean-ip": "auto", "clean-port": 0, "noise": "1-3", "noise-size": "10-30", "noise-delay": "10-30", "noise-mode": "m4" }, "warp2": { "enable": false, "mode": "proxy_over_warp", "wireguard-config": "", "license-key": "", "account-id": "", "access-token": "", "clean-ip": "auto", "clean-port": 0, "noise": "1-3", "noise-size": "10-30", "noise-delay": "10-30", "noise-mode": "m4" } }; kr_configOption = op; // 🔧 调试日志:确认自定义规则已添加 final rules = op["rules"] as List?; KRLogUtil.kr_i('✅ HiddifyOptions 已生成,包含 ${rules?.length ?? 0} 条自定义路由规则', tag: 'SingBox'); if (rules != null && rules.isNotEmpty) { for (var rule in rules) { final ruleMap = rule as Map; KRLogUtil.kr_i( ' - 规则: domains=${ruleMap["domains"]}, outbound=${ruleMap["outbound"]}', tag: 'SingBox'); } } return op; } List> _kr_buildHiddifyRules() { final rules = >[]; 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"}); } KRLogUtil.kr_i('✅ 节点域名白名单数量: ${nodeDomains.length}', tag: 'SingBox'); KRLogUtil.kr_i('✅ 节点域名白名单集合: ${jsonEncode(nodeDomains.toList())}', tag: 'SingBox'); return rules; } Set _kr_collectNodeDomains() { final set = {}; void addFromOutbound(Map o) { final server = o['server']?.toString(); 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) { set.add(sni.toLowerCase()); } } } for (final g in kr_outbounds) { addFromOutbound(g); } return set; } /// 订阅状态变化流 /// 参考 hiddify-app: 监听 libcore 发送的状态事件来自动更新 UI void _kr_subscribeToStatus() { if (kDebugMode) { print('🔵 _kr_subscribeToStatus 被调用,重新订阅状态流'); } KRLogUtil.kr_i('🔵 _kr_subscribeToStatus 被调用', tag: 'SingBox'); // 取消之前的状态订阅 for (var sub in _kr_subscriptions) { if (sub.hashCode.toString().contains('Status')) { sub.cancel(); if (kDebugMode) { print('🔵 已取消旧的状态订阅'); } } } _kr_subscriptions .removeWhere((sub) => sub.hashCode.toString().contains('Status')); _kr_subscriptions.add( kr_singBox.watchStatus().listen( (status) { if (kDebugMode) { print('🔵 收到 Native 状态更新: ${status.runtimeType}'); } KRLogUtil.kr_i('📡 收到状态更新: $status', tag: 'SingBox'); kr_status.value = status; }, onError: (error) { if (kDebugMode) { print('🔵 状态流错误: $error'); } KRLogUtil.kr_e('📡 状态流错误: $error', tag: 'SingBox'); }, cancelOnError: false, ), ); if (kDebugMode) { print('🔵 状态流订阅完成'); } } /// 订阅统计数据流 void _kr_subscribeToStats() { // 取消之前的统计订阅 for (var sub in _kr_subscriptions) { if (sub.hashCode.toString().contains('Stats')) { sub.cancel(); } } _kr_subscriptions .removeWhere((sub) => sub.hashCode.toString().contains('Stats')); // ⚠️ 关键:watchStats() 内部会调用 FFI startCommandClient // 如果此时 command.sock 未就绪,会抛出异常 // 所以外层必须有 try-catch final stream = kr_singBox.watchStats(); final subscription = stream.listen( (stats) { kr_stats.value = stats; }, onError: (error) { KRLogUtil.kr_e('统计数据监听错误: $error', tag: 'SingBox'); }, cancelOnError: false, ); _kr_subscriptions.add(subscription); } /// 订阅分组数据流 void _kr_subscribeToGroups() { print('[_kr_subscribeToGroups] 🚀 开始订阅分组数据流'); // 取消之前的分组订阅 for (var sub in _kr_subscriptions) { if (sub.hashCode.toString().contains('Groups')) { sub.cancel(); } } _kr_subscriptions .removeWhere((sub) => sub.hashCode.toString().contains('Groups')); _kr_subscriptions.add( kr_singBox.watchActiveGroups().listen( (groups) { print('[watchActiveGroups] 📡 收到活动组更新,数量: ${groups.length}'); KRLogUtil.kr_i('📡 收到活动组更新,数量: ${groups.length}', tag: 'SingBox'); kr_activeGroups.value = groups; // 详细打印每个组的信息 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'); 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('✅ 活动组处理完成', tag: 'SingBox'); }, onError: (error) { print('[watchActiveGroups] ❌ 活动分组监听错误: $error'); KRLogUtil.kr_e('❌ 活动分组监听错误: $error', tag: 'SingBox'); }, cancelOnError: false, ), ); _kr_subscriptions.add( kr_singBox.watchGroups().listen( (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}'); } }, onError: (error) { print('[watchGroups] ❌ 所有分组监听错误: $error'); KRLogUtil.kr_e('所有分组监听错误: $error'); }, cancelOnError: false, ), ); print( '[_kr_subscribeToGroups] ✅ 分组数据流订阅完成,当前订阅数: ${_kr_subscriptions.length}'); } /// 验证节点选择是否生效 /// /// 检查活动组中 "select" 组的 selected 字段是否是目标节点 Future _kr_verifyNodeSelection(String targetTag) async { try { KRLogUtil.kr_i('🔍 开始验证节点选择: $targetTag', tag: 'SingBox'); // 查找 "select" 组 final selectGroup = kr_activeGroups.firstWhere( (group) => group.tag == 'select', orElse: () => throw Exception('未找到 "select" 选择器组'), ); KRLogUtil.kr_i('📊 Select 组状态:', tag: 'SingBox'); KRLogUtil.kr_i(' - 组标签: ${selectGroup.tag}', tag: 'SingBox'); KRLogUtil.kr_i(' - 组类型: ${selectGroup.type}', tag: 'SingBox'); KRLogUtil.kr_i(' - 当前选中: ${selectGroup.selected}', tag: 'SingBox'); KRLogUtil.kr_i(' - 目标节点: $targetTag', tag: 'SingBox'); KRLogUtil.kr_i(' - 可用节点数: ${selectGroup.items.length}', tag: 'SingBox'); // 验证目标节点是否在可用列表中 final hasTarget = selectGroup.items.any((item) => item.tag == targetTag); if (!hasTarget) { KRLogUtil.kr_w('⚠️ 目标节点不在 select 组的可用列表中: $targetTag', tag: 'SingBox'); KRLogUtil.kr_w('可用节点列表:', tag: 'SingBox'); for (var item in selectGroup.items) { KRLogUtil.kr_w(' - ${item.tag}', tag: 'SingBox'); } } // 检查是否切换成功 if (selectGroup.selected != targetTag) { KRLogUtil.kr_e('❌ 节点切换验证失败!', tag: 'SingBox'); KRLogUtil.kr_e(' - 期望: $targetTag', tag: 'SingBox'); KRLogUtil.kr_e(' - 实际: ${selectGroup.selected}', tag: 'SingBox'); throw Exception('节点切换失败:实际选中 ${selectGroup.selected},期望 $targetTag'); } KRLogUtil.kr_i('✅ 节点切换验证成功: ${selectGroup.selected} == $targetTag', tag: 'SingBox'); } catch (e) { KRLogUtil.kr_e('❌ 节点验证异常: $e', tag: 'SingBox'); // 不抛出异常,只记录日志,避免阻塞流程 } } /// 带重试机制的节点选择 /// /// 确保 command.sock 准备好后再执行节点选择 Future _kr_selectOutboundWithRetry( String groupTag, String outboundTag, { int maxAttempts = 3, int initialDelay = 100, }) async { int attempt = 0; int delay = initialDelay; while (attempt < maxAttempts) { attempt++; KRLogUtil.kr_i( '🔄 尝试选择节点 $outboundTag (第 $attempt/$maxAttempts 次)', tag: 'SingBox', ); // 只在失败后才延迟,首次尝试立即执行 if (attempt > 1) { await Future.delayed(Duration(milliseconds: delay)); } try { await kr_singBox.selectOutbound(groupTag, outboundTag).run(); KRLogUtil.kr_i('✅ 节点选择成功: $outboundTag', tag: 'SingBox'); return; } catch (e) { KRLogUtil.kr_w( '⚠️ 第 $attempt 次节点选择失败: $e', tag: 'SingBox', ); if (attempt >= maxAttempts) { KRLogUtil.kr_e( '❌ 节点选择失败,已达到最大重试次数', tag: 'SingBox', ); throw Exception('节点选择失败: $e'); } // 指数退避 delay = delay * 2; } } } /// 带重试机制的命令客户端初始化 /// /// command.sock 需要时间创建,因此需要重试机制 /// maxAttempts: 最大重试次数(默认10次,针对macOS优化) /// initialDelay: 初始延迟毫秒数(默认1000ms,针对macOS优化) Future _kr_initializeCommandClientsWithRetry({ int maxAttempts = 10, int initialDelay = 1000, }) async { int attempt = 0; int delay = initialDelay; while (attempt < maxAttempts) { attempt++; KRLogUtil.kr_i( '🔄 尝试初始化命令客户端 (第 $attempt/$maxAttempts 次,延迟 ${delay}ms)', tag: 'SingBox', ); await Future.delayed(Duration(milliseconds: delay)); try { // 先验证 command.sock 是否可访问 final socketFile = File(p.join(kr_configDics.baseDir.path, 'command.sock')); KRLogUtil.kr_i('🔍 检查 socket 文件: ${socketFile.path}', tag: 'SingBox'); if (!socketFile.existsSync()) { throw Exception('command.sock 文件不存在: ${socketFile.path}'); } KRLogUtil.kr_i('✅ socket 文件存在,开始订阅...', tag: 'SingBox'); // 尝试订阅统计和分组数据 // ⚠️ 关键:分别 try-catch,避免一个失败影响另一个 bool statsSubscribed = false; bool groupsSubscribed = false; // ⚠️ 关键修复:使用 Future.delayed(Duration.zero) 将订阅推到下一个事件循环 // 这样可以避免阻塞当前的异步执行 try { KRLogUtil.kr_i('📊 订阅统计数据流...', tag: 'SingBox'); await Future.delayed(Duration.zero); // 让出 UI 线程 _kr_subscribeToStats(); statsSubscribed = true; KRLogUtil.kr_i('✅ 统计数据流订阅成功', tag: 'SingBox'); } catch (e) { KRLogUtil.kr_w('⚠️ 统计数据流订阅失败: $e', tag: 'SingBox'); } try { KRLogUtil.kr_i('📋 订阅分组数据流...', tag: 'SingBox'); await Future.delayed(Duration.zero); // 让出 UI 线程 _kr_subscribeToGroups(); groupsSubscribed = true; KRLogUtil.kr_i('✅ 分组数据流订阅成功', tag: 'SingBox'); } catch (e) { KRLogUtil.kr_w('⚠️ 分组数据流订阅失败: $e', tag: 'SingBox'); } // 至少有一个订阅成功才算初始化成功 if (!statsSubscribed && !groupsSubscribed) { throw Exception('所有订阅都失败了'); } // 等待更长时间验证订阅是否成功,并实际接收到数据 KRLogUtil.kr_i('⏳ 等待800ms验证订阅状态...', tag: 'SingBox'); await Future.delayed(const Duration(milliseconds: 800)); // 验证订阅是否真正工作(检查是否有数据流) if (_kr_subscriptions.isEmpty) { throw Exception('订阅列表为空,command client 未成功连接'); } KRLogUtil.kr_i('✅ 命令客户端初始化成功,活跃订阅数: ${_kr_subscriptions.length}', tag: 'SingBox'); return; } catch (e, stackTrace) { // 详细记录失败原因和堆栈信息 KRLogUtil.kr_w( '⚠️ 第 $attempt 次尝试失败', tag: 'SingBox', ); KRLogUtil.kr_w('❌ 错误类型: ${e.runtimeType}', tag: 'SingBox'); KRLogUtil.kr_w('❌ 错误信息: $e', tag: 'SingBox'); KRLogUtil.kr_w('📚 错误堆栈: $stackTrace', tag: 'SingBox'); if (attempt >= maxAttempts) { KRLogUtil.kr_e( '❌ 命令客户端初始化失败,已达到最大重试次数 ($maxAttempts)', tag: 'SingBox', ); KRLogUtil.kr_e('💡 提示: command.sock 可能未准备好或权限不足', tag: 'SingBox'); throw Exception('命令客户端初始化失败: $e'); } // 指数退避:每次失败后延迟翻倍(1000ms -> 2000ms -> 4000ms -> 8000ms) delay = delay * 2; } } } /// 在后台尝试预连接 command client(不阻塞启动流程) /// /// 借鉴 Hiddify 的设计思想: /// - watchStats/watchGroups 是懒加载的,首次调用时才会 startCommandClient() /// - 但我们可以在后台提前尝试连接,提高成功率 /// - 即使失败也不影响主流程,真正的订阅会在 UI 调用时发生 void _kr_tryPreconnectCommandClientsInBackground() { KRLogUtil.kr_i('🔄 后台启动 command client 预连接任务...', tag: 'SingBox'); // 使用 Future.microtask 确保不阻塞当前执行 Future.microtask(() async { try { // 等待 command.sock 就绪(macOS 需要更长时间) await Future.delayed(const Duration(milliseconds: 1500)); // 检查 socket 文件 final socketFile = File(p.join(kr_configDics.baseDir.path, 'command.sock')); if (!socketFile.existsSync()) { KRLogUtil.kr_w('⚠️ command.sock 尚未创建,预连接取消', tag: 'SingBox'); return; } KRLogUtil.kr_i('✅ command.sock 已就绪,尝试预订阅...', tag: 'SingBox'); // ⚠️ 注意:这里只是"触发"订阅,不等待结果 // 如果失败,UI 调用时会重新尝试 try { _kr_subscribeToStats(); KRLogUtil.kr_i('✅ 统计流预订阅成功', tag: 'SingBox'); } catch (e) { KRLogUtil.kr_w('⚠️ 统计流预订阅失败(正常,UI 调用时会重试): $e', tag: 'SingBox'); } try { _kr_subscribeToGroups(); KRLogUtil.kr_i('✅ 分组流预订阅成功', tag: 'SingBox'); } catch (e) { KRLogUtil.kr_w('⚠️ 分组流预订阅失败(正常,UI 调用时会重试): $e', tag: 'SingBox'); } } catch (e) { // 静默失败,不影响主流程 KRLogUtil.kr_w('⚠️ 后台预连接任务失败(不影响正常使用): $e', tag: 'SingBox'); } }); } /// 确保 command client 已初始化(通过触发订阅来初始化) /// /// 借鉴 Hiddify:watchGroups() 会在首次调用时自动 startCommandClient() Future _kr_ensureCommandClientInitialized() async { // 如果已经有订阅,说明 command client 已初始化 if (_kr_subscriptions.isNotEmpty) { KRLogUtil.kr_i('✅ Command client 已初始化(订阅数: ${_kr_subscriptions.length})', tag: 'SingBox'); return; } KRLogUtil.kr_i('⚠️ Command client 未初始化,触发订阅...', tag: 'SingBox'); try { // 触发 watchGroups(),这会自动调用 startCommandClient() _kr_subscribeToGroups(); // 等待一小段时间让订阅建立 await Future.delayed(const Duration(milliseconds: 300)); if (_kr_subscriptions.isEmpty) { throw Exception('订阅失败,command client 未初始化'); } KRLogUtil.kr_i('✅ Command client 初始化成功', tag: 'SingBox'); } catch (e) { KRLogUtil.kr_e('❌ Command client 初始化失败: $e', tag: 'SingBox'); rethrow; } } /// 在后台恢复用户保存的节点选择(不阻塞启动流程) /// /// selectOutbound() 依赖 command client,必须延迟执行 void _kr_restoreSavedNodeInBackground() { KRLogUtil.kr_i('🔄 启动节点恢复后台任务...', tag: 'SingBox'); Future.microtask(() async { try { // 等待 command client 初始化 await Future.delayed(const Duration(milliseconds: 2000)); final savedNode = await KRSecureStorage().kr_readData(key: _keySelectedNode); if (savedNode != null && savedNode.isNotEmpty && savedNode != 'auto') { KRLogUtil.kr_i('🔄 恢复用户选择的节点: $savedNode', tag: 'SingBox'); try { await _kr_selectOutboundWithRetry("select", savedNode); KRLogUtil.kr_i('✅ 节点恢复完成', tag: 'SingBox'); } catch (e) { KRLogUtil.kr_w('⚠️ 节点恢复失败(可能 command client 未就绪): $e', tag: 'SingBox'); } } else { KRLogUtil.kr_i('ℹ️ 使用默认节点选择 (auto)', tag: 'SingBox'); } } catch (e) { KRLogUtil.kr_w('⚠️ 节点恢复后台任务失败: $e', tag: 'SingBox'); } }); } /// 监听活动组的详细实现 // Future watchActiveGroups() async { // try { // print("开始监听活动组详情..."); // final status = await kr_singBox.status(); // print("服务状态: ${status.toJson()}"); // final outbounds = await kr_singBox.listOutbounds(); // print("出站列表: ${outbounds.toJson()}"); // for (var outbound in outbounds.outbounds) { // print("出站配置: ${outbound.toJson()}"); // // 检查出站是否活动 // final isActive = await kr_singBox.isOutboundActive(outbound.tag); // print("出站 ${outbound.tag} 活动状态: $isActive"); // } // } catch (e, stack) { // print("监听活动组详情时出错: $e"); // print("错误堆栈: $stack"); // } // } /// 保存配置文件 void kr_saveOutbounds(List> outbounds) async { KRLogUtil.kr_i('💾 开始保存配置文件...', tag: 'SingBox'); KRLogUtil.kr_i('📊 出站节点数量: ${outbounds.length}', tag: 'SingBox'); // 打印每个节点的详细配置 for (int i = 0; i < outbounds.length; i++) { final outbound = outbounds[i]; KRLogUtil.kr_i('📋 节点[$i] 配置:', tag: 'SingBox'); 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'); if (outbound['method'] != null) { KRLogUtil.kr_i(' - method: ${outbound['method']}', tag: 'SingBox'); } if (outbound['interval'] != null) { KRLogUtil.kr_i(' - interval: ${outbound['interval']}', tag: 'SingBox'); } if (outbound['password'] != null) { 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(' - 完整配置: ${jsonEncode(outbound)}', tag: 'SingBox'); } // ⚠️ 临时过滤 Hysteria2 节点以避免 libcore 崩溃 kr_outbounds = outbounds.where((outbound) { final type = outbound['type']; if (type == 'hysteria2' || type == 'hysteria') { 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'); // 🔧 修复:生成完整的 SingBox 配置 // 之前只保存 {"outbounds": [...]}, 导致 libcore 无法正确处理 // 现在保存完整配置,包含所有必需字段 final Map fullConfig = { "log": {"level": "debug", "timestamp": true}, "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", "tag": "tun-in", "interface_name": "utun", "inet4_address": "172.19.0.1/30", "auto_route": true, "strict_route": true, "sniff": true, "sniff_override_destination": false } ], "outbounds": [ // 🔧 修复:添加 selector 组,让用户可以手动选择节点 { "type": "selector", "tag": "proxy", "outbounds": kr_outbounds.map((o) => o['tag'] as String).toList(), "default": kr_outbounds.isNotEmpty ? kr_outbounds[0]['tag'] : "direct", }, ...kr_outbounds, {"type": "direct", "tag": "direct"}, {"type": "block", "tag": "block"}, {"type": "dns", "tag": "dns-out"} ], "route": { "rules": _kr_buildRouteRules(), // ✅ 使用动态构建的路由规则 "rule_set": _kr_buildRuleSets(), // ✅ 使用动态构建的规则集 "final": "proxy", // 🔧 修复:使用 selector 组作为默认出站 "auto_detect_interface": true } }; final file = _file(kr_configName); final temp = _tempFile(kr_configName); final mapStr = jsonEncode(fullConfig); 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'); await file.writeAsString(mapStr); await temp.writeAsString(mapStr); _cutPath = file.path; KRLogUtil.kr_i('📁 配置文件路径: $_cutPath', tag: 'SingBox'); await kr_singBox .validateConfigByPath(file.path, temp.path, false) .mapLeft((err) { KRLogUtil.kr_e('❌ 保存配置文件失败: $err', tag: 'SingBox'); }).run(); KRLogUtil.kr_i('✅ 配置文件保存完成', tag: 'SingBox'); } /// 构建 DNS 规则 List> _kr_buildDnsRules() { final currentCountryCode = KRCountryUtil.kr_getCurrentCountryCode(); final rules = >[]; // 🔧 关键修复:只有在"智能代理"模式下,才根据国家添加直连规则 // 如果是"全局代理"模式,即使选择了国家也不添加直连规则 if (kr_connectionType.value == KRConnectionType.rule && currentCountryCode != 'other') { rules.add({ "rule_set": [ "geoip-$currentCountryCode", "geosite-$currentCountryCode", ], "server": "dns-direct" }); KRLogUtil.kr_i('✅ [智能代理模式] 添加 DNS 规则: $currentCountryCode 域名使用直连 DNS', tag: 'SingBox'); } else if (kr_connectionType.value == KRConnectionType.global) { KRLogUtil.kr_i('🌐 [全局代理模式] 跳过国家 DNS 规则,所有DNS查询走代理', tag: 'SingBox'); } return rules; } /// 构建路由规则 List> _kr_buildRouteRules() { final currentCountryCode = KRCountryUtil.kr_getCurrentCountryCode(); final rules = >[]; // 基础规则: DNS 查询走 dns-out rules.add({"protocol": "dns", "outbound": "dns-out"}); // ✅ 自定义域名直连规则(优先级高,放在前面) // rules.add({ // "domain_suffix": ["ip138.com"], // "outbound": "direct" // }); // KRLogUtil.kr_i('✅ 添加自定义域名直连规则: ip138.com -> direct', tag: 'SingBox'); // 🔧 关键修复:只有在"智能代理"模式下,才根据国家添加直连规则 // 如果是"全局代理"模式,即使选择了国家也不添加直连规则 if (kr_connectionType.value == KRConnectionType.rule && currentCountryCode != 'other') { rules.add({ "rule_set": [ "geoip-$currentCountryCode", "geosite-$currentCountryCode", ], "outbound": "direct" }); KRLogUtil.kr_i('✅ [智能代理模式] 添加路由规则: $currentCountryCode IP/域名直连', tag: 'SingBox'); } else if (kr_connectionType.value == KRConnectionType.global) { KRLogUtil.kr_i('🌐 [全局代理模式] 跳过国家路由规则,所有流量走代理', tag: 'SingBox'); } return rules; } /// 构建规则集配置 List> _kr_buildRuleSets() { final currentCountryCode = KRCountryUtil.kr_getCurrentCountryCode(); final ruleSets = >[]; // 🔧 关键修复:只有在"智能代理"模式下,才加载国家规则集 // 如果是"全局代理"模式,即使选择了国家也不加载规则集 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')); if (geoipFile.existsSync() && geositeFile.existsSync()) { // ✅ 使用本地文件 ruleSets.add({ "type": "local", "tag": "geoip-$currentCountryCode", "format": "binary", "path": "./geosite/geoip-$currentCountryCode.srs" // 相对于 workingDir }); ruleSets.add({ "type": "local", "tag": "geosite-$currentCountryCode", "format": "binary", "path": "./geosite/geosite-$currentCountryCode.srs" }); 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'); } else { // ❌ 本地文件不存在,使用远程规则集作为后备 KRLogUtil.kr_w('⚠️ 本地规则集不存在,使用远程规则集', 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", "download_detour": "direct", "update_interval": "7d" }); ruleSets.add({ "type": "remote", "tag": "geosite-$currentCountryCode", "format": "binary", "url": "https://raw.githubusercontent.com/hiddify/hiddify-geo/rule-set/country/geosite-$currentCountryCode.srs", "download_detour": "direct", "update_interval": "7d" }); } } return ruleSets; } Future kr_start() async { // 不再手动设置状态,libcore 会通过 status stream 自动发送状态更新 try { // 🔧 修复3: 添加登录状态检查 - 只有已登录用户才能连接VPN if (!KRAppRunData().kr_isLogin.value) { KRLogUtil.kr_e('❌ 未登录用户,禁止启动VPN连接', tag: 'SingBox'); throw Exception('用户未登录,无法启动VPN服务'); } // ⚠️ 强制编译标记 - v2.0-lazy-load KRLogUtil.kr_i('🚀🚀🚀 [v2.0-lazy-load] 开始启动 SingBox...', tag: 'SingBox'); // 🔧 强制重新生成配置文件(确保最新的路由规则生效) if (kr_outbounds.isNotEmpty) { KRLogUtil.kr_i('🔄 启动前强制重新生成配置文件...', tag: 'SingBox'); 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 && !_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'); } } catch (e) { KRLogUtil.kr_w('⚠️ Windows DNS 备份异常: $e,将在停止时使用兜底恢复', tag: 'SingBox'); } } else if (Platform.isWindows && _dnsBackedUp) { KRLogUtil.kr_i('⏭️ Windows 平台,DNS 已备份,跳过重复备份(节点切换优化)', tag: 'SingBox'); } // 🔑 先尝试停止旧实例,避免 command.sock 冲突 // 注意:如果没有旧实例,libcore 会报错 "command.sock: no such file",这是正常的 try { await kr_singBox.stop().run(); await Future.delayed(const Duration(milliseconds: 500)); KRLogUtil.kr_i('✅ 已清理旧实例', tag: 'SingBox'); } catch (e) { // 预期行为:没有旧实例时会报错,可以忽略 KRLogUtil.kr_i('ℹ️ 没有运行中的旧实例(正常)', tag: 'SingBox'); } // 🔑 关键步骤:在 start 之前必须调用 changeOptions 初始化 HiddifyOptions // 否则 libcore 的 StartService 会因为 HiddifyOptions == nil 而 panic KRLogUtil.kr_i('📡 初始化 HiddifyOptions...', tag: 'SingBox'); final oOption = SingboxConfigOption.fromJson(_getConfigOption()); final changeResult = await kr_singBox.changeOptions(oOption).run(); changeResult.match( (error) { KRLogUtil.kr_e('❌ changeOptions() 失败: $error', tag: 'SingBox'); throw Exception('初始化 HiddifyOptions 失败: $error'); }, (_) { KRLogUtil.kr_i('✅ HiddifyOptions 初始化成功', tag: 'SingBox'); }, ); // 检查配置文件是否存在 final configFile = File(_cutPath); 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'); } else { KRLogUtil.kr_w('⚠️ 配置文件不存在: $_cutPath', tag: 'SingBox'); } // 🔧 修复: 在启动前重新订阅状态流,确保能收到状态更新 _kr_subscribeToStatus(); await kr_singBox.start(_cutPath, kr_configName, false).map( (r) { KRLogUtil.kr_i('✅ SingBox 启动成功', tag: 'SingBox'); }, ).mapLeft((err) { KRLogUtil.kr_e('❌ SingBox 启动失败: $err', tag: 'SingBox'); // 不需要手动设置状态,libcore 会通过 status stream 自动发送 Stopped 事件 throw err; }).run(); // ⚠️ 关键修复:在启动成功后立即订阅统计流 // 原因: // 1. 统计流需要主动订阅才能接收数据 // 2. UI 只是读取 kr_stats.value,不会触发订阅 // 3. command.sock 在 start() 成功后会立即创建 KRLogUtil.kr_i('✅ SingBox 核心已启动,开始初始化 command client', tag: 'SingBox'); // 🔑 在后台延迟订阅统计流和分组流,避免阻塞 UI Future.delayed(const Duration(milliseconds: 1000), () async { try { KRLogUtil.kr_i('📊 开始订阅统计数据流...', tag: 'SingBox'); _kr_subscribeToStats(); KRLogUtil.kr_i('✅ 统计数据流订阅成功', tag: 'SingBox'); } catch (e) { KRLogUtil.kr_w('⚠️ 统计数据流订阅失败(稍后重试): $e', tag: 'SingBox'); // 如果第一次失败,再等待一段时间重试 Future.delayed(const Duration(milliseconds: 2000), () { try { _kr_subscribeToStats(); KRLogUtil.kr_i('✅ 统计数据流重试订阅成功', tag: 'SingBox'); } catch (e2) { KRLogUtil.kr_e('❌ 统计数据流重试订阅失败: $e2', tag: 'SingBox'); } }); } // 🔧 关键修复:订阅分组数据流 try { KRLogUtil.kr_i('📋 开始订阅分组数据流...', tag: 'SingBox'); _kr_subscribeToGroups(); KRLogUtil.kr_i('✅ 分组数据流订阅成功', tag: 'SingBox'); } catch (e) { KRLogUtil.kr_w('⚠️ 分组数据流订阅失败(稍后重试): $e', tag: 'SingBox'); // 如果第一次失败,再等待一段时间重试 Future.delayed(const Duration(milliseconds: 2000), () { try { _kr_subscribeToGroups(); KRLogUtil.kr_i('✅ 分组数据流重试订阅成功', tag: 'SingBox'); } catch (e2) { KRLogUtil.kr_e('❌ 分组数据流重试订阅失败: $e2', tag: 'SingBox'); } }); } // 🔧 关键修复:恢复用户选择的节点 try { final selectedNode = await KRSecureStorage().kr_readData(key: _keySelectedNode); if (selectedNode != null && selectedNode.isNotEmpty) { KRLogUtil.kr_i('🎯 恢复用户选择的节点: $selectedNode', tag: 'SingBox'); if (kDebugMode) { print('🔵 启动后恢复节点选择: $selectedNode'); } // 延迟500ms确保sing-box完全启动 await Future.delayed(const Duration(milliseconds: 500)); // 🔧 关键修复:使用 await 等待节点切换完成 try { await kr_selectOutbound(selectedNode); KRLogUtil.kr_i('✅ 节点已切换到用户选择: $selectedNode', tag: 'SingBox'); if (kDebugMode) { print('🔵 节点切换成功: $selectedNode'); } } catch (e) { KRLogUtil.kr_e('❌ 节点切换失败: $e', tag: 'SingBox'); if (kDebugMode) { print('🔵 节点切换失败: $e'); } } } else { KRLogUtil.kr_i('ℹ️ 没有保存的节点选择,使用默认配置', tag: 'SingBox'); if (kDebugMode) { print('🔵 没有保存的节点选择,使用默认'); } } } catch (e) { KRLogUtil.kr_e('❌ 恢复节点选择失败: $e', tag: 'SingBox'); if (kDebugMode) { print('🔵 恢复节点选择失败: $e'); } } }); } catch (e, stackTrace) { KRLogUtil.kr_e('💥 SingBox 启动异常: $e', tag: 'SingBox'); KRLogUtil.kr_e('📚 错误堆栈: $stackTrace', tag: 'SingBox'); // 不需要手动设置状态,libcore 会自动处理 rethrow; } } /// 停止服务 Future kr_stop() async { try { KRLogUtil.kr_i('🛑 停止 SingBox 服务...', tag: 'SingBox'); // 取消节点选择监控定时器 _nodeSelectionTimer?.cancel(); _nodeSelectionTimer = null; KRLogUtil.kr_i('✅ 节点选择监控已停止', tag: 'SingBox'); await Future.delayed(const Duration(milliseconds: 100)); // 添加超时保护,防止 stop() 调用阻塞 // 🔧 延长超时时间到 10 秒,给 Windows DNS 清理足够时间 try { await kr_singBox.stop().run().timeout( const Duration(seconds: 10), // ← 从 3 秒延长到 10 秒 onTimeout: () { KRLogUtil.kr_w('⚠️ 停止操作超时(10秒),强制继续', tag: 'SingBox'); return const Left('timeout'); }, ); } catch (e) { KRLogUtil.kr_w('⚠️ 停止操作失败(可能已经停止): $e', tag: 'SingBox'); // 继续执行清理操作 } // 🔑 Windows 平台:仅在有备份时恢复 DNS 设置 if (Platform.isWindows && _dnsBackedUp) { KRLogUtil.kr_i('🪟 Windows 平台,等待 sing-box 完全停止...', tag: 'SingBox'); // 🔧 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 final restoreSuccess = await KRWindowsDnsUtil.instance.kr_restoreDnsSettings(); if (restoreSuccess) { KRLogUtil.kr_i('✅ Windows DNS 恢复成功', tag: 'SingBox'); } else { KRLogUtil.kr_e('❌ Windows DNS 恢复失败', tag: 'SingBox'); } } 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)); } // 取消统计和分组订阅,但保留状态订阅以便继续接收状态更新 final subscriptionsToCancel = _kr_subscriptions.where((sub) { final hashStr = sub.hashCode.toString(); return !hashStr.contains('Status'); // 不取消状态订阅 }).toList(); for (var subscription in subscriptionsToCancel) { try { await subscription.cancel(); _kr_subscriptions.remove(subscription); } catch (e) { KRLogUtil.kr_e('取消订阅时出错: $e'); } } // 不手动设置状态,由 libcore 通过 status stream 自动发送 Stopped 事件 KRLogUtil.kr_i('✅ SingBox 停止请求已发送', tag: 'SingBox'); } catch (e, stackTrace) { KRLogUtil.kr_e('停止服务时出错: $e'); KRLogUtil.kr_e('错误堆栈: $stackTrace'); // 🔑 即使出错,也要尝试恢复 Windows DNS 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'); } } // 不手动设置状态,信任 libcore 的状态管理 rethrow; } } /// void kr_updateAdBlockEnabled(bool bl) async { final oOption = _getConfigOption(); oOption["block-ads"] = bl; final op = SingboxConfigOption.fromJson(oOption); await kr_singBox.changeOptions(op) ..map((r) {}).mapLeft((err) { KRLogUtil.kr_e('更新广告拦截失败: $err'); }).run(); if (kr_status.value == SingboxStarted()) { await kr_restart(); } kr_blockAds.value = bl; } Future kr_restart() async { KRLogUtil.kr_i('🔄 重启 SingBox...', tag: 'SingBox'); // 🔧 修复:使用 stop + start 而不是 restart // 这样可以触发 Windows DNS 恢复和备份逻辑 try { // 1. 先停止(会触发 DNS 恢复) await kr_stop(); // 2. 等待完全停止 await Future.delayed(const Duration(milliseconds: 500)); // 3. 重新启动(会触发 DNS 备份) await kr_start(); KRLogUtil.kr_i('✅ SingBox 重启完成', tag: 'SingBox'); } catch (e) { KRLogUtil.kr_e('❌ 重启失败: $e', tag: 'SingBox'); rethrow; } } //// 设置出站模式 Future kr_updateConnectionType(KRConnectionType newType) async { try { KRLogUtil.kr_i('🔄 开始更新连接类型...', tag: 'SingBox'); KRLogUtil.kr_i('📊 当前类型: ${kr_connectionType.value}', tag: 'SingBox'); KRLogUtil.kr_i('📊 新类型: $newType', tag: 'SingBox'); if (kr_connectionType.value == newType) { KRLogUtil.kr_i('⚠️ 连接类型相同,无需更新', tag: 'SingBox'); return; } kr_connectionType.value = newType; final oOption = _getConfigOption(); var mode = ""; switch (newType) { case KRConnectionType.global: mode = "other"; KRLogUtil.kr_i('🌍 切换到全局代理模式', tag: 'SingBox'); break; case KRConnectionType.rule: mode = KRCountryUtil.kr_getCurrentCountryCode(); KRLogUtil.kr_i('🎯 切换到规则代理模式: $mode', tag: 'SingBox'); break; // case KRConnectionType.direct: // mode = "direct"; // break; } oOption["region"] = mode; KRLogUtil.kr_i('📝 更新 region 配置: $mode', tag: 'SingBox'); final op = SingboxConfigOption.fromJson(oOption); KRLogUtil.kr_i('📄 配置选项: ${oOption.toString()}', tag: 'SingBox'); await kr_singBox.changeOptions(op) ..map((r) { KRLogUtil.kr_i('✅ 连接类型更新成功', tag: 'SingBox'); }).mapLeft((err) { KRLogUtil.kr_e('❌ 更新连接类型失败: $err', tag: 'SingBox'); throw err; }).run(); if (kr_status.value == SingboxStarted()) { KRLogUtil.kr_i('🔄 VPN已启动,准备重启以应用新配置...', tag: 'SingBox'); await kr_restart(); KRLogUtil.kr_i('✅ VPN重启完成', tag: 'SingBox'); } else { KRLogUtil.kr_i('ℹ️ VPN未启动,配置已更新', tag: 'SingBox'); } } catch (e, stackTrace) { KRLogUtil.kr_e('💥 更新连接类型异常: $e', tag: 'SingBox'); KRLogUtil.kr_e('📚 错误堆栈: $stackTrace', tag: 'SingBox'); rethrow; } } /// 更新国家设置 Future kr_updateCountry(KRCountry kr_country) async { // 如果国家相同,直接返回 if (kr_country.kr_code == KRCountryUtil.kr_getCurrentCountryCode()) { return; } try { // 更新工具类中的当前国家 await KRCountryUtil.kr_setCurrentCountry(kr_country); // 更新配置选项 final oOption = _getConfigOption(); oOption["region"] = kr_country.kr_code; final op = SingboxConfigOption.fromJson(oOption); await kr_singBox.changeOptions(op) ..map((r) {}).mapLeft((err) { KRLogUtil.kr_e('更新国家设置失败: $err'); }).run(); // 如果服务正在运行,重启服务 if (kr_status.value == SingboxStarted()) { await kr_restart(); } } catch (err) { KRLogUtil.kr_e('更新国家失败: $err'); rethrow; } } Stream kr_watchStatus() { return kr_singBox.watchStatus(); } Stream> kr_watchGroups() { return kr_singBox.watchGroups(); } // 节点选择监控定时器 Timer? _nodeSelectionTimer; Future kr_selectOutbound(String tag) async { 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; for (var group in kr_activeGroups) { for (var item in group.items) { if (item.tag == tag) { foundNode = true; KRLogUtil.kr_i('✅ 在组 "${group.tag}" 中找到目标节点: $tag', tag: 'SingBox'); break; } } if (foundNode) break; } if (!foundNode) { KRLogUtil.kr_w('⚠️ 未能在任何活动组中找到目标节点: $tag', tag: 'SingBox'); // 打印所有可用的节点 for (var group in kr_activeGroups) { for (var item in group.items) { KRLogUtil.kr_d(' 可用节点: ${item.tag}', tag: 'SingBox'); } } } // 🔧 关键修复:使用 await 确保保存完成 try { await KRSecureStorage().kr_saveData(key: _keySelectedNode, value: tag); KRLogUtil.kr_i('✅ 节点选择已保存: $tag', tag: 'SingBox'); } catch (e) { KRLogUtil.kr_e('❌ 保存节点选择失败: $e', tag: 'SingBox'); } // 🔧 关键修复:使用 await 确保 command client 初始化完成 try { await _kr_ensureCommandClientInitialized(); KRLogUtil.kr_i('✅ Command client 已就绪,执行节点切换', tag: 'SingBox'); // 🔧 关键修复:使用正确的 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); KRLogUtil.kr_i('✅ 节点切换API调用完成: $tag', tag: 'SingBox'); // 🔧 新增:验证节点切换是否生效 await Future.delayed(const Duration(milliseconds: 300)); // 等待活动组更新 await _kr_verifyNodeSelection(tag); KRLogUtil.kr_i('✅ 节点切换验证完成: $tag', tag: 'SingBox'); } catch (e) { KRLogUtil.kr_e('❌ 节点选择失败: $e', tag: 'SingBox'); rethrow; // 抛出异常,让调用者知道失败了 } // 🔄 如果用户选择了具体节点(不是 auto),启动定期检查和重新选择 // 这是为了防止 urltest 自动覆盖用户的手动选择 _nodeSelectionTimer?.cancel(); if (tag != 'auto') { KRLogUtil.kr_i('🔁 启动节点选择监控,防止被 auto 覆盖', tag: 'SingBox'); _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'), ); }).catchError((error) { KRLogUtil.kr_w('🔁 定时器重选节点异常: $error', tag: 'SingBox'); }); KRLogUtil.kr_d('🔁 重新确认节点选择: $tag', tag: 'SingBox'); }); } } /// 配合文件地址 Directory get directory => Directory(p.join(kr_configDics.workingDir.path, "configs")); File _file(String fileName) { return File(p.join(directory.path, "$fileName.json")); } File _tempFile(String fileName) => _file("$fileName.tmp"); // File tempFile(String fileName) => file("$fileName.tmp"); Future kr_urlTest(String groupTag) async { KRLogUtil.kr_i('🧪 开始 URL 测试: $groupTag', tag: 'SingBox'); KRLogUtil.kr_i('📊 当前活动组数量: ${kr_activeGroups.length}', 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'); 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'); } } try { KRLogUtil.kr_i('🚀 调用 SingBox URL 测试 API...', tag: 'SingBox'); final result = await kr_singBox.urlTest(groupTag).run(); KRLogUtil.kr_i('✅ URL 测试完成: $groupTag, 结果: $result', tag: 'SingBox'); // 等待一段时间让 SingBox 完成测试 await Future.delayed(const Duration(seconds: 2)); // 再次检查活动组状态 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'); 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'); } } } catch (e) { KRLogUtil.kr_e('❌ URL 测试失败: $groupTag, 错误: $e', tag: 'SingBox'); KRLogUtil.kr_e('📚 错误详情: ${e.toString()}', tag: 'SingBox'); } } }