import 'dart:convert'; import 'dart:io'; import 'dart:async'; import 'dart:ffi'; // ✅ dart:isolate 已移除(原用于 DNS 恢复的 Isolate,现由 libcore 管理) import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:fpdart/fpdart.dart'; import 'package:get/get.dart'; import 'package:synchronized/synchronized.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_file_logger.dart'; import '../../utils/kr_secure_storage.dart'; // ✅ DNS 由 libcore.dll 自动管理,已移除 kr_windows_dns_util.dart import '../../utils/kr_windows_process_util.dart'; import '../../common/app_run_data.dart'; import 'package:flutter/foundation.dart'; enum KRConnectionType { global, rule, // direct, } // 🔧 P0-1: 订阅类型标识 - 用于精确管理订阅 class _SubscriptionType { static const String groups = 'groups'; static const String stats = 'stats'; static const String status = 'status'; } // 🔧 _WindowsProxyRestoreWorker 类已删除 - 改用 WinINet API 直接在主线程执行(无需 Isolate) // 优势: // ✅ 毫秒级速度(无进程启动开销) // ✅ 无黑窗问题 // ✅ 代码更简洁 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; // Windows 下 old command server 使用固定 TCP 端口(libbox hard-coded: 127.0.0.1:8964) static const int _krWindowsCommandServerPort = 8964; /// 统计 final kr_stats = SingboxStats( connectionsIn: 0, connectionsOut: 0, uplink: 0, downlink: 0, uplinkTotal: 0, downlinkTotal: 0, ).obs; /// 活动的出站分组 RxList kr_activeGroups = [].obs; // ✅ Windows 管理员权限检查缓存(避免重复执行系统命令) bool? _cachedAdminPrivilege; DateTime? _cachedAdminPrivilegeTime; static const Duration _adminPrivilegeCacheDuration = Duration(hours: 1); // ✅ DNS 和系统代理都由 Go 后端 (libcore.dll) 自动管理 // 参考 Hiddify 实现:Dart 层不再手动处理 DNS 和代理设置 // sing-box 内部有完整的 DNS 代理机制(local-dns-port、remote-dns-address 等) /// 所有的出站分组 RxList kr_allGroups = [].obs; /// Stream 订阅管理器 final List> _kr_subscriptions = []; // 🔧 P0-1 + P2-11: 改进订阅管理 - 使用 Map 而非 List,防止重复订阅 // P2-11: hashCode 去重不可靠 - 改为使用类型安全的 Map,避免基于 hashCode.toString().contains() 的脆弱判断 /// 精确的订阅管理(Map) final Map> _subscriptionMap = {}; // ✅ 系统代理操作已移交给 Go 后端 (libcore.dll) 处理 // 不再需要 Dart 层的代理操作互斥锁 /// 初始化标志,防止重复初始化 bool _kr_isInitialized = false; // 🔧 P1-3: 命令客户端初始化锁,防止并发调用导致重复初始化 final Lock _commandClientInitLock = Lock(); // 🔧 防止 kr_start() 和 kr_stop() 并发执行导致卡顿 // 快速点击VPN开关时,通过此 Lock 确保上一个操作完成后才能开始新操作 final Lock _startStopLock = Lock(); // 【关键】停止请求标志 // 用于通知后台任务停止执行,避免竞态条件 // 当 kr_stop() 开始时设置为 true,让正在执行或即将执行的后台任务立即返回 bool _stopRequested = false; // ✅ DNS 相关的方法已移除 // 参考 Hiddify 实现:sing-box 内部有完整的 DNS 代理机制 // 通过 local-dns-port、remote-dns-address、direct-dns-address 等配置处理 DNS /// 当前混合代理端口是否就绪 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 = false}) { 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; } 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 发送消息 await KRFileLogger.log('[黑屏调试] ⏳ 准备调用 FFI setup()...'); KRLogUtil.kr_i('📡 开始调用 setup() 注册 FFI 端口', tag: 'SingBox'); final setupStartTime = DateTime.now(); final setupResult = await kr_singBox.setup(kr_configDics, false).run(); final setupDuration = DateTime.now().difference(setupStartTime).inMilliseconds; await KRFileLogger.log('[黑屏调试] ✅ FFI setup() 完成: 耗时 ${setupDuration}ms'); setupResult.match( (error) async { await KRFileLogger.log('[黑屏调试] ❌ FFI setup() 失败: $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(); // ✅ DNS 由 libcore.dll 自动管理,无需手动备份 KRLogUtil.kr_i('✅ 状态订阅已设置', tag: 'SingBox'); } 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; 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 useWindowsDnsDefaults = Platform.isWindows; final remoteDnsAddress = useWindowsDnsDefaults ? 'udp://1.1.1.1' : 'https://dns.google/dns-query'; final directDnsAddress = useWindowsDnsDefaults ? (effectiveRegion == 'cn' ? 'udp://223.5.5.5' : 'udp://1.1.1.1') : 'local'; final dnsDomainStrategy = useWindowsDnsDefaults ? '' : 'prefer_ipv4'; const enableDnsRouting = true; final op = { "region": effectiveRegion, // 🔧 修复:根据出站模式动态设置 region "block-ads": false, // 参考 hiddify-app: 默认关闭广告拦截 "use-xray-core-when-possible": false, "execute-config-as-is": false, // ✅ 按 hiddify-app 方式交给 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": remoteDnsAddress, // 使用 Google DoH,避免中转节点 DNS 死锁 "remote-dns-domain-strategy": dnsDomainStrategy, "direct-dns-address": directDnsAddress, // 使用系统 DNS,确保中转服务器域名能被解析 "direct-dns-domain-strategy": dnsDomainStrategy, "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, // 🔧 关键改动:根据连接类型选择使用 TUN 还是系统代理 // 全局代理(global) → 启用 TUN 模式(强制代理,Chrome 插件无法劫持) // 智能代理(rule) → 使用系统代理(智能分流) // ✅ 参考 Hiddify:Windows/macOS/Linux 全局模式都启用 TUN "enable-tun": (Platform.isIOS || Platform.isAndroid) || ((Platform.isWindows || Platform.isMacOS || Platform.isLinux) && kr_connectionType.value == KRConnectionType.global), "enable-tun-service": false, "set-system-proxy": (Platform.isWindows || Platform.isLinux || Platform.isMacOS) && kr_connectionType.value != KRConnectionType.global, "bypass-lan": false, "allow-connection-from-lan": false, "enable-fake-dns": false, "enable-dns-routing": enableDnsRouting, "independent-dns-cache": true, "rules": [ // ✅ 自定义域名直连规则 - 添加到 HiddifyOptions.Rules 中 // 🔧 修复: 空规则列表,避免 "missing conditions" 错误 // 如果需要添加规则,必须确保所有必需字段都存在 ], "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) { KRFileLogger.log('[黑屏调试] 🔵 _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) { KRFileLogger.log('[黑屏调试] 🔵 已取消旧的状态订阅'); } } } _kr_subscriptions .removeWhere((sub) => sub.hashCode.toString().contains('Status')); _kr_subscriptions.add( kr_singBox.watchStatus().listen( (status) { if (kDebugMode) { KRFileLogger.log('[黑屏调试] 🔵 收到 Native 状态更新: ${status.runtimeType}'); } KRLogUtil.kr_i('📡 收到状态更新: $status', tag: 'SingBox'); kr_status.value = status; // ✅ 系统代理由 Go 后端 (libcore.dll) 自动管理 // libcore 在 stop() 时会自动恢复系统代理,无需在 Dart 层处理 }, onError: (error) { if (kDebugMode) { KRFileLogger.log('[黑屏调试] 🔵 状态流错误: $error'); } KRLogUtil.kr_e('📡 状态流错误: $error', tag: 'SingBox'); }, cancelOnError: false, ), ); if (kDebugMode) { KRFileLogger.log('[黑屏调试] 🔵 状态流订阅完成'); } } /// 订阅统计数据流 /// 🔧 UI阻塞修复:改为异步方法,在FFI调用前让出控制权 Future _kr_subscribeToStats() async { // 🔧 P0-1: 先取消旧的 stats 订阅 _subscriptionMap[_SubscriptionType.stats]?.cancel(); // 移除列表中的旧订阅(向后兼容) for (var sub in _kr_subscriptions) { if (sub.hashCode.toString().contains('Stats')) { sub.cancel(); } } _kr_subscriptions .removeWhere((sub) => sub.hashCode.toString().contains('Stats')); // 🔧 关键修复:在调用同步FFI之前让出控制权,给UI一帧渲染时间 await Future.delayed(Duration.zero); // ⚠️ 关键: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, ); // 🔧 P0-1: 使用 Map 精确管理订阅 _subscriptionMap[_SubscriptionType.stats] = subscription; _kr_subscriptions.add(subscription); KRLogUtil.kr_i('✅ Stats 订阅已建立', tag: 'SingBox'); } /// 订阅分组数据流 /// 🔧 UI阻塞修复:改为异步方法,在FFI调用前让出控制权 Future _kr_subscribeToGroups() async { KRFileLogger.log('[黑屏调试] [_kr_subscribeToGroups] 🚀 开始订阅分组数据流'); // 🔧 P0-1: 先取消旧的 groups 订阅 _subscriptionMap[_SubscriptionType.groups]?.cancel(); // 移除列表中的旧订阅(向后兼容) for (var sub in _kr_subscriptions) { if (sub.hashCode.toString().contains('Groups')) { sub.cancel(); } } _kr_subscriptions .removeWhere((sub) => sub.hashCode.toString().contains('Groups')); // 🔧 关键修复:在调用同步FFI之前让出控制权,给UI一帧渲染时间 await Future.delayed(Duration.zero); // 订阅活动分组 final activeGroupsSubscription = kr_singBox.watchActiveGroups().listen( (groups) { KRFileLogger.log('[黑屏调试] [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) { KRFileLogger.log('[黑屏调试] [watchActiveGroups] ❌ 活动分组监听错误: $error'); KRLogUtil.kr_e('❌ 活动分组监听错误: $error', tag: 'SingBox'); }, cancelOnError: false, ); _kr_subscriptions.add(activeGroupsSubscription); // 🔧 关键修复:在两个FFI调用之间让出控制权,避免连续阻塞UI await Future.delayed(Duration.zero); // 订阅所有分组 final allGroupsSubscription = kr_singBox.watchGroups().listen( (groups) { KRFileLogger.log('[黑屏调试] [watchGroups] 📡 收到所有组更新,数量: ${groups.length}'); kr_allGroups.value = groups; // 打印每个组的基本信息 for (int i = 0; i < groups.length; i++) { final group = groups[i]; KRFileLogger.log('[黑屏调试] [watchGroups] 组[$i]: tag=${group.tag}, type=${group.type}, selected=${group.selected}'); } }, onError: (error) { KRFileLogger.log('[黑屏调试] [watchGroups] ❌ 所有分组监听错误: $error'); KRLogUtil.kr_e('所有分组监听错误: $error'); }, cancelOnError: false, ); _kr_subscriptions.add(allGroupsSubscription); // 🔧 P0-1: 使用 Map 精确管理订阅(将两个都归为 'groups' 类型,因为它们共享生命周期) _subscriptionMap[_SubscriptionType.groups] = activeGroupsSubscription; KRFileLogger.log('[黑屏调试] [_kr_subscribeToGroups] ✅ 分组数据流订阅完成,当前订阅数: ${_kr_subscriptions.length}'); KRLogUtil.kr_i('✅ Groups 订阅已建立', 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; // 🔧 UI阻塞修复:订阅方法现在是异步的,会在FFI调用前让出控制权 try { KRLogUtil.kr_i('📊 订阅统计数据流...', tag: 'SingBox'); await _kr_subscribeToStats(); // 🔧 修复:使用await调用异步方法 statsSubscribed = true; KRLogUtil.kr_i('✅ 统计数据流订阅成功', tag: 'SingBox'); } catch (e) { KRLogUtil.kr_w('⚠️ 统计数据流订阅失败: $e', tag: 'SingBox'); } try { KRLogUtil.kr_i('📋 订阅分组数据流...', tag: 'SingBox'); await _kr_subscribeToGroups(); // 🔧 修复:使用await调用异步方法 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阻塞修复:订阅方法现在是异步的,会在FFI调用前让出控制权 // 如果失败,UI 调用时会重新尝试 try { await _kr_subscribeToStats(); // 🔧 修复:使用await KRLogUtil.kr_i('✅ 统计流预订阅成功', tag: 'SingBox'); } catch (e) { KRLogUtil.kr_w('⚠️ 统计流预订阅失败(正常,UI 调用时会重试): $e', tag: 'SingBox'); } try { await _kr_subscribeToGroups(); // 🔧 修复:使用await 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 { // 🔧 P0-1 + P2新: 改进订阅检查 - 检查是否真正有 groups 和 stats 订阅 // 而不是仅检查列表是否非空(Status 订阅也会在列表中,容易误判) final hasGroupsSubscription = _subscriptionMap[_SubscriptionType.groups] != null; final hasStatsSubscription = _subscriptionMap[_SubscriptionType.stats] != null; if (hasGroupsSubscription && hasStatsSubscription) { KRLogUtil.kr_i('✅ Command client 已初始化(groups: ✓, stats: ✓)', tag: 'SingBox'); return; } KRLogUtil.kr_i('⚠️ Command client 未初始化或订阅丢失,重新初始化...', tag: 'SingBox'); // 🔧 P1-3: 使用 Mutex 保护初始化过程,防止并发调用导致重复初始化(内存泄漏) await _commandClientInitLock.synchronized(() async { try { // 再次检查(double-check locking 模式,避免在等待锁期间被其他线程初始化) final hasGroupsSubscription = _subscriptionMap[_SubscriptionType.groups] != null; final hasStatsSubscription = _subscriptionMap[_SubscriptionType.stats] != null; if (hasGroupsSubscription && hasStatsSubscription) { KRLogUtil.kr_i('✅ Command client 在等待期间已被初始化,跳过重复初始化', tag: 'SingBox'); return; } // 🔧 UI阻塞修复:订阅方法现在是异步的,会在FFI调用前让出控制权 await _kr_subscribeToGroups(); // 🔧 修复:使用await await _kr_subscribeToStats(); // 🔧 修复:使用await // 等待订阅建立完成 await Future.delayed(const Duration(milliseconds: 100)); // 验证订阅是否真的建立了 if (_subscriptionMap[_SubscriptionType.groups] == null || _subscriptionMap[_SubscriptionType.stats] == null) { 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['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'); // Use hiddify-app flow: save only base outbounds. // libcore builds full config from HiddifyOptions. final Map baseConfig = { "outbounds": [ if (kr_outbounds.isNotEmpty) { "type": "selector", "tag": "proxy", "outbounds": kr_outbounds.map((o) => o['tag'] as String).toList(), "default": kr_outbounds[0]['tag'], }, ...kr_outbounds, ], }; final file = _file(kr_configName); final temp = _tempFile(kr_configName); final mapStr = jsonEncode(baseConfig); KRLogUtil.kr_i('Config length: ${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'); // Align with hiddify-app: send HiddifyOptions before validate. final oOption = SingboxConfigOption.fromJson(_getConfigOption()); await kr_singBox.changeOptions(oOption).mapLeft((err) { KRLogUtil.kr_e('Config changeOptions failed before validate: $err', tag: 'SingBox'); }).run(); 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" }); // 🔧 关键修复:只有在"智能代理"模式下,才根据国家添加直连规则 // 如果是"全局代理"模式,即使选择了国家也不添加直连规则 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 { // 🔧 文件日志:记录 kr_start() 调用 + 时间戳 final clickTime = DateTime.now(); await KRFileLogger.log('[黑屏调试] [DEBUG-START] kr_start() 方法被调用 - $clickTime'); await KRFileLogger.log('[kr_start] ⏱️ 用户点击启动 VPN - $clickTime'); // 🔧 P0-3: 防止重复启动,避免备份被覆盖 if (kr_status.value is SingboxStarted) { KRLogUtil.kr_w('⚠️ VPN 已在运行,请勿重复启动', tag: 'SingBox'); await KRFileLogger.log('[kr_start] VPN 已在运行,返回'); await KRFileLogger.log('[黑屏调试] [DEBUG-START] VPN 已在运行,直接返回'); return; } // 🔧 文件日志:等待 Lock + 时间戳 final beforeLock = DateTime.now(); await KRFileLogger.log('[黑屏调试] [DEBUG-START] 准备等待 Lock - $beforeLock'); await KRFileLogger.log('[kr_start] ⏱️ 开始等待 Lock... - $beforeLock'); await KRFileLogger.log('[黑屏调试] [DEBUG-START] 已打印等待 Lock 日志 - ${DateTime.now()}'); // 🔧 VPN 开关防卡顿:使用 Mutex 确保 kr_start() 和 kr_stop() 不会并发执行 // 快速连续点击 VPN 开关时,此 Lock 会序列化所有操作,防止系统过载 return _startStopLock.synchronized(() async { final afterLock = DateTime.now(); final lockWaitMs = afterLock.difference(beforeLock).inMilliseconds; // 🔍 诊断:Lock 等待时间检查 if (lockWaitMs > 1000) { await KRFileLogger.log('[kr_start] 🚨 【关键诊断】Lock 等待超过 1000ms(等待 ${lockWaitMs}ms)!'); await KRFileLogger.log('[kr_start] 🚨 这说明前一个操作(kr_stop)还没释放 Lock,可能被后台任务阻塞!'); await KRFileLogger.log('[kr_start] 🚨 可能原因:后台 DNS/代理恢复或者后台订阅任务被阻塞'); } else { await KRFileLogger.log('[kr_start] ✅ Lock 已获取(等待 ${lockWaitMs}ms,正常)'); } await KRFileLogger.log('[kr_start] ✅ Lock 已获取(等待 ${lockWaitMs}ms)- $afterLock'); try { final beforeInternal = DateTime.now(); await _kr_startInternal(); final afterInternal = DateTime.now(); final internalMs = afterInternal.difference(beforeInternal).inMilliseconds; await KRFileLogger.log('[kr_start] ✅ _kr_startInternal() 完成(耗时 ${internalMs}ms)- $afterInternal'); } catch (e) { await KRFileLogger.log('[kr_start] ❌ _kr_startInternal() 异常: $e - ${DateTime.now()}'); rethrow; } }); } /// 实际的启动逻辑(被 Lock 保护) Future _kr_startInternal() async { // ✅ DNS 由 libcore.dll 自动管理 // 再次检查状态(Lock 内部二次检查) if (kr_status.value is SingboxStarted) { KRLogUtil.kr_i('✅ VPN 已在运行(Lock 内检查)', tag: 'SingBox'); return; } // 不再手动设置状态,libcore 会通过 status stream 自动发送状态更新 try { // 🔧 修复3: 添加登录状态检查 - 只有已登录用户才能连接VPN if (!KRAppRunData().kr_isLogin.value) { KRLogUtil.kr_e('❌ 未登录用户,禁止启动VPN连接', tag: 'SingBox'); throw Exception('用户未登录,无法启动VPN服务'); } // ✅ 改进:应用启动时已由 manifest 要求管理员权限(UAC 提升) // 用户同意 UAC 提示后,应用以管理员身份运行,可以直接使用 TUN 模式 // 无需在运行时再做权限检查 if (Platform.isWindows && kr_connectionType.value == KRConnectionType.global) { KRLogUtil.kr_i('✅ 应用已以管理员身份运行,可以使用全局代理模式(TUN)', tag: 'SingBox'); } // 🪟 Windows 预检查:避免 command server 固定端口 8964 被占用导致启动失败 // ⚠️ 强制编译标记 - 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'); // ✅ DNS 由 sing-box 内部代理机制处理,不需要手动备份/恢复 // 参考 Hiddify 实现:sing-box 有 local-dns-port、remote-dns-address 等 DNS 配置 // 🔑 先尝试停止旧实例,避免 command.sock 冲突 // 注意:如果没有旧实例,libcore 会报错 "command.sock: no such file",这是正常的 try { await KRFileLogger.log('[_kr_startInternal] 开始停止旧实例 - ${DateTime.now()}'); await kr_singBox.stop().run(); await Future.delayed(const Duration(milliseconds: 500)); KRLogUtil.kr_i('✅ 已清理旧实例', tag: 'SingBox'); await KRFileLogger.log('[_kr_startInternal] 旧实例已停止 - ${DateTime.now()}'); } catch (e) { // 预期行为:没有旧实例时会报错,可以忽略 KRLogUtil.kr_i('ℹ️ 没有运行中的旧实例(正常)', tag: 'SingBox'); await KRFileLogger.log('[_kr_startInternal] 旧实例不存在(正常)- ${DateTime.now()}'); } // 🔑 关键步骤:在 start 之前必须调用 changeOptions 初始化 HiddifyOptions // 否则 libcore 的 StartService 会因为 HiddifyOptions == nil 而 panic await KRFileLogger.log('[黑屏调试] [${DateTime.now()}] ▶️ 步骤1: 准备 changeOptions...'); KRLogUtil.kr_i('📡 初始化 HiddifyOptions...', tag: 'SingBox'); await KRFileLogger.log('[_kr_startInternal] 开始 changeOptions - ${DateTime.now()}'); await _kr_preflightWindowsCommandServerPort(); final configMap = _getConfigOption(); final oOption = SingboxConfigOption.fromJson(configMap); // ✅ 调试日志:确认 setSystemProxy 值正确传递给 libcore.dll await KRFileLogger.log('[黑屏调试] [${DateTime.now()}] 📦 set-system-proxy: ${configMap["set-system-proxy"]}'); await KRFileLogger.log('[黑屏调试] [${DateTime.now()}] 📦 enable-tun: ${configMap["enable-tun"]}'); KRLogUtil.kr_i('📡 HiddifyOptions 关键配置:', tag: 'SingBox'); KRLogUtil.kr_i(' - set-system-proxy: ${configMap["set-system-proxy"]}', tag: 'SingBox'); KRLogUtil.kr_i(' - enable-tun: ${configMap["enable-tun"]}', tag: 'SingBox'); KRLogUtil.kr_i(' - mixed-port: ${configMap["mixed-port"]}', tag: 'SingBox'); await KRFileLogger.log('[_kr_startInternal] HiddifyOptions: set-system-proxy=${configMap["set-system-proxy"]}, enable-tun=${configMap["enable-tun"]}, mixed-port=${configMap["mixed-port"]}'); await KRFileLogger.log('[黑屏调试] [${DateTime.now()}] ⏳ 步骤2: 调用 changeOptions (libcore.dll)...'); final changeOptionsStartTime = DateTime.now(); final changeResult = await kr_singBox.changeOptions(oOption).run(); final changeOptionsDuration = DateTime.now().difference(changeOptionsStartTime).inMilliseconds; await KRFileLogger.log('[黑屏调试] [${DateTime.now()}] ✅ 步骤2完成: changeOptions 耗时 ${changeOptionsDuration}ms'); await KRFileLogger.log('[_kr_startInternal] changeOptions 完成 - ${DateTime.now()}'); changeResult.match( (error) { KRFileLogger.log('[黑屏调试] [${DateTime.now()}] ❌ changeOptions 失败: $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 KRFileLogger.log('[黑屏调试] [${DateTime.now()}] ⏳ 步骤3: 调用 start (libcore.dll)...'); final startStartTime = DateTime.now(); await KRFileLogger.log('[_kr_startInternal] 🔴 开始 kr_singBox.start() - $startStartTime'); await kr_singBox.start(_cutPath, kr_configName, false).map( (r) { KRLogUtil.kr_i('✅ SingBox 启动成功', tag: 'SingBox'); }, ).mapLeft((err) { KRFileLogger.log('[黑屏调试] [${DateTime.now()}] ❌ start 失败: $err'); KRLogUtil.kr_e('❌ SingBox 启动失败: $err', tag: 'SingBox'); // 不需要手动设置状态,libcore 会通过 status stream 自动发送 Stopped 事件 throw err; }).run(); final startEndTime = DateTime.now(); final startDurationMs = startEndTime.difference(startStartTime).inMilliseconds; await KRFileLogger.log('[黑屏调试] [${DateTime.now()}] ✅ 步骤3完成: start 耗时 ${startDurationMs}ms'); await KRFileLogger.log('[_kr_startInternal] ✅ kr_singBox.start() 完成(耗时: ${startDurationMs}ms)- $startEndTime'); // 🔍 关键诊断:检查 8964 端口是否真的准备好了! await KRFileLogger.log('[_kr_startInternal] 🔍 检查 sing-box 的 8964 端口是否真的准备好...'); bool port8964Ready = false; int connectionAttempts = 0; final portCheckStartTime = DateTime.now(); // 立即尝试连接,看 sing-box 是否真的启动了 try { final socket = await Socket.connect('127.0.0.1', 8964).timeout( const Duration(milliseconds: 500), onTimeout: () => throw Exception('8964 端口连接超时'), ); socket.destroy(); port8964Ready = true; connectionAttempts = 0; await KRFileLogger.log('[_kr_startInternal] ✅ 8964 端口已准备好(立即连接成功)'); } catch (e) { // 立即失败,说明 sing-box 还没准备好 connectionAttempts = 1; final portCheckDuration = DateTime.now().difference(portCheckStartTime).inMilliseconds; await KRFileLogger.log('[_kr_startInternal] ❌ 【关键发现】8964 端口未准备好!(连接失败:$e)'); await KRFileLogger.log('[_kr_startInternal] 🚨 这说明 start() 返回成功,但 sing-box 还在初始化中!'); await KRFileLogger.log('[_kr_startInternal] 🚨 后台任务在 1000ms 后会尝试订阅,极可能失败(这是 UI 卡顿的根本原因!)'); } // ✅ 系统代理由 Go 后端 (libcore.dll) 自动处理 // 原因:通过 changeOptions() 传递 setSystemProxy: true,libcore 会自动设置系统代理 // 参考 Hiddify 实现:Dart 层不再手动调用注册表或 WinINet API await KRFileLogger.log('[_kr_startInternal] ℹ️ 系统代理由 libcore 自动管理 - ${DateTime.now()}'); // 🍎 macOS 专用:额外设置 SOCKS5 系统代理(让 Telegram 等应用自动走代理) // 原因:libcore 只设置 HTTP/HTTPS 代理,但 Telegram 只检测 SOCKS5 代理 // 参考 Clash:同时设置 HTTP + SOCKS5 系统代理 if (Platform.isMacOS && kr_connectionType.value != KRConnectionType.global) { await _kr_setMacOSSocks5Proxy(true); } // 🔴 【方案 A】启动 sing-box 进程健康监测 // 目的:检测 sing-box 是否在启动后 1-2 秒内崩溃 // 背景:日志显示 start() 返回成功,但 1.4 秒后 8964 端口无法连接 // 说明 sing-box 进程启动后崩溃了,导致后续 UI 卡顿 unawaited(_monitorSingBoxProcess()); // ⚠️ 关键修复:在启动成功后立即订阅统计流 // 原因: // 1. 统计流需要主动订阅才能接收数据 // 2. UI 只是读取 kr_stats.value,不会触发订阅 // 3. command.sock 在 start() 成功后会立即创建 KRLogUtil.kr_i('✅ SingBox 核心已启动,开始初始化 command client', tag: 'SingBox'); // 🔑 在后台延迟订阅统计流和分组流,避免阻塞 UI await KRFileLogger.log('[_kr_startInternal] 🟢 关键路径完成,返回给调用者 - ${DateTime.now()}'); Future.delayed(const Duration(milliseconds: 1000), () async { final backgroundStartTime = DateTime.now(); // 【关键】检查停止标志,如果用户在延迟期间点击了停止,就立即返回 // 这避免了竞态条件:后台任务在 stop() 过程中仍然在执行订阅 if (_stopRequested) { await KRFileLogger.log('[后台任务] ⏭️ 检测到停止标志,后台任务立即返回(避免竞态条件) - $backgroundStartTime'); return; } try { await KRFileLogger.log('[后台任务] 🔴 启动 1000ms 延迟后的后台任务 - $backgroundStartTime'); // 订阅统计流 try { // 🔍 诊断:检查 8964 端口状态 final subscribeCheckStartTime = DateTime.now(); bool port8964ConnectOk = false; try { final socket = await Socket.connect('127.0.0.1', 8964).timeout( const Duration(milliseconds: 200), onTimeout: () => throw Exception('端口连接超时'), ); socket.destroy(); port8964ConnectOk = true; await KRFileLogger.log('[后台任务] 📊 准备订阅前检查:8964 端口✅可以连接'); } catch (portE) { await KRFileLogger.log('[后台任务] 📊 准备订阅前检查:8964 端口❌无法连接($portE)- 这说明 sing-box 还没准备好!'); } // 🔧 关键修复:如果端口检查失败,立即返回,避免 FFI 调用阻塞 UI 3+ 秒 if (!port8964ConnectOk) { await KRFileLogger.log('[后台任务] ⛔ 8964 端口不可用,跳过订阅操作(避免 FFI 超时阻塞 UI)- ${DateTime.now()}'); KRLogUtil.kr_w('⚠️ sing-box command 端口不可用,跳过订阅', tag: 'SingBox'); return; } // 【关键】在执行订阅前再次检查停止标志 if (_stopRequested) { await KRFileLogger.log('[后台任务] ⏭️ 停止标志已设置,取消统计订阅 - ${DateTime.now()}'); return; } await KRFileLogger.log('[后台任务] 🟡 开始订阅统计数据流 - ${DateTime.now()}'); KRLogUtil.kr_i('📊 开始订阅统计数据流...', tag: 'SingBox'); await _kr_subscribeToStats(); // 🔧 UI阻塞修复:使用await确保让出控制权 KRLogUtil.kr_i('✅ 统计数据流订阅成功', tag: 'SingBox'); await KRFileLogger.log('[后台任务] ✅ 统计数据流订阅成功 - ${DateTime.now()}'); } catch (e) { KRLogUtil.kr_w('⚠️ 统计数据流订阅失败(稍后重试): $e', tag: 'SingBox'); await KRFileLogger.log('[后台任务] ❌ 统计数据流订阅失败: $e - ${DateTime.now()}'); await KRFileLogger.log('[后台任务] 🚨 这是导致后续操作失败的关键问题!后台任务被阻塞,导致 Lock 被占用!'); // 如果第一次失败,再等待一段时间重试 Future.delayed(const Duration(milliseconds: 2000), () async { // 🔧 重试前检查端口可用性,避免 FFI 阻塞 try { final socket = await Socket.connect('127.0.0.1', 8964).timeout( const Duration(milliseconds: 200), onTimeout: () => throw Exception('端口连接超时'), ); socket.destroy(); } catch (_) { KRLogUtil.kr_w('⚠️ 重试订阅统计流时端口不可用,跳过', tag: 'SingBox'); return; } try { await _kr_subscribeToStats(); // 🔧 UI阻塞修复:使用await KRLogUtil.kr_i('✅ 统计数据流重试订阅成功', tag: 'SingBox'); } catch (e2) { KRLogUtil.kr_e('❌ 统计数据流重试订阅失败: $e2', tag: 'SingBox'); } }); } // 🔧 关键修复:订阅分组数据流 try { // 【关键】在执行订阅前再次检查停止标志 if (_stopRequested) { await KRFileLogger.log('[后台任务] ⏭️ 停止标志已设置,取消分组订阅 - ${DateTime.now()}'); return; } await KRFileLogger.log('[后台任务] 🟡 开始订阅分组数据流 - ${DateTime.now()}'); KRLogUtil.kr_i('📋 开始订阅分组数据流...', tag: 'SingBox'); await _kr_subscribeToGroups(); // 🔧 UI阻塞修复:使用await确保让出控制权 KRLogUtil.kr_i('✅ 分组数据流订阅成功', tag: 'SingBox'); await KRFileLogger.log('[后台任务] ✅ 分组数据流订阅成功 - ${DateTime.now()}'); } catch (e) { KRLogUtil.kr_w('⚠️ 分组数据流订阅失败(稍后重试): $e', tag: 'SingBox'); await KRFileLogger.log('[后台任务] ❌ 分组数据流订阅失败: $e - ${DateTime.now()}'); // 如果第一次失败,再等待一段时间重试 Future.delayed(const Duration(milliseconds: 2000), () async { // 🔧 重试前检查端口可用性,避免 FFI 阻塞 try { final socket = await Socket.connect('127.0.0.1', 8964).timeout( const Duration(milliseconds: 200), onTimeout: () => throw Exception('端口连接超时'), ); socket.destroy(); } catch (_) { KRLogUtil.kr_w('⚠️ 重试订阅分组流时端口不可用,跳过', tag: 'SingBox'); return; } try { await _kr_subscribeToGroups(); // 🔧 UI阻塞修复:使用await KRLogUtil.kr_i('✅ 分组数据流重试订阅成功', tag: 'SingBox'); } catch (e2) { KRLogUtil.kr_e('❌ 分组数据流重试订阅失败: $e2', tag: 'SingBox'); } }); } // 🔧 关键修复:恢复用户选择的节点 try { // 【关键】在执行节点恢复前再次检查停止标志 if (_stopRequested) { await KRFileLogger.log('[后台任务] ⏭️ 停止标志已设置,取消节点恢复 - ${DateTime.now()}'); return; } final nodeRestoreStartTime = DateTime.now(); await KRFileLogger.log('[后台任务] 🟡 开始恢复用户选择的节点 - $nodeRestoreStartTime'); final selectedNode = await KRSecureStorage().kr_readData(key: _keySelectedNode); if (selectedNode != null && selectedNode.isNotEmpty && selectedNode != 'auto') { KRLogUtil.kr_i('🎯 恢复用户选择的节点: $selectedNode', tag: 'SingBox'); await KRFileLogger.log('[后台任务] 🟡 找到保存的节点: $selectedNode - ${DateTime.now()}'); if (kDebugMode) { KRFileLogger.log('[黑屏调试] 🔵 启动后恢复节点选择: $selectedNode'); } // 🔧 关键修复:等待活动组准备就绪,而不是固定延迟 // 这是解决"连接后 selected=auto"问题的关键 KRLogUtil.kr_i('⏳ 等待活动组准备就绪...', tag: 'SingBox'); final groupWaitStartTime = DateTime.now(); await KRFileLogger.log('[后台任务] ⏳ 开始等待活动组准备就绪 - $groupWaitStartTime'); int waitCount = 0; const maxWaitCount = 25; // 最多等待 5 秒 (25 * 200ms) while (kr_activeGroups.isEmpty && waitCount < maxWaitCount) { await Future.delayed(const Duration(milliseconds: 200)); waitCount++; if (waitCount % 5 == 0) { KRLogUtil.kr_d('⏳ 等待活动组... ($waitCount/$maxWaitCount)', tag: 'SingBox'); await KRFileLogger.log('[后台任务] ⏳ 等待活动组进行中... (${waitCount}/$maxWaitCount, 已耗时 ${DateTime.now().difference(groupWaitStartTime).inMilliseconds}ms) - ${DateTime.now()}'); } } if (kr_activeGroups.isEmpty) { KRLogUtil.kr_w('⚠️ 等待活动组超时,仍尝试恢复节点', tag: 'SingBox'); final groupWaitDuration = DateTime.now().difference(groupWaitStartTime).inMilliseconds; await KRFileLogger.log('[后台任务] ⚠️ 等待活动组超时 (${groupWaitDuration}ms) - ${DateTime.now()}'); } else { KRLogUtil.kr_i('✅ 活动组已就绪,数量: ${kr_activeGroups.length}', tag: 'SingBox'); final groupWaitDuration = DateTime.now().difference(groupWaitStartTime).inMilliseconds; await KRFileLogger.log('[后台任务] ✅ 活动组已就绪,数量: ${kr_activeGroups.length} (耗时 ${groupWaitDuration}ms) - ${DateTime.now()}'); } // 🔧 关键修复:使用 await 等待节点切换完成 try { final switchStartTime = DateTime.now(); await KRFileLogger.log('[后台任务] 🟡 开始恢复节点切换: $selectedNode - $switchStartTime'); await kr_selectOutbound(selectedNode); KRLogUtil.kr_i('✅ 节点已切换到用户选择: $selectedNode', tag: 'SingBox'); final switchDuration = DateTime.now().difference(switchStartTime).inMilliseconds; await KRFileLogger.log('[后台任务] ✅ 节点切换成功: $selectedNode (耗时 ${switchDuration}ms) - ${DateTime.now()}'); if (kDebugMode) { KRFileLogger.log('[黑屏调试] 🔵 节点切换成功: $selectedNode'); } } catch (e) { KRLogUtil.kr_e('❌ 节点切换失败: $e', tag: 'SingBox'); await KRFileLogger.log('[后台任务] ❌ 节点切换失败: $e - ${DateTime.now()}'); if (kDebugMode) { KRFileLogger.log('[黑屏调试] 🔵 节点切换失败: $e'); } } final nodeRestoreDuration = DateTime.now().difference(nodeRestoreStartTime).inMilliseconds; await KRFileLogger.log('[后台任务] ✅ 节点恢复流程完成(总耗时 ${nodeRestoreDuration}ms)- ${DateTime.now()}'); } else { KRLogUtil.kr_i('ℹ️ 没有保存的节点选择或为auto,使用默认配置', tag: 'SingBox'); await KRFileLogger.log('[后台任务] ℹ️ 没有保存的节点选择或为auto - ${DateTime.now()}'); if (kDebugMode) { KRFileLogger.log('[黑屏调试] 🔵 没有保存的节点选择,使用默认'); } } } catch (e) { KRLogUtil.kr_e('❌ 恢复节点选择失败: $e', tag: 'SingBox'); await KRFileLogger.log('[后台任务] ❌ 恢复节点选择失败: $e - ${DateTime.now()}'); if (kDebugMode) { KRFileLogger.log('[黑屏调试] 🔵 恢复节点选择失败: $e'); } } final backgroundDuration = DateTime.now().difference(backgroundStartTime).inMilliseconds; await KRFileLogger.log('[后台任务] 🟢 所有后台任务完成(总耗时 ${backgroundDuration}ms)- ${DateTime.now()}'); } catch (e) { KRLogUtil.kr_e('💥 后台任务异常: $e', tag: 'SingBox'); await KRFileLogger.log('[后台任务] 💥 后台任务异常: $e - ${DateTime.now()}'); } }); } catch (e, stackTrace) { KRLogUtil.kr_e('💥 SingBox 启动异常: $e', tag: 'SingBox'); KRLogUtil.kr_e('📚 错误堆栈: $stackTrace', tag: 'SingBox'); // 不需要手动设置状态,libcore 会自动处理 rethrow; } } /// 停止服务 Future kr_stop() async { // 🔧 文件日志:记录 kr_stop() 调用 + 时间戳 final clickTime = DateTime.now(); await KRFileLogger.log('[kr_stop] ⏱️ 用户点击关闭 VPN - $clickTime'); // 🔧 文件日志:等待 Lock + 时间戳 final beforeLock = DateTime.now(); await KRFileLogger.log('[kr_stop] ⏱️ 开始等待 Lock... - $beforeLock'); // ❌ 关键修复:添加 Lock 保护,防止 kr_start() 和 kr_stop() 并发执行 // 快速连续点击开关时,此 Lock 会序列化所有操作,防止 UI 堵塞 return _startStopLock.synchronized(() async { final afterLock = DateTime.now(); final lockWaitMs = afterLock.difference(beforeLock).inMilliseconds; await KRFileLogger.log('[kr_stop] ✅ Lock 已获取(等待 ${lockWaitMs}ms)- $afterLock'); // 【关键】设置停止标志,通知后台任务停止执行 // 这会让正在执行或即将执行的后台任务立即返回,避免竞态条件 _stopRequested = true; await KRFileLogger.log('[kr_stop] 🔴 设置停止标志,通知后台任务立即停止执行 - ${DateTime.now()}'); try { final beforeInternal = DateTime.now(); await _kr_stopInternal(); final afterInternal = DateTime.now(); final internalMs = afterInternal.difference(beforeInternal).inMilliseconds; await KRFileLogger.log('[kr_stop] ✅ _kr_stopInternal() 完成(耗时 ${internalMs}ms)- $afterInternal'); } catch (e) { await KRFileLogger.log('[kr_stop] ❌ _kr_stopInternal() 异常: $e - ${DateTime.now()}'); rethrow; } finally { // 在返回之前重置标志,为下一次启动做准备 _stopRequested = false; await KRFileLogger.log('[kr_stop] 🟢 停止标志已重置,为下一次启动做准备 - ${DateTime.now()}'); } }); } /// 实际的停止逻辑(被 Lock 保护) Future _kr_stopInternal() async { final stopInternalStartTime = DateTime.now(); await KRFileLogger.log('[_kr_stopInternal] 🔴 _kr_stopInternal 方法开始执行 - $stopInternalStartTime'); // ✅ DNS 由 libcore.dll 自动管理 try { KRLogUtil.kr_i('🛑 停止 SingBox 服务...', tag: 'SingBox'); // 取消节点选择监控定时器 _nodeSelectionTimer?.cancel(); _nodeSelectionTimer = null; KRLogUtil.kr_i('✅ 节点选择监控已停止', tag: 'SingBox'); await KRFileLogger.log('[_kr_stopInternal] ✅ 节点选择监控已停止 - ${DateTime.now()}'); await Future.delayed(const Duration(milliseconds: 100)); // 添加超时保护,防止 stop() 调用阻塞 // 🔧 超时时间 10 秒,确保 libcore 完成清理 try { await KRFileLogger.log('[黑屏调试] [${DateTime.now()}] ⏳ 步骤: 调用 stop (libcore.dll)...'); final stopStartTime = DateTime.now(); await KRFileLogger.log('[_kr_stopInternal] 开始 kr_singBox.stop() - ${DateTime.now()}'); await kr_singBox.stop().run().timeout( const Duration(seconds: 10), // ← 从 3 秒延长到 10 秒 onTimeout: () { KRFileLogger.log('[黑屏调试] [${DateTime.now()}] ⚠️ stop 超时(10秒)'); KRLogUtil.kr_w('⚠️ 停止操作超时(10秒),强制继续', tag: 'SingBox'); return const Left('timeout'); }, ); final stopDuration = DateTime.now().difference(stopStartTime).inMilliseconds; await KRFileLogger.log('[黑屏调试] [${DateTime.now()}] ✅ stop 完成: 耗时 ${stopDuration}ms'); await KRFileLogger.log('[_kr_stopInternal] kr_singBox.stop() 完成 - ${DateTime.now()}'); } catch (e) { KRLogUtil.kr_w('⚠️ 停止操作失败(可能已经停止): $e', tag: 'SingBox'); await KRFileLogger.log('[_kr_stopInternal] kr_singBox.stop() 异常: $e - ${DateTime.now()}'); // 继续执行清理操作 } // ✅ DNS 和系统代理都由 Go 后端 (libcore.dll) 自动管理 // 参考 Hiddify 实现:sing-box 内部有完整的 DNS 代理机制,不需要手动恢复系统 DNS await KRFileLogger.log('[_kr_stopInternal] ℹ️ DNS 由 libcore 自动管理 - ${DateTime.now()}'); // 🍎 macOS 专用:清除 SOCKS5 系统代理 if (Platform.isMacOS) { await _kr_setMacOSSocks5Proxy(false); } // 🔧 P0-1: 明确清理 groups 和 stats 订阅 final subscriptionCleanStartTime = DateTime.now(); await KRFileLogger.log('[_kr_stopInternal] 🔴 开始清理订阅 - $subscriptionCleanStartTime'); KRLogUtil.kr_i('🧹 清理 command client 订阅...', tag: 'SingBox'); // 清理 groups 订阅 try { await KRFileLogger.log('[_kr_stopInternal] 🟡 清理Groups订阅中 - ${DateTime.now()}'); _subscriptionMap[_SubscriptionType.groups]?.cancel(); _subscriptionMap.remove(_SubscriptionType.groups); KRLogUtil.kr_i('✅ Groups 订阅已取消', tag: 'SingBox'); await KRFileLogger.log('[_kr_stopInternal] ✅ Groups 订阅已取消 - ${DateTime.now()}'); } catch (e) { KRLogUtil.kr_w('⚠️ Groups 订阅取消失败: $e', tag: 'SingBox'); await KRFileLogger.log('[_kr_stopInternal] ❌ Groups 订阅取消失败: $e - ${DateTime.now()}'); } // 清理 stats 订阅 try { await KRFileLogger.log('[_kr_stopInternal] 🟡 清理Stats订阅中 - ${DateTime.now()}'); _subscriptionMap[_SubscriptionType.stats]?.cancel(); _subscriptionMap.remove(_SubscriptionType.stats); KRLogUtil.kr_i('✅ Stats 订阅已取消', tag: 'SingBox'); await KRFileLogger.log('[_kr_stopInternal] ✅ Stats 订阅已取消 - ${DateTime.now()}'); } catch (e) { KRLogUtil.kr_w('⚠️ Stats 订阅取消失败: $e', tag: 'SingBox'); await KRFileLogger.log('[_kr_stopInternal] ❌ Stats 订阅取消失败: $e - ${DateTime.now()}'); } // 从列表中清除非 Status 项(向后兼容) await KRFileLogger.log('[_kr_stopInternal] 🟡 清理其他订阅中 - ${DateTime.now()}'); final subscriptionsToCancel = _kr_subscriptions.where((sub) { final hashStr = sub.hashCode.toString(); return !hashStr.contains('Status'); // 不取消状态订阅 }).toList(); for (var subscription in subscriptionsToCancel) { _kr_subscriptions.remove(subscription); } final subscriptionCleanDuration = DateTime.now().difference(subscriptionCleanStartTime).inMilliseconds; KRLogUtil.kr_i('✅ 订阅清理完成,剩余订阅数: ${_kr_subscriptions.length}', tag: 'SingBox'); await KRFileLogger.log('[_kr_stopInternal] ✅ 订阅清理完成(耗时: ${subscriptionCleanDuration}ms),剩余订阅数: ${_kr_subscriptions.length} - ${DateTime.now()}'); // 不手动设置状态,由 libcore 通过 status stream 自动发送 Stopped 事件 KRLogUtil.kr_i('✅ SingBox 停止请求已发送', tag: 'SingBox'); } catch (e, stackTrace) { KRLogUtil.kr_e('停止服务时出错: $e'); KRLogUtil.kr_e('错误堆栈: $stackTrace'); // ✅ DNS 由 libcore 自动管理,无需手动恢复 // 不手动设置状态,信任 libcore 的状态管理 rethrow; } finally { // ✅ 关键修复:系统代理恢复改为异步后台执行,不在 finally 块中 await // 这样 kr_stop() 可以立即返回,不被注册表操作阻塞 final finallyStartTime = DateTime.now(); await KRFileLogger.log('[_kr_stopInternal] 🔴 finally 块开始执行 - $finallyStartTime'); // ✅ 系统代理恢复由 Go 后端 (libcore.dll) 自动处理 // 原因:libcore.stop() 会自动恢复系统代理设置 // 参考 Hiddify 实现:Dart 层不再手动调用注册表或 WinINet API await KRFileLogger.log('[_kr_stopInternal] ℹ️ 系统代理恢复由 libcore 自动管理 - ${DateTime.now()}'); // 最后的诊断日志 final stopInternalEndTime = DateTime.now(); await KRFileLogger.log('[_kr_stopInternal] 🟢 _kr_stopInternal 方法即将退出 - $stopInternalEndTime'); } } /// 🪟 Windows:Start 之前检查 old command server 端口占用情况 /// /// ✅ 极限修复:完全跳过 Windows 端口检查! /// 原因: /// 1. netstat、powershell、taskkill 都是系统命令,每个 0.5-1 秒 /// 2. 连续调用导致 2-3 秒的累计卡顿 /// 3. 造成 icon 停止转动、整个框架卡死 /// 4. libcore 已经有完整的端口冲突处理逻辑 /// /// 相信 libcore 的自动重试机制,比强制同步检查更可靠! Future _kr_preflightWindowsCommandServerPort() async { if (!Platform.isWindows) return; KRLogUtil.kr_i('⏭️ 跳过 Windows 端口预检查,相信 libcore 的自动处理机制', tag: 'SingBox'); // 完全不做检查,直接返回 // libcore 会自动处理所有的端口冲突情况 } // ✅ 系统代理设置/恢复已移交给 Go 后端 (libcore.dll) 自动处理 // 参考 Hiddify 实现:通过 changeOptions() 传递 setSystemProxy: true // libcore 在 start() 时自动设置代理,在 stop() 时自动恢复 // 优势: // ✅ 无黑窗问题(Go 后端内部处理,不执行外部命令) // ✅ 更快速(无进程启动开销) // ✅ 更可靠(由 sing-box 核心统一管理) /// Windows: cleanup fixed command server port on logout. /// Returns a user-facing warning message if another process owns the port. Future kr_cleanupWindowsCommandServerPortOnLogout() async { if (!Platform.isWindows) return null; try { final pidInUse = await _kr_getListeningPidByPort(_krWindowsCommandServerPort); if (pidInUse == null) return null; final processName = await _kr_getProcessNameByPid(pidInUse); final currentExeName = p.basename(Platform.resolvedExecutable).toLowerCase(); final normalizedProcessName = processName?.toLowerCase(); if (normalizedProcessName != null && normalizedProcessName == currentExeName) { KRLogUtil.kr_w( 'logout cleanup: detected port $_krWindowsCommandServerPort in use by self (pid=$pidInUse), killing', tag: 'SingBox', ); final killed = await _kr_killProcess(pidInUse); if (!killed) { KRLogUtil.kr_w( 'logout cleanup: failed to kill process (pid=$pidInUse)', tag: 'SingBox', ); } return null; } final displayName = processName ?? 'unknown'; return '当前有其他进程占用$_krWindowsCommandServerPort端口,进程名: $displayName,Pid: $pidInUse,请手动关闭'; } catch (e) { KRLogUtil.kr_w('logout cleanup: port check failed: $e', tag: 'SingBox'); return null; } } /// Windows: get the listening PID for a port (LISTEN only). Future _kr_getListeningPidByPort(int port) async { if (!Platform.isWindows) return null; // Use numeric output to avoid reverse DNS lookups, which can stall when DNS is unstable. final result = await KRWindowsProcessUtil.runHidden('netstat', ['-ano', '-p', 'tcp', '-n']); if (result.exitCode != 0) { KRLogUtil.kr_w('⚠️ netstat 执行失败: ${result.stderr}', tag: 'SingBox'); return null; } final output = result.stdout.toString(); final lines = output.split(RegExp(r'\r?\n')); for (final rawLine in lines) { final line = rawLine.trim(); if (line.isEmpty) continue; if (!line.startsWith('TCP')) continue; final parts = line.split(RegExp(r'\s+')); // 预期格式: TCP if (parts.length < 4) continue; final local = parts.length >= 2 ? parts[1] : ''; final foreign = parts.length >= 3 ? parts[2] : ''; final pidStr = parts.isNotEmpty ? parts.last : ''; // 🔧 P2-1: 改进端口检测精度 - 使用更精确的正则匹配 // 监听行 foreign 一般为 0.0.0.0:0 或 [::]:0(避免依赖 LISTENING 文本是否本地化) final isListeningRow = RegExp(r'(0\.0\.0\.0|::|.*?):\s*0\s*$').hasMatch(foreign); if (!isListeningRow) continue; // 精确匹配端口号(不使用字符串 endsWith,避免误匹配) // 例如: 127.0.0.1:51213 或 [::1]:51213 或 *:51213 final portPattern = RegExp(r':\s*$port\s*$'); if (!portPattern.hasMatch(local)) continue; final pidValue = int.tryParse(pidStr.trim()); if (pidValue != null && pidValue > 0) { KRLogUtil.kr_d( '🔍 端口 $port 被进程 $pidValue 占用 (local: $local, foreign: $foreign)', tag: 'SingBox', ); return pidValue; } } KRLogUtil.kr_d('✅ 端口 $port 未被占用', tag: 'SingBox'); return null; } Future _kr_getProcessNameByPid(int pidValue) async { if (!Platform.isWindows) return null; try { final result = await KRWindowsProcessUtil.runHidden('tasklist', [ '/FI', 'PID eq $pidValue', '/FO', 'CSV', '/NH', ]); if (result.exitCode != 0) { KRLogUtil.kr_w('⚠️ tasklist 执行失败: ${result.stderr}', tag: 'SingBox'); return null; } final stdout = result.stdout.toString().trim(); if (stdout.isEmpty) return null; if (stdout.startsWith('INFO:')) return null; // CSV 第一列为 Image Name,例如: "BearVPN.exe","1234",... final firstLine = stdout.split(RegExp(r'\r?\n')).first.trim(); final match = RegExp(r'^"([^"]+)"').firstMatch(firstLine); return match?.group(1); } catch (e) { // 🔧 P2-8: 读/写竞争条件 - 捕获异常,防止进程查询导致的崩溃 KRLogUtil.kr_w('⚠️ 进程信息查询异常: $e', tag: 'SingBox'); return null; } } Future _kr_killProcess(int pidValue) async { if (!Platform.isWindows) return false; final result = await KRWindowsProcessUtil.runHidden('taskkill', [ '/PID', pidValue.toString(), '/T', '/F', ]); if (result.exitCode == 0) return true; KRLogUtil.kr_w( '⚠️ taskkill 失败: exitCode=${result.exitCode}, stderr=${result.stderr}, stdout=${result.stdout}', tag: 'SingBox', ); return false; } Future _kr_waitPortFree(int port, {required Duration timeout}) async { final deadline = DateTime.now().add(timeout); while (DateTime.now().isBefore(deadline)) { final pidInUse = await _kr_getListeningPidByPort(port); if (pidInUse == null) return true; await Future.delayed(const Duration(milliseconds: 200)); } return false; } /// 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'); // 🔧 P0-2: 改进异常处理 - stop 失败也要尝试 start // 🔧 P1-1: 优化网络抖动 - 监听状态变化而非固定延迟 // ✅ DNS/代理 由 libcore.dll 自动管理 try { // 1. 先停止 try { await kr_stop(); } catch (stopError) { KRLogUtil.kr_e('❌ 停止失败: $stopError,仍尝试启动', tag: 'SingBox'); // 不 rethrow,继续尝试启动 } // 🔧 P1-1: 2. 等待完全停止 - 监听状态而非固定延迟(减少网络中断时间) try { // 如果已经是停止状态,直接继续 if (kr_status.value is SingboxStopped) { KRLogUtil.kr_i('✅ SingBox 已是停止状态,立即启动', tag: 'SingBox'); } else { // 等待状态变为停止,最多等待2秒(比原来的盲等500ms更聪明) final completer = Completer(); late final Worker worker; worker = ever(kr_status, (status) { if (status is SingboxStopped) { if (!completer.isCompleted) { completer.complete(); worker.dispose(); KRLogUtil.kr_i('✅ 检测到 SingBox 已停止,立即启动新实例', tag: 'SingBox'); } } }); await completer.future.timeout( const Duration(seconds: 2), onTimeout: () { KRLogUtil.kr_w('⏱️ 等待停止状态超时(2s),使用备用延迟', tag: 'SingBox'); worker.dispose(); }, ); } } catch (stateError) { KRLogUtil.kr_w('⚠️ 状态监听异常: $stateError,使用备用延迟', tag: 'SingBox'); // 备用:使用较短的延迟(300ms代替原来的500ms) await Future.delayed(const Duration(milliseconds: 300)); } // 🔧 P1-1: 3. 重新启动 try { await kr_start(); KRLogUtil.kr_i('✅ SingBox 重启完成', tag: 'SingBox'); } catch (startError) { KRLogUtil.kr_e('❌ 重启失败: $startError', tag: 'SingBox'); rethrow; // 只有启动失败才抛异常 } } catch (e) { KRLogUtil.kr_e('❌ 重启异常: $e', tag: 'SingBox'); rethrow; } } //// 设置出站模式 Future kr_updateConnectionType(KRConnectionType newType) async { final previousType = kr_connectionType.value; // 保存原模式用于失败时回滚 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; } // ✅ 改进:直接尝试切换(不做权限检查避免黑屏) // 关键改进:保存原模式,失败时回滚以防止自动重连机制无限循环 KRLogUtil.kr_i('📝 尝试切换模式到: $newType(原模式: $previousType)', tag: 'SingBox'); 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'); try { // 重启前添加超时保护(10秒),避免卡顿 await kr_restart().timeout( const Duration(seconds: 10), onTimeout: () { KRLogUtil.kr_w('⏱️ VPN重启超时(10s),可能缺少权限', tag: 'SingBox'); throw TimeoutException('VPN重启超时,请检查是否需要管理员权限'); }, ); KRLogUtil.kr_i('✅ VPN重启完成', tag: 'SingBox'); } catch (restartError) { KRLogUtil.kr_e('❌ VPN重启失败: $restartError', tag: 'SingBox'); // 重启失败时回滚模式改变,避免触发自动重连机制无限循环 KRLogUtil.kr_w('🔙 重启失败,回滚模式到: $previousType', tag: 'SingBox'); kr_connectionType.value = previousType; // ❌ 关键修复:不要 rethrow,异常已经完整处理(已回滚 + Toast 提示由监听器负责) // rethrow; } } else { KRLogUtil.kr_i('ℹ️ VPN未启动,配置已更新', tag: 'SingBox'); } } catch (e, stackTrace) { KRLogUtil.kr_e('💥 更新连接类型异常: $e', tag: 'SingBox'); KRLogUtil.kr_e('📚 错误堆栈: $stackTrace', tag: 'SingBox'); // 最终保险:异常时确保回滚模式 if (kr_connectionType.value != previousType) { KRLogUtil.kr_w('🔙 异常捕获,回滚模式到: $previousType', tag: 'SingBox'); kr_connectionType.value = previousType; } // ❌ 关键修复:不要 rethrow // 异常已经通过 Toast 由监听器提示,模式已回滚 // 继续 rethrow 会导致对话框无法关闭,让应用看起来"卡"了 // 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 { // ========== 关键路径:必需的同步操作 ========== // 🔧 P2新: 检查 VPN 是否在运行 if (kr_status.value is! SingboxStarted) { KRLogUtil.kr_w('⚠️ VPN 未运行,无法选择节点: $tag', tag: 'SingBox'); throw Exception('VPN 未启动,无法选择节点。请先启动 VPN'); } KRLogUtil.kr_i('🎯 开始选择出站节点: $tag', tag: 'SingBox'); // 🔧 关键修复:使用 await 确保保存完成 try { await KRSecureStorage().kr_saveData(key: _keySelectedNode, value: tag); if (kDebugMode) { KRLogUtil.kr_d('✅ 节点选择已保存: $tag', tag: 'SingBox'); } } catch (e) { KRLogUtil.kr_e('❌ 保存节点选择失败: $e', tag: 'SingBox'); } // 🔧 关键修复:使用 await 确保 command client 初始化完成 try { await _kr_ensureCommandClientInitialized(); if (kDebugMode) { KRLogUtil.kr_d('✅ Command client 已就绪,执行节点切换', tag: 'SingBox'); } // 🔧 关键修复:使用正确的 group tag // libcore 生成的selector组的tag是"proxy"而不是"select" final selectorGroupTag = kr_activeGroups.any((g) => g.tag == 'select') ? 'select' : 'proxy'; // ⚡ 关键优化:核心 API 调用 await _kr_selectOutboundWithRetry(selectorGroupTag, tag, maxAttempts: 3, initialDelay: 50); if (kDebugMode) { KRLogUtil.kr_d('✅ 节点切换API调用完成: $tag', tag: 'SingBox'); } // ========== 关键路径结束!以下操作都在后台进行 ========== // ⚡ 后台任务:验证 + 刷新 + 监控(不影响返回) unawaited(_kr_backgroundNodeVerificationAndRefresh(tag)); } catch (e) { KRLogUtil.kr_e('❌ 节点选择失败: $e', tag: 'SingBox'); rethrow; // 抛出异常,让调用者知道失败了 } } /// ⚡ 后台验证和刷新(不影响 UI 响应) /// 包含:验证节点选择 + TUN 模式刷新 + 启动监控定时器 Future _kr_backgroundNodeVerificationAndRefresh(String tag) async { try { // 第一步:等待一点时间让活动组更新 await Future.delayed(const Duration(milliseconds: 100)); // 第二步:后台验证节点选择 await _krVerifyNodeSelectionInBackground(tag); // 第三步:TUN 模式刷新连接 if (Platform.isWindows && kr_connectionType.value == KRConnectionType.global) { await _kr_tunModeRefreshInBackground(tag); } // 第四步:启动节点选择监控(防止被 auto 覆盖) _kr_startNodeSelectionMonitoring(tag); } catch (e) { KRLogUtil.kr_e('❌ 后台任务异常(不影响节点切换): $e', tag: 'SingBox'); } } /// 后台验证节点选择(异步,失败只记日志) Future _krVerifyNodeSelectionInBackground(String tag) async { try { final selectGroup = kr_activeGroups.firstWhere( (group) => group.tag == 'select', orElse: () => throw Exception('未找到 "select" 选择器组'), ); if (selectGroup.selected != tag) { KRLogUtil.kr_w('⚠️ 节点验证失败: 期望 $tag, 实际 ${selectGroup.selected}', tag: 'SingBox'); } else if (kDebugMode) { KRLogUtil.kr_d('✅ 节点验证成功: $tag', tag: 'SingBox'); } } catch (e) { KRLogUtil.kr_e('❌ 节点验证异常: $e', tag: 'SingBox'); } } /// TUN 模式连接刷新(后台执行) Future _kr_tunModeRefreshInBackground(String tag) async { try { if (kDebugMode) { KRLogUtil.kr_d('🔄 TUN 模式:后台刷新连接...', tag: 'SingBox'); } final selectorGroupTag = kr_activeGroups.any((g) => g.tag == 'select') ? 'select' : 'proxy'; // 第一步:urlTest 创建新连接 final testResult = await kr_singBox.urlTest(selectorGroupTag).run().timeout( const Duration(seconds: 3), onTimeout: () { if (kDebugMode) { KRLogUtil.kr_d('⚠️ urlTest 超时', tag: 'SingBox'); } return const Left('timeout'); }, ); testResult.match( (error) { if (kDebugMode) { KRLogUtil.kr_d('⚠️ urlTest 失败: $error', tag: 'SingBox'); } }, (_) { if (kDebugMode) { KRLogUtil.kr_d('✅ urlTest 完成', tag: 'SingBox'); } }, ); // 第二步:HTTP 刷新(最有效) await _kr_forceHttpConnectionRefresh(tag); // 第三步:等待连接稳定 await Future.delayed(const Duration(milliseconds: 500)); if (kDebugMode) { KRLogUtil.kr_d('✅ TUN 模式连接刷新完成', tag: 'SingBox'); } } catch (e) { KRLogUtil.kr_e('❌ TUN 模式刷新异常: $e(不影响节点切换)', tag: 'SingBox'); } } /// 启动节点选择监控定时器(防止 urltest 自动覆盖) void _kr_startNodeSelectionMonitoring(String tag) { try { _nodeSelectionTimer?.cancel(); _nodeSelectionTimer = null; if (tag != 'auto') { final currentSelectorGroupTag = kr_activeGroups.any((g) => g.tag == 'select') ? 'select' : 'proxy'; final selectedTag = tag; if (kDebugMode) { KRLogUtil.kr_d('🔁 启动节点选择监控: $selectedTag', tag: 'SingBox'); } _nodeSelectionTimer = Timer.periodic(const Duration(seconds: 20), (timer) { try { kr_singBox.selectOutbound(currentSelectorGroupTag, selectedTag).run().then((result) { result.match( (error) { if (kDebugMode) { KRLogUtil.kr_d('🔁 定时器重选失败: $error', tag: 'SingBox'); } }, (_) { if (kDebugMode) { KRLogUtil.kr_d('🔁 定时器重选成功', tag: 'SingBox'); } }, ); }).catchError((error) { KRLogUtil.kr_w('🔁 定时器异常: $error', tag: 'SingBox'); }); } catch (e) { KRLogUtil.kr_e('💥 定时器回调异常: $e', tag: 'SingBox'); timer.cancel(); _nodeSelectionTimer = null; } }); } } catch (timerError) { KRLogUtil.kr_e('❌ 启动定时器异常: $timerError', tag: 'SingBox'); _nodeSelectionTimer?.cancel(); _nodeSelectionTimer = null; } } /// 配合文件地址 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"); /// 启动节点选择监控定时器,防止用户手动选择被自动策略覆盖 void kr_startNodeSelectionMonitor(String tag) { kr_resetNodeSelectionMonitor(); 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'); }); } } /// 重置并停止节点选择监控定时器 void kr_resetNodeSelectionMonitor() { _nodeSelectionTimer?.cancel(); _nodeSelectionTimer = null; KRLogUtil.kr_i('✅ 节点选择监控已停止', tag: 'SingBox'); } // File tempFile(String fileName) => file("$fileName.tmp"); /// ✅ 临时切换模式以完全断开浏览器连接(推荐方案) /// 原理: /// TUN 模式:浏览器 → 操作系统 → sing-box → 节点(浏览器无法关闭连接)❌ /// 系统代理:浏览器 → 代理软件 → 节点(代理可强制关闭连接)✅ /// 流程: /// 1. TUN → 系统代理(这时代理会主动关闭所有旧连接) /// 2. 发送 HTTP 请求(刷新代理缓存) /// 3. 系统代理 → TUN(恢复原状态) /// 优点:100% 彻底断开,无残留连接,UX 无感知(<100ms) Future _kr_refreshConnectionByModeSwitch(String nodeTag) async { if (!Platform.isWindows) { // 非 Windows 系统,使用原有的 HTTP 刷新方案 await _kr_forceHttpConnectionRefresh(nodeTag); return; } try { KRLogUtil.kr_i('🔄 [高级] 临时切换到系统代理以断开所有连接...', tag: 'SingBox'); final originalMode = kr_connectionType.value; // 第一步:临时切换到系统代理模式 KRLogUtil.kr_i(' ① 临时切换:TUN → 系统代理模式', tag: 'SingBox'); await kr_updateConnectionType(KRConnectionType.rule); await Future.delayed(const Duration(milliseconds: 100)); // 等待模式切换生效 // 第二步:发送 HTTP 请求强制刷新(此时用系统代理) KRLogUtil.kr_i(' ② 发送 HTTP 请求(通过系统代理刷新缓存)', tag: 'SingBox'); await _kr_forceHttpConnectionRefresh(nodeTag); // 第三步:切换回原模式(TUN) KRLogUtil.kr_i(' ③ 恢复:系统代理 → TUN 模式', tag: 'SingBox'); await kr_updateConnectionType(originalMode); await Future.delayed(const Duration(milliseconds: 100)); // 等待模式切换生效 KRLogUtil.kr_i('✅ [高级] 连接完全刷新完成,浏览器将使用新节点的新连接', tag: 'SingBox'); } catch (e) { KRLogUtil.kr_w('⚠️ 模式切换刷新异常: $e(节点切换已生效)', tag: 'SingBox'); } } /// ✅ 激进方案:完全重启 VPN(100% 清理所有连接) /// 原理:完全关闭 VPN 再重新启动,所有连接都会断开并重建 /// 优点:最彻底,无任何残留连接 /// 缺点:用户会短暂看到"已断开"状态,可能有 DNS 泄漏短暂风险 Future _kr_restartVpnForNodeSwitch(String nodeTag) async { try { KRLogUtil.kr_i('🔄 [激进] 重启 VPN 以彻底清理所有连接...', tag: 'SingBox'); // 第一步:关闭 VPN(断开所有连接) KRLogUtil.kr_i(' ① 停止 VPN', tag: 'SingBox'); await kr_stop(); await Future.delayed(const Duration(milliseconds: 300)); // 等待VPN完全停止 // 第二步:重新启动 VPN(建立新连接到新节点) KRLogUtil.kr_i(' ② 重新启动 VPN', tag: 'SingBox'); await kr_start(); KRLogUtil.kr_i('✅ [激进] VPN 重启完成,所有连接已刷新', tag: 'SingBox'); } catch (e) { KRLogUtil.kr_e('❌ VPN 重启失败: $e(请手动重新启动)', tag: 'SingBox'); // 尝试恢复 try { await kr_start(); } catch (e2) { KRLogUtil.kr_e('❌ VPN 恢复启动失败: $e2', tag: 'SingBox'); } } } /// ✅ TUN模式连接强制刷新:发送真实 HTTP 请求 /// 原理:实际的 HTTP 请求会强制建立新的 TCP 连接,让浏览器感知到网络路由变化 /// 效果:比仅做 urlTest 测试更有效,因为这是真实的数据传输 Future _kr_forceHttpConnectionRefresh(String nodeTag) async { try { // 使用多个测试 URL,确保至少有一个能成功 final testUrls = [ 'http://connectivitycheck.gstatic.com/generate_204', // Google 连接检查 'http://www.google.com/generate_204', 'http://ipv4.icanhazip.com/', // IP 检查,会返回当前外网 IP ]; KRLogUtil.kr_i('🔗 使用 HTTP 请求强制刷新连接(节点: $nodeTag)', tag: 'SingBox'); for (int i = 0; i < testUrls.length; i++) { final httpClient = HttpClient(); httpClient.connectionTimeout = const Duration(seconds: 2); try { final url = testUrls[i]; KRLogUtil.kr_i(' 尝试 URL[$i]: $url', tag: 'SingBox'); // 发送 HTTP 请求(不使用 Dio,直接使用 HttpClient 避免代理) final request = await httpClient.getUrl(Uri.parse(url)); request.headers.set('Connection', 'close'); // 明确关闭连接 request.headers.set('User-Agent', 'BearVPN/1.0'); // 标识为 BearVPN 请求 final response = await request.close().timeout( const Duration(seconds: 2), ); // 消费响应体 final _ = await response.transform(utf8.decoder).join(); KRLogUtil.kr_i('✅ HTTP 刷新成功 ($url),状态码: ${response.statusCode}', tag: 'SingBox'); return; // 成功就返回,不需要尝试其他 URL } catch (e) { KRLogUtil.kr_w('⚠️ HTTP 刷新失败 (${testUrls[i]}): $e', tag: 'SingBox'); // 继续尝试下一个 URL if (i < testUrls.length - 1) { await Future.delayed(const Duration(milliseconds: 100)); } } finally { // ✅ 关键修复:确保在任何情况下都关闭连接,避免资源泄漏 httpClient.close(); } } KRLogUtil.kr_w('⚠️ 所有 HTTP 刷新 URL 都失败,但节点切换仍然生效', tag: 'SingBox'); } catch (e) { KRLogUtil.kr_e('❌ 连接刷新异常: $e', tag: 'SingBox'); } } 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'); } } /// 🪟 Windows: 检查当前进程是否有管理员权限(TUN 模式需要) /// 返回 true 表示有管理员权限,false 表示无权限 /// ✅ 公开方法:检查 Windows 管理员权限 /// 返回 true: 有管理员权限,可以使用 TUN 模式 /// 返回 false: 无管理员权限,只能使用规则/系统代理模式 Future kr_checkWindowsAdminPrivilege() async { return await _kr_checkWindowsAdminPrivilege(); } /// 私有方法:内部实现(带缓存) /// ✅ 改进:使用 FFI 直接调用 IsUserAnAdmin() Windows API /// ⭐ 优化: /// - 不启动新进程,无黑屏风险 /// - 直接内存操作,速度快(1-5ms vs 600ms) /// - 零资源开销 /// - Windows 官方 API,完全可靠 Future _kr_checkWindowsAdminPrivilege() async { if (!Platform.isWindows) return true; // 检查缓存是否有效 if (_cachedAdminPrivilege != null && _cachedAdminPrivilegeTime != null) { final elapsed = DateTime.now().difference(_cachedAdminPrivilegeTime!); if (elapsed < _adminPrivilegeCacheDuration) { KRLogUtil.kr_i('📦 使用缓存的管理员权限状态: ${_cachedAdminPrivilege}', tag: 'SingBox'); return _cachedAdminPrivilege!; } } try { // ✅ 方案 B:使用 FFI 直接调用 IsUserAnAdmin() Windows API // 比 Process.run('net session') 快 300 倍,且无黑屏风险 KRLogUtil.kr_i('🔍 使用 FFI 检查管理员权限(无黑屏)...', tag: 'SingBox'); final hasPrivilege = _isUserAnAdminFFI(); // 缓存结果 _cachedAdminPrivilege = hasPrivilege; _cachedAdminPrivilegeTime = DateTime.now(); if (hasPrivilege) { KRLogUtil.kr_i('✅ 检测到管理员权限', tag: 'SingBox'); } else { KRLogUtil.kr_w('⚠️ 未检测到管理员权限,TUN 模式需要管理员权限', tag: 'SingBox'); } return hasPrivilege; } catch (e) { KRLogUtil.kr_e('❌ 检查管理员权限失败: $e', tag: 'SingBox'); // 检查失败时返回 false,让用户重新以管理员身份运行 return false; } } /// 【方案 A】sing-box 进程健康监测 /// 用于检测 sing-box 是否在启动后不久崩溃 /// /// 背景:诊断日志显示 /// - start() 立即返回成功,8964 端口可连接 ✅ /// - 1.4 秒后,8964 端口突然无法连接 ❌ /// - 说明 sing-box 进程在启动后崩溃了 /// /// 监测策略: /// 1. 启动成功后,立即开始监测 /// 2. 每隔 500ms 检查一次 8964 端口 /// 3. 如果连续 3 次无法连接,说明进程崩溃了 /// 4. 但如果 _stopRequested 为 true,说明用户要求停止,不要恢复! Future _monitorSingBoxProcess() async { const checkInterval = Duration(milliseconds: 500); const maxAttempts = 10; // 监测 5 秒(10 次 * 500ms) int failureCount = 0; int checkCount = 0; try { await KRFileLogger.log('[进程监测] 🟢 开始 sing-box 进程健康监测(每 500ms 检查一次,共 10 次)'); for (int i = 0; i < maxAttempts; i++) { checkCount++; await Future.delayed(checkInterval); // 【关键】检查停止标志:如果用户要求停止,立即返回 if (_stopRequested) { await KRFileLogger.log('[进程监测] ⏭️ 检测到停止标志,监测任务立即返回'); return; } try { final socket = await Socket.connect('127.0.0.1', 8964).timeout( const Duration(milliseconds: 200), onTimeout: () => throw Exception('端口连接超时'), ); socket.destroy(); // 连接成功,重置失败计数 failureCount = 0; await KRFileLogger.log('[进程监测] ✅ 检查 #$checkCount: 8964 端口可连接(进程正常运行)'); } catch (e) { failureCount++; await KRFileLogger.log('[进程监测] ❌ 检查 #$checkCount: 8964 端口无法连接(失败 #$failureCount)'); // 如果连续 3 次失败,说明进程崩溃了 if (failureCount >= 3) { // 【关键】但首先检查停止标志! if (_stopRequested) { await KRFileLogger.log('[进程监测] ⏭️ 连接失败,但用户已要求停止,不触发恢复'); return; } await KRFileLogger.log('[进程监测] 🚨 【严重】连续 3 次无法连接,sing-box 进程已崩溃!'); await KRFileLogger.log('[进程监测] 🚨 时间:${DateTime.now()}'); await KRFileLogger.log('[进程监测] 🚨 这可能是导致后续 UI 卡顿的根本原因!'); KRLogUtil.kr_e('【严重】sing-box 进程在启动后崩溃了!端口无法连接', tag: 'ProcessMonitor'); // 【可选】触发恢复机制:尝试重启 sing-box // await _triggerSingBoxRecovery(); return; // 停止监测 } } } // 监测完成 await KRFileLogger.log('[进程监测] ✅ 监测完成:sing-box 进程在启动后 5 秒内保持稳定运行'); KRLogUtil.kr_i('✅ sing-box 进程健康检查通过', tag: 'ProcessMonitor'); } catch (e) { await KRFileLogger.log('[进程监测] ❌ 监测异常: $e'); KRLogUtil.kr_e('进程监测异常: $e', tag: 'ProcessMonitor'); } } /// 【方案 A】sing-box 进程恢复机制 /// 当检测到进程崩溃时,尝试恢复 /// /// 改进: /// 1. 先尝试优雅关闭(graceful shutdown) /// 2. 如果进程仍活着,强制杀死以立即释放端口 /// 3. 轮询确认端口已释放后再重启 Future _triggerSingBoxRecovery() async { try { await KRFileLogger.log('[进程恢复] 🟡 检测到进程崩溃,尝试恢复...'); KRLogUtil.kr_w('⚠️ sing-box 进程崩溃,尝试恢复...', tag: 'ProcessMonitor'); // 1️⃣ 先尝试优雅停止 try { await kr_singBox.stop().run(); await KRFileLogger.log('[进程恢复] ✅ 已优雅停止旧进程'); } catch (e) { await KRFileLogger.log('[进程恢复] ⚠️ 优雅停止失败(可能已经停止): $e'); } // 2️⃣ 等待进程优雅关闭 await Future.delayed(const Duration(milliseconds: 300)); // 3️⃣ 检查进程是否还活着(检查 8964 端口) bool processStillAlive = false; try { final socket = await Socket.connect('127.0.0.1', 8964).timeout( const Duration(milliseconds: 200), onTimeout: () => throw Exception('端口连接超时'), ); socket.destroy(); processStillAlive = true; await KRFileLogger.log('[进程恢复] ⚠️ 8964 端口仍在被占用,进程未完全关闭'); } catch (e) { processStillAlive = false; await KRFileLogger.log('[进程恢复] ✅ 8964 端口已释放,进程已完全关闭'); } // 4️⃣ 如果进程还活着,强制杀死(taskkill - Windows only) bool killSucceeded = true; if (processStillAlive) { await KRFileLogger.log('[进程恢复] 🔴 进程仍活着,执行强制杀死...'); try { // 【Windows 专用】使用 taskkill 强制杀死所有 sing-box 进程 // ✅ 使用 KRWindowsProcessUtil.runHidden 避免黑窗 if (Platform.isWindows) { final result = await KRWindowsProcessUtil.runHidden( 'taskkill', ['/IM', 'sing-box.exe', '/F'], ); // 【关键】检查返回码! if (result.exitCode == 0) { await KRFileLogger.log('[进程恢复] ✅ taskkill 成功(返回码: 0)'); killSucceeded = true; } else { await KRFileLogger.log('[进程恢复] ⚠️ taskkill 返回码: ${result.exitCode}(可能失败)'); // 返回码 128 = 进程未找到(可能已自动退出) // 返回码 1 = 没有权限 killSucceeded = (result.exitCode == 128); // 128 = 进程已不存在(正常) } } else { // 【非 Windows 平台】跳过 taskkill,仅依赖优雅关闭 await KRFileLogger.log('[进程恢复] ℹ️ 非 Windows 平台,跳过 taskkill,依赖优雅关闭'); killSucceeded = true; // 假设优雅关闭足够 } } catch (e) { await KRFileLogger.log('[进程恢复] ⚠️ taskkill 执行异常: $e'); killSucceeded = false; } // 5️⃣ 等待进程资源完全释放 // 如果 kill 成功,等待较短的时间;如果失败,等待较长的时间 final waitDuration = killSucceeded ? const Duration(milliseconds: 500) : const Duration(milliseconds: 1000); await Future.delayed(waitDuration); } // 6️⃣ 轮询等待端口完全释放(避免 TIME_WAIT 状态) await KRFileLogger.log('[进程恢复] 🔍 轮询验证 8964 端口是否完全释放...'); int releaseRetries = 0; const maxReleaseRetries = 20; // 最多等待 10 秒 bool portFullyReleased = false; while (releaseRetries < maxReleaseRetries) { try { // 尝试连接,如果失败说明端口已释放 final socket = await Socket.connect('127.0.0.1', 8964).timeout( const Duration(milliseconds: 100), onTimeout: () => throw Exception('端口连接超时'), ); socket.destroy(); // 仍然能连接,继续等待 releaseRetries++; final elapsedMs = releaseRetries * 500; await KRFileLogger.log('[进程恢复] ⏳ 端口仍被占用,等待中... (#$releaseRetries, 已等待 ${elapsedMs}ms)'); // 如果等待超过 5 秒且端口仍被占用,说明有其他进程占用端口(异常) if (elapsedMs > 5000 && releaseRetries % 4 == 0) { await KRFileLogger.log('[进程恢复] ⚠️ 【警告】端口占用超过 5 秒,可能有其他进程占用!'); } await Future.delayed(const Duration(milliseconds: 500)); } catch (e) { // 无法连接,端口已释放 portFullyReleased = true; final elapsedMs = releaseRetries * 500; await KRFileLogger.log('[进程恢复] ✅ 确认 8964 端口已完全释放(耗时 ${elapsedMs}ms)'); break; } } if (!portFullyReleased) { await KRFileLogger.log('[进程恢复] ⚠️ 等待端口释放超时(已等待 ${maxReleaseRetries * 500}ms),继续尝试重启...'); } // 7️⃣ 重新启动 sing-box(带重试) await KRFileLogger.log('[进程恢复] 🟡 准备重新启动 sing-box...'); int startRetries = 0; const maxStartRetries = 3; bool startSucceeded = false; while (startRetries < maxStartRetries) { try { await kr_singBox.start(_cutPath, kr_configName, false).map( (r) { KRLogUtil.kr_i('✅ sing-box 恢复启动成功', tag: 'ProcessMonitor'); return r; }, ).mapLeft((err) { KRLogUtil.kr_e('❌ sing-box 恢复启动失败: $err', tag: 'ProcessMonitor'); throw err; }).run(); startSucceeded = true; await KRFileLogger.log('[进程恢复] ✅ sing-box 进程已恢复启动(第 ${startRetries + 1} 次尝试)'); break; } catch (e) { startRetries++; await KRFileLogger.log('[进程恢复] ❌ sing-box 启动失败(第 $startRetries 次,错误:$e)'); if (startRetries < maxStartRetries) { // 启动失败,增加延迟后重试 final delayMs = 1000 + (startRetries * 1000); // 1s, 2s, 3s 递增延迟 await KRFileLogger.log('[进程恢复] ⏳ 将在 ${delayMs}ms 后进行第 ${startRetries + 1} 次尝试...'); await Future.delayed(Duration(milliseconds: delayMs)); } } } if (!startSucceeded) { await KRFileLogger.log('[进程恢复] ❌ sing-box 恢复启动失败(3 次尝试都失败)'); throw Exception('sing-box 恢复启动失败'); } // 8️⃣ 启动后等待 sing-box 真正准备好 await Future.delayed(const Duration(milliseconds: 500)); // 验证 8964 端口真的可以连接 int portCheckRetries = 0; while (portCheckRetries < 5) { try { final socket = await Socket.connect('127.0.0.1', 8964).timeout( const Duration(milliseconds: 200), onTimeout: () => throw Exception('端口连接超时'), ); socket.destroy(); await KRFileLogger.log('[进程恢复] ✅ 8964 端口已准备好,sing-box 完全就绪'); break; } catch (e) { portCheckRetries++; if (portCheckRetries < 5) { await Future.delayed(const Duration(milliseconds: 200)); } } } // 9️⃣ 重新开始监测 unawaited(_monitorSingBoxProcess()); } catch (e) { await KRFileLogger.log('[进程恢复] ❌ 进程恢复失败: $e'); KRLogUtil.kr_e('进程恢复失败: $e', tag: 'ProcessMonitor'); } } /// FFI 实现:直接调用 Windows IsUserAnAdmin() API /// 返回 true 表示有管理员权限,false 表示无权限 /// /// ✅ 优点: /// - 不创建新进程,无黑屏 /// - 直接 API 调用,超快速(1-5ms) /// - 内存操作,零 I/O 等待 /// - Windows 官方 API,完全可靠 bool _isUserAnAdminFFI() { try { // 加载 shell32.dll(包含 IsUserAnAdmin 函数) final shell32 = DynamicLibrary.open('shell32.dll'); // 定义 FFI 绑定 // IsUserAnAdmin 返回 BOOL(Uint8 in Dart),无参数 final isUserAnAdmin = shell32.lookupFunction< Uint8 Function(), int Function() >('IsUserAnAdmin'); // 调用 API 并检查返回值 // 返回值 != 0 表示是管理员,== 0 表示不是 final result = isUserAnAdmin(); return result != 0; } catch (e) { KRLogUtil.kr_e('❌ FFI 权限检查异常: $e', tag: 'SingBox'); // 异常时返回 false,安全起见假设无权限 return false; } } // ==================== macOS SOCKS5 系统代理 ==================== // 🍎 让 Telegram 等应用自动走代理(参考 Clash 实现) // 原因:libcore 只设置 HTTP/HTTPS 代理,但 Telegram 只检测 SOCKS5 代理 /// 获取当前活动的网络服务名称(Wi-Fi、Ethernet 等) Future _kr_getMacOSActiveNetworkService() async { if (!Platform.isMacOS) return null; try { // 获取默认路由的网络接口 final routeResult = await Process.run('route', ['-n', 'get', 'default']); if (routeResult.exitCode != 0) { KRLogUtil.kr_w('⚠️ 获取默认路由失败', tag: 'SingBox'); return 'Wi-Fi'; // 默认返回 Wi-Fi } final routeOutput = routeResult.stdout.toString(); final interfaceMatch = RegExp(r'interface:\s*(\S+)').firstMatch(routeOutput); if (interfaceMatch == null) { return 'Wi-Fi'; } final interfaceName = interfaceMatch.group(1); KRLogUtil.kr_i('🔍 检测到网络接口: $interfaceName', tag: 'SingBox'); // 获取网络服务列表,找到对应的服务名称 final servicesResult = await Process.run('networksetup', ['-listallhardwareports']); if (servicesResult.exitCode != 0) { return 'Wi-Fi'; } final servicesOutput = servicesResult.stdout.toString(); final lines = servicesOutput.split('\n'); String? currentService; for (final line in lines) { if (line.startsWith('Hardware Port:')) { currentService = line.replaceFirst('Hardware Port:', '').trim(); } else if (line.startsWith('Device:') && currentService != null) { final device = line.replaceFirst('Device:', '').trim(); if (device == interfaceName) { KRLogUtil.kr_i('✅ 找到网络服务: $currentService', tag: 'SingBox'); return currentService; } } } return 'Wi-Fi'; // 默认返回 Wi-Fi } catch (e) { KRLogUtil.kr_e('❌ 获取网络服务失败: $e', tag: 'SingBox'); return 'Wi-Fi'; } } /// 设置或清除 macOS SOCKS5 系统代理 /// [enable] true=设置代理, false=清除代理 Future _kr_setMacOSSocks5Proxy(bool enable) async { if (!Platform.isMacOS) return; try { final networkService = await _kr_getMacOSActiveNetworkService(); if (networkService == null) { KRLogUtil.kr_w('⚠️ 无法获取网络服务名称', tag: 'SingBox'); return; } if (enable) { // 设置 SOCKS5 代理(参考 Clash 实现) KRLogUtil.kr_i('🍎 设置 macOS SOCKS5 系统代理: $networkService → 127.0.0.1:$kr_port', tag: 'SingBox'); // 设置 SOCKS 代理服务器和端口 final setResult = await Process.run('networksetup', [ '-setsocksfirewallproxy', networkService, '127.0.0.1', kr_port.toString(), ]); if (setResult.exitCode != 0) { KRLogUtil.kr_e('❌ 设置 SOCKS5 代理失败: ${setResult.stderr}', tag: 'SingBox'); return; } // 启用 SOCKS 代理 final enableResult = await Process.run('networksetup', [ '-setsocksfirewallproxystate', networkService, 'on', ]); if (enableResult.exitCode != 0) { KRLogUtil.kr_e('❌ 启用 SOCKS5 代理失败: ${enableResult.stderr}', tag: 'SingBox'); return; } KRLogUtil.kr_i('✅ macOS SOCKS5 系统代理已设置(Telegram 等应用将自动走代理)', tag: 'SingBox'); } else { // 清除 SOCKS5 代理 KRLogUtil.kr_i('🍎 清除 macOS SOCKS5 系统代理: $networkService', tag: 'SingBox'); // 禁用 SOCKS 代理 final disableResult = await Process.run('networksetup', [ '-setsocksfirewallproxystate', networkService, 'off', ]); if (disableResult.exitCode != 0) { KRLogUtil.kr_w('⚠️ 禁用 SOCKS5 代理失败: ${disableResult.stderr}', tag: 'SingBox'); } else { KRLogUtil.kr_i('✅ macOS SOCKS5 系统代理已清除', tag: 'SingBox'); } } } catch (e) { KRLogUtil.kr_e('❌ macOS SOCKS5 代理操作异常: $e', tag: 'SingBox'); } } } // 🔧 _KRWindowsProxySnapshot 类已删除 - 改用 WinINet API 直接操作,无需快照对象