656 lines
21 KiB
Dart
Executable File
656 lines
21 KiB
Dart
Executable File
import 'dart:convert';
|
||
import 'dart:io';
|
||
import 'dart:async';
|
||
|
||
import 'package:flutter/services.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';
|
||
|
||
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 = "BearVPN";
|
||
|
||
/// 通道方法
|
||
final _kr_methodChannel = const MethodChannel("com.baer.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 = true.obs;
|
||
|
||
/// 是否自动自动选择线路
|
||
final kr_isAutoOutbound = true.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;
|
||
|
||
/// 所有的出站分组
|
||
RxList<SingboxOutboundGroup> kr_allGroups = <SingboxOutboundGroup>[].obs;
|
||
|
||
/// Stream 订阅管理器
|
||
final List<StreamSubscription<dynamic>> _kr_subscriptions = [];
|
||
|
||
/// 初始化
|
||
Future<void> init() async {
|
||
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();
|
||
kr_configDics = (
|
||
baseDir: baseDir,
|
||
workingDir: workingDir!,
|
||
tempDir: tempDir,
|
||
);
|
||
KRLogUtil.kr_i('其他平台路径初始化完成');
|
||
}
|
||
|
||
KRLogUtil.kr_i('开始创建目录');
|
||
if (!kr_configDics.baseDir.existsSync()) {
|
||
await kr_configDics.baseDir.create(recursive: true);
|
||
}
|
||
if (!kr_configDics.workingDir.existsSync()) {
|
||
await kr_configDics.workingDir.create(recursive: true);
|
||
}
|
||
if (!kr_configDics.tempDir.existsSync()) {
|
||
await kr_configDics.tempDir.create(recursive: true);
|
||
}
|
||
if (!directory.existsSync()) {
|
||
await directory.create(recursive: true);
|
||
}
|
||
KRLogUtil.kr_i('目录创建完成');
|
||
|
||
KRLogUtil.kr_i('开始设置 SingBox');
|
||
await kr_singBox.setup(kr_configDics, false).map((r) {
|
||
KRLogUtil.kr_i('SingBox 设置成功');
|
||
}).mapLeft((err) {
|
||
KRLogUtil.kr_e('SingBox 设置失败: $err');
|
||
throw err;
|
||
}).run();
|
||
|
||
KRLogUtil.kr_i('开始更新 SingBox 选项');
|
||
KRLogUtil.kr_i('📋 SingBox 配置选项: ${oOption.toJson()}', tag: 'SingBox');
|
||
await kr_singBox.changeOptions(oOption)
|
||
..map((r) {
|
||
KRLogUtil.kr_i('✅ SingBox 选项更新成功', tag: 'SingBox');
|
||
}).mapLeft((err) {
|
||
KRLogUtil.kr_e('❌ SingBox 选项更新失败: $err', tag: 'SingBox');
|
||
throw err;
|
||
}).run();
|
||
|
||
KRLogUtil.kr_i('开始监听状态');
|
||
// 初始订阅状态流
|
||
kr_singBox.watchStatus().listen((status) {
|
||
KRLogUtil.kr_i('🔄 SingBox 状态变化: $status', tag: 'SingBox');
|
||
KRLogUtil.kr_i('📊 状态类型: ${status.runtimeType}', tag: 'SingBox');
|
||
|
||
// 确保状态更新
|
||
kr_status.value = status;
|
||
|
||
switch (status) {
|
||
case SingboxStopped():
|
||
KRLogUtil.kr_i('🔴 SingBox 已停止', tag: 'SingBox');
|
||
break;
|
||
case SingboxStarting():
|
||
KRLogUtil.kr_i('🟡 SingBox 正在启动', tag: 'SingBox');
|
||
break;
|
||
case SingboxStarted():
|
||
KRLogUtil.kr_i('🟢 SingBox 已启动', tag: 'SingBox');
|
||
kr_isFristStart.value = true;
|
||
// 使用 GetX 的方式处理 Stream 订阅
|
||
_kr_subscribeToStats();
|
||
_kr_subscribeToGroups();
|
||
// 强制触发状态更新
|
||
kr_status.refresh();
|
||
break;
|
||
case SingboxStopping():
|
||
KRLogUtil.kr_i('🟠 SingBox 正在停止', tag: 'SingBox');
|
||
break;
|
||
}
|
||
});
|
||
|
||
KRLogUtil.kr_i('SingBox 初始化完成');
|
||
} catch (e, stackTrace) {
|
||
KRLogUtil.kr_e('SingBox 初始化失败: $e');
|
||
KRLogUtil.kr_e('错误堆栈: $stackTrace');
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
Map<String, dynamic> _getConfigOption() {
|
||
if (kr_configOption.isNotEmpty) {
|
||
return kr_configOption;
|
||
}
|
||
final op = {
|
||
"region": KRCountryUtil.kr_getCurrentCountryCode(),
|
||
"block-ads": kr_blockAds.value,
|
||
"use-xray-core-when-possible": false,
|
||
"execute-config-as-is": false,
|
||
"log-level": "warn",
|
||
"resolve-destination": false,
|
||
"ipv6-mode": "ipv4_only",
|
||
// "remote-dns-address": "https://cloudflare-dns.com/dns-query",
|
||
"remote-dns-address": "udp://1.1.1.1",
|
||
"remote-dns-domain-strategy": "",
|
||
"direct-dns-address": "223.5.5.5",
|
||
"direct-dns-domain-strategy": "",
|
||
"mixed-port": kr_port,
|
||
"tproxy-port": kr_port,
|
||
"local-dns-port": 36450,
|
||
"tun-implementation": "gvisor",
|
||
"mtu": 9000,
|
||
"strict-route": true,
|
||
// "connection-test-url": "http://www.cloudflare.com",
|
||
"connection-test-url": "http://www.gstatic.com/generate_204",
|
||
"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": [],
|
||
"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;
|
||
return op;
|
||
}
|
||
|
||
/// 订阅统计数据流
|
||
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'));
|
||
|
||
_kr_subscriptions.add(
|
||
kr_singBox.watchStats().listen(
|
||
(stats) {
|
||
kr_stats.value = stats;
|
||
},
|
||
onError: (error) {
|
||
KRLogUtil.kr_e('统计数据监听错误: $error');
|
||
},
|
||
cancelOnError: false,
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 订阅分组数据流
|
||
void _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) {
|
||
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) {
|
||
KRLogUtil.kr_e('❌ 活动分组监听错误: $error', tag: 'SingBox');
|
||
},
|
||
cancelOnError: false,
|
||
),
|
||
);
|
||
|
||
_kr_subscriptions.add(
|
||
kr_singBox.watchGroups().listen(
|
||
(groups) {
|
||
kr_allGroups.value = groups;
|
||
},
|
||
onError: (error) {
|
||
KRLogUtil.kr_e('所有分组监听错误: $error');
|
||
},
|
||
cancelOnError: false,
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 监听活动组的详细实现
|
||
// 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');
|
||
|
||
kr_outbounds = outbounds;
|
||
|
||
final map = {};
|
||
map["outbounds"] = kr_outbounds;
|
||
|
||
final file = _file(kr_configName);
|
||
final temp = _tempFile(kr_configName);
|
||
final mapStr = jsonEncode(map);
|
||
|
||
KRLogUtil.kr_i('📄 配置文件内容长度: ${mapStr.length}', tag: 'SingBox');
|
||
KRLogUtil.kr_i('📄 配置文件前500字符: ${mapStr.substring(0, mapStr.length > 500 ? 500 : 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');
|
||
}
|
||
|
||
Future<void> kr_start() async {
|
||
kr_status.value = SingboxStarting();
|
||
try {
|
||
KRLogUtil.kr_i('🚀 开始启动 SingBox...', tag: 'SingBox');
|
||
KRLogUtil.kr_i('📁 配置文件路径: $_cutPath', tag: 'SingBox');
|
||
KRLogUtil.kr_i('📝 配置名称: $kr_configName', 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');
|
||
}
|
||
|
||
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');
|
||
// 确保状态重置为Stopped,触发UI更新
|
||
kr_status.value = SingboxStopped();
|
||
// 强制刷新状态以触发观察者
|
||
kr_status.refresh();
|
||
throw err;
|
||
}).run();
|
||
} catch (e, stackTrace) {
|
||
KRLogUtil.kr_e('💥 SingBox 启动异常: $e', tag: 'SingBox');
|
||
KRLogUtil.kr_e('📚 错误堆栈: $stackTrace', tag: 'SingBox');
|
||
// 确保状态重置为Stopped,触发UI更新
|
||
kr_status.value = SingboxStopped();
|
||
// 强制刷新状态以触发观察者
|
||
kr_status.refresh();
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
/// 停止服务
|
||
Future<void> kr_stop() async {
|
||
try {
|
||
// 不主动赋值 kr_status
|
||
await Future.delayed(const Duration(milliseconds: 100));
|
||
await kr_singBox.stop().run();
|
||
await Future.delayed(const Duration(milliseconds: 1000));
|
||
// 取消订阅
|
||
final subscriptions = List<StreamSubscription<dynamic>>.from(_kr_subscriptions);
|
||
_kr_subscriptions.clear();
|
||
for (var subscription in subscriptions) {
|
||
try {
|
||
await subscription.cancel();
|
||
} catch (e) {
|
||
KRLogUtil.kr_e('取消订阅时出错: $e');
|
||
}
|
||
}
|
||
// 不主动赋值 kr_status
|
||
} catch (e, stackTrace) {
|
||
KRLogUtil.kr_e('停止服务时出错: $e');
|
||
KRLogUtil.kr_e('错误堆栈: $stackTrace');
|
||
// 兜底,防止状态卡死
|
||
kr_status.value = SingboxStopped();
|
||
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("restart");
|
||
kr_singBox.restart(_cutPath, kr_configName, false).mapLeft((err) {
|
||
KRLogUtil.kr_e('重启失败: $err');
|
||
}).run();
|
||
}
|
||
|
||
//// 设置出站模式
|
||
Future<void> kr_updateConnectionType(KRConnectionType newType) async {
|
||
if (kr_connectionType.value == newType) {
|
||
return;
|
||
}
|
||
|
||
kr_connectionType.value = newType;
|
||
|
||
final oOption = _getConfigOption();
|
||
|
||
var mode = "";
|
||
switch (newType) {
|
||
case KRConnectionType.global:
|
||
mode = "other";
|
||
break;
|
||
case KRConnectionType.rule:
|
||
mode = KRCountryUtil.kr_getCurrentCountryCode();
|
||
break;
|
||
// case KRConnectionType.direct:
|
||
// mode = "direct";
|
||
// break;
|
||
}
|
||
oOption["region"] = mode;
|
||
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();
|
||
}
|
||
}
|
||
|
||
/// 更新国家设置
|
||
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();
|
||
}
|
||
|
||
void kr_selectOutbound(String tag) {
|
||
KRLogUtil.kr_i('🎯 开始选择出站节点: $tag', 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');
|
||
}
|
||
}
|
||
kr_singBox.selectOutbound("select", tag).run();
|
||
}
|
||
|
||
/// 配合文件地址
|
||
|
||
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');
|
||
}
|
||
}
|
||
}
|