hi-client/lib/app/services/singbox_imp/kr_sing_box_imp.dart
2025-12-01 06:54:18 -08:00

2066 lines
77 KiB
Dart
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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