hi-client/lib/app/services/singbox_imp/kr_sing_box_imp.dart
Rust bba8acfe76 windows路径问题
(cherry picked from commit e226b8635d60a8c2fea8a99c151a5161a797aa52)
2025-10-31 00:13:42 -07:00

927 lines
34 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: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 = "hiFastVPN";
/// 通道方法
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 = true.obs;
/// 是否自动自动选择线路
final kr_isAutoOutbound = true.obs;
bool _initialized = false;
/// 连接类型
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 = [];
/// 初始化标志,防止重复初始化
bool _kr_isInitialized = false;
/// 当前混合代理端口是否就绪
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;
}
try {
if (_initialized) {
KRLogUtil.kr_i('SingBox 已经初始化,跳过重复初始化');
return;
}
_initialized = true;
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');
}
}
KRLogUtil.kr_i('✅ SingBox 初始化完成');
_kr_isInitialized = true;
} 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;
}
}
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": "udp://8.8.8.8",
"remote-dns-domain-strategy": "prefer_ipv4",
"direct-dns-address": "udp://1.1.1.1",
"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://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');
// 打印每个节点的详细配置
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');
}
kr_outbounds = outbounds;
// 只保存 outboundsMobile.buildConfig() 会添加其他配置
final 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('📄 完整配置文件内容: $mapStr', 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 {
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();
}
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');
}
}
}