修改flutter兼容版本,优化macos无法切换节点和协议不兼容等问题
(cherry picked from commit dcc07886f8ba73eb2630a14a81bda191468c7a1f)
This commit is contained in:
parent
8bba2441c2
commit
064a0a7402
4
.gitignore
vendored
4
.gitignore
vendored
@ -58,7 +58,6 @@ app.*.map.json
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
|
||||
/data
|
||||
/.gradle/
|
||||
|
||||
@ -99,3 +98,6 @@ libcore/*.aar
|
||||
|
||||
# Android 编译产物
|
||||
android/app/libs/*.aar
|
||||
|
||||
# FVM Version Cache
|
||||
.fvm/
|
||||
@ -138,7 +138,9 @@ class KROutboundItem {
|
||||
"password": nodeListItem.uuid
|
||||
};
|
||||
break;
|
||||
case "hysteria":
|
||||
case "hysteria2":
|
||||
// 后端的 "hysteria" 实际上是 Hysteria2 协议
|
||||
final securityConfig =
|
||||
json["security_config"] as Map<String, dynamic>? ?? {};
|
||||
config = {
|
||||
@ -156,8 +158,7 @@ class KROutboundItem {
|
||||
"tls": {
|
||||
"enabled": true,
|
||||
"server_name": securityConfig["sni"] ?? "",
|
||||
"insecure": securityConfig["allow_insecure"] ?? true,
|
||||
"alpn": ["h3"]
|
||||
"insecure": securityConfig["allow_insecure"] ?? true
|
||||
}
|
||||
};
|
||||
break;
|
||||
@ -260,6 +261,10 @@ class KROutboundItem {
|
||||
print('📄 完整配置: $config');
|
||||
break;
|
||||
case "vless":
|
||||
// 判断是否为域名(非IP地址)
|
||||
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||
.hasMatch(nodeListItem.serverAddr);
|
||||
|
||||
config = {
|
||||
"type": "vless",
|
||||
"tag": nodeListItem.name,
|
||||
@ -268,7 +273,7 @@ class KROutboundItem {
|
||||
"uuid": nodeListItem.uuid,
|
||||
"tls": {
|
||||
"enabled": true,
|
||||
"server_name": nodeListItem.serverAddr,
|
||||
if (isDomain) "server_name": nodeListItem.serverAddr,
|
||||
"insecure": true,
|
||||
"utls": {
|
||||
"enabled": true,
|
||||
@ -280,6 +285,10 @@ class KROutboundItem {
|
||||
print('📄 完整配置: $config');
|
||||
break;
|
||||
case "vmess":
|
||||
// 判断是否为域名(非IP地址)
|
||||
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||
.hasMatch(nodeListItem.serverAddr);
|
||||
|
||||
config = {
|
||||
"type": "vmess",
|
||||
"tag": nodeListItem.name,
|
||||
@ -290,7 +299,7 @@ class KROutboundItem {
|
||||
"security": "auto",
|
||||
"tls": {
|
||||
"enabled": true,
|
||||
"server_name": nodeListItem.serverAddr,
|
||||
if (isDomain) "server_name": nodeListItem.serverAddr,
|
||||
"insecure": true,
|
||||
"utls": {"enabled": true, "fingerprint": "chrome"}
|
||||
}
|
||||
@ -299,6 +308,10 @@ class KROutboundItem {
|
||||
print('📄 完整配置: $config');
|
||||
break;
|
||||
case "trojan":
|
||||
// 判断是否为域名(非IP地址)
|
||||
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||
.hasMatch(nodeListItem.serverAddr);
|
||||
|
||||
config = {
|
||||
"type": "trojan",
|
||||
"tag": nodeListItem.name,
|
||||
@ -307,7 +320,7 @@ class KROutboundItem {
|
||||
"password": nodeListItem.uuid,
|
||||
"tls": {
|
||||
"enabled": true,
|
||||
"server_name": nodeListItem.serverAddr,
|
||||
if (isDomain) "server_name": nodeListItem.serverAddr,
|
||||
"insecure": true,
|
||||
"utls": {"enabled": true, "fingerprint": "chrome"}
|
||||
}
|
||||
@ -316,42 +329,30 @@ class KROutboundItem {
|
||||
print('📄 完整配置: $config');
|
||||
break;
|
||||
case "hysteria":
|
||||
config = {
|
||||
"type": "hysteria",
|
||||
"tag": nodeListItem.name,
|
||||
"server": nodeListItem.serverAddr,
|
||||
"server_port": nodeListItem.port,
|
||||
"up_mbps": 100,
|
||||
"down_mbps": 100,
|
||||
"auth_str": nodeListItem.uuid,
|
||||
"tls": {
|
||||
"enabled": true,
|
||||
"server_name": nodeListItem.serverAddr,
|
||||
"insecure": true,
|
||||
"alpn": ["h3"]
|
||||
}
|
||||
};
|
||||
print('✅ Hysteria 节点配置构建成功: ${nodeListItem.name}');
|
||||
print('📄 完整配置: $config');
|
||||
break;
|
||||
case "hysteria2":
|
||||
// 后端的 "hysteria" 实际上是 Hysteria2 协议
|
||||
print('🔍 构建 Hysteria2 节点: ${nodeListItem.name}');
|
||||
print(' - serverAddr: ${nodeListItem.serverAddr}');
|
||||
print(' - port: ${nodeListItem.port}');
|
||||
print(' - uuid: ${nodeListItem.uuid}');
|
||||
|
||||
//判断是否为域名
|
||||
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||
.hasMatch(nodeListItem.serverAddr);
|
||||
|
||||
config = {
|
||||
"type": "hysteria2",
|
||||
"tag": nodeListItem.name,
|
||||
"server": nodeListItem.serverAddr,
|
||||
"server_port": nodeListItem.port,
|
||||
"password": nodeListItem.uuid,
|
||||
"up_mbps": 100,
|
||||
"down_mbps": 100,
|
||||
"tls": {
|
||||
"enabled": true,
|
||||
"server_name": nodeListItem.serverAddr,
|
||||
"insecure": true,
|
||||
"alpn": ["h3"]
|
||||
if (isDomain) "server_name": nodeListItem.serverAddr,
|
||||
}
|
||||
};
|
||||
print('✅ Hysteria2 节点配置构建成功: ${nodeListItem.name}');
|
||||
print('📄 完整配置: $config');
|
||||
print('✅ Hysteria2 节点配置构建成功');
|
||||
print('📄 完整配置: ${jsonEncode(config)}');
|
||||
break;
|
||||
default:
|
||||
print('⚠️ 不支持的协议类型: ${nodeListItem.protocol}');
|
||||
|
||||
@ -665,6 +665,8 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
|
||||
try {
|
||||
kr_isSwitching = true;
|
||||
KRLogUtil.kr_i('🔄 开始切换连接状态: $value', tag: 'HomeController');
|
||||
|
||||
if (value) {
|
||||
await KRSingBoxImp.instance.kr_start();
|
||||
|
||||
@ -678,16 +680,28 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
kr_forceSyncConnectionStatus();
|
||||
});
|
||||
} else {
|
||||
await KRSingBoxImp.instance.kr_stop();
|
||||
KRLogUtil.kr_i('🛑 准备停止连接...', tag: 'HomeController');
|
||||
// 添加超时保护
|
||||
await KRSingBoxImp.instance.kr_stop().timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () {
|
||||
KRLogUtil.kr_e('⚠️ 停止操作超时', tag: 'HomeController');
|
||||
// 强制同步状态
|
||||
kr_forceSyncConnectionStatus();
|
||||
throw TimeoutException('Stop operation timeout');
|
||||
},
|
||||
);
|
||||
KRLogUtil.kr_i('✅ 停止命令已发送', tag: 'HomeController');
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('切换失败: $e', tag: 'HomeController');
|
||||
// 当启动失败时(如VPN权限被拒绝),强制同步状态
|
||||
// 当启动或停止失败时,强制同步状态
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
kr_forceSyncConnectionStatus();
|
||||
});
|
||||
} finally {
|
||||
// 确保在任何情况下都会重置标志
|
||||
KRLogUtil.kr_i('🔓 重置切换标志', tag: 'HomeController');
|
||||
kr_isSwitching = false;
|
||||
}
|
||||
}
|
||||
@ -1088,10 +1102,11 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
// 更新连接信息
|
||||
kr_updateConnectionInfo();
|
||||
|
||||
// 🔧 修复:只有在核心已启动时才选择节点,避免触发重启
|
||||
if (KRSingBoxImp.instance.kr_status.value == SingboxStarted()) {
|
||||
KRSingBoxImp.instance.kr_selectOutbound(tag);
|
||||
|
||||
// 🔧 修复:选择节点后启动延迟值更新(带超时保护)
|
||||
// 🔧 修复:选择节点后启动延迟值更新(带超时保护)
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (kr_currentNodeLatency.value == -1 && kr_isConnected.value) {
|
||||
KRLogUtil.kr_w('⚠️ 选择节点后延迟值未更新,尝试手动更新', tag: 'HomeController');
|
||||
@ -1101,7 +1116,13 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
KRSingBoxImp().kr_start();
|
||||
// 🔧 修复:核心未启动时,仍需保存用户选择,以便启动VPN时应用
|
||||
KRLogUtil.kr_i('💾 核心未启动,保存节点选择以便稍后应用: $tag', tag: 'HomeController');
|
||||
KRSecureStorage().kr_saveData(key: 'SELECTED_NODE_TAG', value: tag).then((_) {
|
||||
KRLogUtil.kr_i('✅ 节点选择已保存: $tag', tag: 'HomeController');
|
||||
}).catchError((e) {
|
||||
KRLogUtil.kr_e('❌ 保存节点选择失败: $e', tag: 'HomeController');
|
||||
});
|
||||
}
|
||||
|
||||
// 移动到选中的节点
|
||||
|
||||
@ -3,6 +3,7 @@ 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';
|
||||
@ -19,6 +20,7 @@ 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';
|
||||
|
||||
enum KRConnectionType {
|
||||
global,
|
||||
@ -45,6 +47,9 @@ class KRSingBoxImp {
|
||||
/// 配置文件名称
|
||||
String kr_configName = "hiFastVPN";
|
||||
|
||||
/// 存储键:用户选择的节点
|
||||
static const String _keySelectedNode = 'SELECTED_NODE_TAG';
|
||||
|
||||
/// 通道方法
|
||||
final _kr_methodChannel = const MethodChannel("com.hi.app/platform");
|
||||
|
||||
@ -65,10 +70,10 @@ class KRSingBoxImp {
|
||||
final kr_status = SingboxStatus.stopped().obs;
|
||||
|
||||
/// 拦截广告
|
||||
final kr_blockAds = true.obs;
|
||||
final kr_blockAds = false.obs;
|
||||
|
||||
/// 是否自动自动选择线路
|
||||
final kr_isAutoOutbound = true.obs;
|
||||
final kr_isAutoOutbound = false.obs;
|
||||
|
||||
bool _initialized = false;
|
||||
|
||||
@ -396,8 +401,27 @@ class KRSingBoxImp {
|
||||
}
|
||||
}
|
||||
|
||||
// 🔑 关键步骤:调用 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 初始化完成');
|
||||
_kr_isInitialized = true;
|
||||
|
||||
// 🔑 关键:在初始化完成后立即订阅状态变化流
|
||||
// 这样可以确保 UI 始终与 libcore 的实际状态同步
|
||||
_kr_subscribeToStatus();
|
||||
KRLogUtil.kr_i('✅ 状态订阅已设置', tag: 'SingBox');
|
||||
} catch (e, stackTrace) {
|
||||
KRLogUtil.kr_e('❌ SingBox 初始化失败: $e');
|
||||
KRLogUtil.kr_e('📚 错误堆栈: $stackTrace');
|
||||
@ -438,20 +462,21 @@ class KRSingBoxImp {
|
||||
}
|
||||
|
||||
Map<String, dynamic> _getConfigOption() {
|
||||
if (kr_configOption.isNotEmpty) {
|
||||
return kr_configOption;
|
||||
}
|
||||
// 不使用缓存,每次都重新生成配置
|
||||
// if (kr_configOption.isNotEmpty) {
|
||||
// return kr_configOption;
|
||||
// }
|
||||
final op = {
|
||||
"region": KRCountryUtil.kr_getCurrentCountryCode(),
|
||||
"block-ads": kr_blockAds.value,
|
||||
"region": "other", // 参考 hiddify-app: 默认使用 "other" 跳过规则集下载
|
||||
"block-ads": false, // 参考 hiddify-app: 默认关闭广告拦截
|
||||
"use-xray-core-when-possible": false,
|
||||
"execute-config-as-is": false,
|
||||
"log-level": "warn",
|
||||
"log-level": "info", // 调试阶段使用 info,生产环境改为 warn
|
||||
"resolve-destination": false,
|
||||
"ipv6-mode": "ipv4_only",
|
||||
"remote-dns-address": "udp://8.8.8.8",
|
||||
"ipv6-mode": "ipv4_only", // 参考 hiddify-app: 仅使用 IPv4 (有效值: ipv4_only, prefer_ipv4, prefer_ipv6, ipv6_only)
|
||||
"remote-dns-address": "udp://1.1.1.1", // 参考 hiddify-app: 使用 Cloudflare DNS
|
||||
"remote-dns-domain-strategy": "prefer_ipv4",
|
||||
"direct-dns-address": "udp://1.1.1.1",
|
||||
"direct-dns-address": "udp://1.1.1.1", // 参考 hiddify-app: 统一使用 1.1.1.1
|
||||
"direct-dns-domain-strategy": "prefer_ipv4",
|
||||
"mixed-port": kr_port,
|
||||
"tproxy-port": kr_port,
|
||||
@ -459,8 +484,7 @@ class KRSingBoxImp {
|
||||
"tun-implementation": "gvisor",
|
||||
"mtu": 9000,
|
||||
"strict-route": true,
|
||||
// "connection-test-url": "http://www.cloudflare.com",
|
||||
"connection-test-url": "http://www.gstatic.com/generate_204",
|
||||
"connection-test-url": "http://cp.cloudflare.com", // 参考 hiddify-app: 使用 Cloudflare 测试端点
|
||||
"url-test-interval": 30,
|
||||
"enable-clash-api": true,
|
||||
"clash-api-port": 36756,
|
||||
@ -521,6 +545,32 @@ class KRSingBoxImp {
|
||||
return op;
|
||||
}
|
||||
|
||||
/// 订阅状态变化流
|
||||
/// 参考 hiddify-app: 监听 libcore 发送的状态事件来自动更新 UI
|
||||
void _kr_subscribeToStatus() {
|
||||
// 取消之前的状态订阅
|
||||
for (var sub in _kr_subscriptions) {
|
||||
if (sub.hashCode.toString().contains('Status')) {
|
||||
sub.cancel();
|
||||
}
|
||||
}
|
||||
_kr_subscriptions
|
||||
.removeWhere((sub) => sub.hashCode.toString().contains('Status'));
|
||||
|
||||
_kr_subscriptions.add(
|
||||
kr_singBox.watchStatus().listen(
|
||||
(status) {
|
||||
KRLogUtil.kr_i('📡 收到状态更新: $status', tag: 'SingBox');
|
||||
kr_status.value = status;
|
||||
},
|
||||
onError: (error) {
|
||||
KRLogUtil.kr_e('📡 状态流错误: $error', tag: 'SingBox');
|
||||
},
|
||||
cancelOnError: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 订阅统计数据流
|
||||
void _kr_subscribeToStats() {
|
||||
// 取消之前的统计订阅
|
||||
@ -532,17 +582,20 @@ class KRSingBoxImp {
|
||||
_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,
|
||||
),
|
||||
// ⚠️ 关键: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);
|
||||
}
|
||||
|
||||
/// 订阅分组数据流
|
||||
@ -594,6 +647,261 @@ class KRSingBoxImp {
|
||||
);
|
||||
}
|
||||
|
||||
/// 带重试机制的节点选择
|
||||
///
|
||||
/// 确保 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 {
|
||||
@ -643,7 +951,17 @@ class KRSingBoxImp {
|
||||
KRLogUtil.kr_i(' - 完整配置: ${jsonEncode(outbound)}', tag: 'SingBox');
|
||||
}
|
||||
|
||||
kr_outbounds = outbounds;
|
||||
// ⚠️ 临时过滤 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');
|
||||
|
||||
// 只保存 outbounds,Mobile.buildConfig() 会添加其他配置
|
||||
final map = {
|
||||
@ -673,12 +991,39 @@ class KRSingBoxImp {
|
||||
}
|
||||
|
||||
Future<void> kr_start() async {
|
||||
kr_status.value = SingboxStarting();
|
||||
// 不再手动设置状态,libcore 会通过 status stream 自动发送状态更新
|
||||
try {
|
||||
KRLogUtil.kr_i('🚀 开始启动 SingBox...', tag: 'SingBox');
|
||||
// ⚠️ 强制编译标记 - v2.0-lazy-load
|
||||
KRLogUtil.kr_i('🚀🚀🚀 [v2.0-lazy-load] 开始启动 SingBox...', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📁 配置文件路径: $_cutPath', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('📝 配置名称: $kr_configName', 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()) {
|
||||
@ -695,19 +1040,26 @@ class KRSingBoxImp {
|
||||
},
|
||||
).mapLeft((err) {
|
||||
KRLogUtil.kr_e('❌ SingBox 启动失败: $err', tag: 'SingBox');
|
||||
// 确保状态重置为Stopped,触发UI更新
|
||||
kr_status.value = SingboxStopped();
|
||||
// 强制刷新状态以触发观察者
|
||||
kr_status.refresh();
|
||||
// 不需要手动设置状态,libcore 会通过 status stream 自动发送 Stopped 事件
|
||||
throw err;
|
||||
}).run();
|
||||
|
||||
// ⚠️ 借鉴 Hiddify 架构:不在 start() 时立即订阅 command client
|
||||
// 原因:
|
||||
// 1. 避免阻塞 UI 线程 - startCommandClient() 是同步 FFI 调用
|
||||
// 2. watchStats/watchGroups 是懒加载的,首次调用时自动初始化
|
||||
// 3. 如果 command.sock 未就绪,会在后台自动抛出异常,不影响启动流程
|
||||
|
||||
KRLogUtil.kr_i('✅ SingBox 核心已启动,command client 将延迟初始化', tag: 'SingBox');
|
||||
KRLogUtil.kr_i('💡 watchStats/watchGroups 会在首次调用时自动连接', tag: 'SingBox');
|
||||
|
||||
// ⚠️ 完全不主动订阅,避免阻塞 UI 线程
|
||||
// 借鉴 Hiddify:所有订阅由 UI 被动触发(懒加载)
|
||||
KRLogUtil.kr_i('⚠️ 不再主动订阅 command client,完全由 UI 触发', tag: 'SingBox');
|
||||
} 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();
|
||||
// 不需要手动设置状态,libcore 会自动处理
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@ -715,26 +1067,53 @@ class KRSingBoxImp {
|
||||
/// 停止服务
|
||||
Future<void> kr_stop() async {
|
||||
try {
|
||||
// 不主动赋值 kr_status
|
||||
KRLogUtil.kr_i('🛑 停止 SingBox 服务...', tag: 'SingBox');
|
||||
|
||||
// 取消节点选择监控定时器
|
||||
_nodeSelectionTimer?.cancel();
|
||||
_nodeSelectionTimer = null;
|
||||
KRLogUtil.kr_i('✅ 节点选择监控已停止', tag: 'SingBox');
|
||||
|
||||
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) {
|
||||
|
||||
// 添加超时保护,防止 stop() 调用阻塞
|
||||
// 缩短超时时间,避免用户等待过久
|
||||
try {
|
||||
await kr_singBox.stop().run().timeout(
|
||||
const Duration(seconds: 3),
|
||||
onTimeout: () {
|
||||
KRLogUtil.kr_w('⚠️ 停止操作超时(3秒),强制继续', tag: 'SingBox');
|
||||
return const Left('timeout');
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('⚠️ 停止操作失败(可能已经停止): $e', tag: 'SingBox');
|
||||
// 继续执行清理操作
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
// 不主动赋值 kr_status
|
||||
|
||||
// 不手动设置状态,由 libcore 通过 status stream 自动发送 Stopped 事件
|
||||
KRLogUtil.kr_i('✅ SingBox 停止请求已发送', tag: 'SingBox');
|
||||
} catch (e, stackTrace) {
|
||||
KRLogUtil.kr_e('停止服务时出错: $e');
|
||||
KRLogUtil.kr_e('错误堆栈: $stackTrace');
|
||||
// 兜底,防止状态卡死
|
||||
kr_status.value = SingboxStopped();
|
||||
// 不手动设置状态,信任 libcore 的状态管理
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@ -859,20 +1238,52 @@ class KRSingBoxImp {
|
||||
return kr_singBox.watchGroups();
|
||||
}
|
||||
|
||||
void kr_selectOutbound(String tag) {
|
||||
KRLogUtil.kr_i('🎯 开始选择出站节点: $tag', tag: 'SingBox');
|
||||
// 节点选择监控定时器
|
||||
Timer? _nodeSelectionTimer;
|
||||
|
||||
void kr_selectOutbound(String tag) async {
|
||||
KRLogUtil.kr_i('🎯 [v2.1] 开始选择出站节点: $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');
|
||||
}
|
||||
// 持久化用户选择的节点(异步执行,不阻塞UI)
|
||||
KRSecureStorage().kr_saveData(key: _keySelectedNode, value: tag).then((_) {
|
||||
KRLogUtil.kr_i('✅ 节点选择已保存: $tag', tag: 'SingBox');
|
||||
}).catchError((e) {
|
||||
KRLogUtil.kr_e('❌ 保存节点选择失败: $e', tag: 'SingBox');
|
||||
});
|
||||
|
||||
// ⚠️ 关键修复:确保 command client 已初始化
|
||||
// 借鉴 Hiddify:通过触发 watchGroups() 来初始化 command client
|
||||
_kr_ensureCommandClientInitialized().then((_) {
|
||||
KRLogUtil.kr_i('✅ Command client 已就绪,执行节点切换', tag: 'SingBox');
|
||||
|
||||
// 🔄 使用重试机制执行节点选择(异步执行,不阻塞UI)
|
||||
_kr_selectOutboundWithRetry("select", tag, maxAttempts: 3, initialDelay: 50).catchError((e) {
|
||||
KRLogUtil.kr_e('❌ 节点选择失败: $e', tag: 'SingBox');
|
||||
});
|
||||
}).catchError((e) {
|
||||
KRLogUtil.kr_e('❌ Command client 初始化失败,节点切换取消: $e', tag: 'SingBox');
|
||||
});
|
||||
|
||||
// 🔄 如果用户选择了具体节点(不是 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');
|
||||
});
|
||||
}
|
||||
kr_singBox.selectOutbound("select", tag).run();
|
||||
}
|
||||
|
||||
/// 配合文件地址
|
||||
|
||||
@ -3,13 +3,11 @@ import 'dart:convert';
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
// import 'package:combine/combine.dart'; // 暂时注释掉,使用 Isolate.run 替代
|
||||
import 'package:combine/combine.dart';
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:kaer_with_panels/core/model/directories.dart';
|
||||
import 'package:kaer_with_panels/gen/singbox_generated_bindings.dart';
|
||||
|
||||
import 'package:kaer_with_panels/singbox/model/singbox_config_option.dart';
|
||||
import 'package:kaer_with_panels/singbox/model/singbox_outbound.dart';
|
||||
import 'package:kaer_with_panels/singbox/model/singbox_stats.dart';
|
||||
@ -31,9 +29,6 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
late final ReceivePort _statusReceiver;
|
||||
Stream<SingboxStats>? _serviceStatsStream;
|
||||
Stream<List<SingboxOutboundGroup>>? _outboundsStream;
|
||||
|
||||
/// 初始化标志,防止重复初始化
|
||||
bool _isInitialized = false;
|
||||
|
||||
static SingboxNativeLibrary _gen() {
|
||||
String fullPath = "";
|
||||
@ -53,26 +48,14 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
Future<void> init() async {
|
||||
// 防止重复初始化
|
||||
if (_isInitialized) {
|
||||
loggy.debug("already initialized, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
loggy.debug("initializing");
|
||||
_statusReceiver = ReceivePort('service status receiver');
|
||||
final source = _statusReceiver
|
||||
.asBroadcastStream()
|
||||
.map((event) => jsonDecode(event as String))
|
||||
.map(SingboxStatus.fromEvent);
|
||||
final source = _statusReceiver.asBroadcastStream().map((event) => jsonDecode(event as String)).map(SingboxStatus.fromEvent);
|
||||
_status = ValueConnectableStream.seeded(
|
||||
source,
|
||||
const SingboxStopped(),
|
||||
).autoConnect();
|
||||
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -82,7 +65,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
) {
|
||||
final port = _statusReceiver.sendPort.nativePort;
|
||||
return TaskEither(
|
||||
() => Isolate.run(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
_box.setupOnce(NativeApi.initializeApiDLData);
|
||||
final err = _box
|
||||
@ -111,7 +94,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
bool debug,
|
||||
) {
|
||||
return TaskEither(
|
||||
() => Isolate.run(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final err = _box
|
||||
.parse(
|
||||
@ -133,13 +116,10 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
@override
|
||||
TaskEither<String, Unit> changeOptions(SingboxConfigOption options) {
|
||||
return TaskEither(
|
||||
() => Isolate.run(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final json = jsonEncode(options.toJson());
|
||||
final err = _box
|
||||
.changeHiddifyOptions(json.toNativeUtf8().cast())
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
final err = _box.changeHiddifyOptions(json.toNativeUtf8().cast()).cast<Utf8>().toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
return left(err);
|
||||
}
|
||||
@ -154,7 +134,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
String path,
|
||||
) {
|
||||
return TaskEither(
|
||||
() => Isolate.run(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final response = _box
|
||||
.generateConfig(
|
||||
@ -179,7 +159,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
) {
|
||||
loggy.debug("starting, memory limit: [${!disableMemoryLimit}]");
|
||||
return TaskEither(
|
||||
() => Isolate.run(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final err = _box
|
||||
.start(
|
||||
@ -200,7 +180,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
@override
|
||||
TaskEither<String, Unit> stop() {
|
||||
return TaskEither(
|
||||
() => Isolate.run(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final err = _box.stop().cast<Utf8>().toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
@ -220,7 +200,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
) {
|
||||
loggy.debug("restarting, memory limit: [${!disableMemoryLimit}]");
|
||||
return TaskEither(
|
||||
() => Isolate.run(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final err = _box
|
||||
.restart(
|
||||
@ -278,10 +258,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
},
|
||||
);
|
||||
|
||||
final err = _box
|
||||
.startCommandClient(1, receiver.sendPort.nativePort)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
final err = _box.startCommandClient(1, receiver.sendPort.nativePort).cast<Utf8>().toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
loggy.error("error starting status command: $err");
|
||||
throw err;
|
||||
@ -323,10 +300,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
);
|
||||
|
||||
try {
|
||||
final err = _box
|
||||
.startCommandClient(5, receiver.sendPort.nativePort)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
final err = _box.startCommandClient(5, receiver.sendPort.nativePort).cast<Utf8>().toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
logger.error("error starting group command: $err");
|
||||
throw err;
|
||||
@ -370,10 +344,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
);
|
||||
|
||||
try {
|
||||
final err = _box
|
||||
.startCommandClient(13, receiver.sendPort.nativePort)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
final err = _box.startCommandClient(13, receiver.sendPort.nativePort).cast<Utf8>().toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
logger.error("error starting: $err");
|
||||
throw err;
|
||||
@ -389,7 +360,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
@override
|
||||
TaskEither<String, Unit> selectOutbound(String groupTag, String outboundTag) {
|
||||
return TaskEither(
|
||||
() => Isolate.run(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final err = _box
|
||||
.selectOutbound(
|
||||
@ -410,12 +381,9 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
@override
|
||||
TaskEither<String, Unit> urlTest(String groupTag) {
|
||||
return TaskEither(
|
||||
() => Isolate.run(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final err = _box
|
||||
.urlTest(groupTag.toNativeUtf8().cast())
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
final err = _box.urlTest(groupTag.toNativeUtf8().cast()).cast<Utf8>().toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
return left(err);
|
||||
}
|
||||
@ -431,9 +399,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
@override
|
||||
Stream<List<String>> watchLogs(String path) async* {
|
||||
yield await _readLogFile(File(path));
|
||||
yield* Watcher(path, pollingDelay: const Duration(seconds: 1))
|
||||
.events
|
||||
.asyncMap((event) async {
|
||||
yield* Watcher(path, pollingDelay: const Duration(seconds: 1)).events.asyncMap((event) async {
|
||||
if (event.type == ChangeType.MODIFY) {
|
||||
await _readLogFile(File(path));
|
||||
}
|
||||
@ -444,7 +410,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
@override
|
||||
TaskEither<String, Unit> clearLogs() {
|
||||
return TaskEither(
|
||||
() => Isolate.run(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
_logBuffer.clear();
|
||||
return right(unit);
|
||||
@ -455,8 +421,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
|
||||
Future<List<String>> _readLogFile(File file) async {
|
||||
if (_logFilePosition == 0 && file.lengthSync() == 0) return [];
|
||||
final content =
|
||||
await file.openRead(_logFilePosition).transform(utf8.decoder).join();
|
||||
final content = await file.openRead(_logFilePosition).transform(utf8.decoder).join();
|
||||
_logFilePosition = file.lengthSync();
|
||||
final lines = const LineSplitter().convert(content);
|
||||
if (lines.length > 300) {
|
||||
@ -479,7 +444,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
}) {
|
||||
loggy.debug("generating warp config");
|
||||
return TaskEither(
|
||||
() => Isolate.run(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final response = _box
|
||||
.generateWarpConfig(
|
||||
|
||||
@ -82,7 +82,7 @@ SPEC CHECKSUMS:
|
||||
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
|
||||
flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
|
||||
flutter_udid: d26e455e8c06174e6aff476e147defc6cae38495
|
||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
|
||||
800
pubspec.lock
Executable file → Normal file
800
pubspec.lock
Executable file → Normal file
File diff suppressed because it is too large
Load Diff
@ -61,7 +61,7 @@ dependencies:
|
||||
fpdart: ^1.1.0
|
||||
dartx: ^1.2.0
|
||||
rxdart: ^0.27.7
|
||||
# combine: ^0.5.8 # 暂时移除,使用 Isolate.run 替代
|
||||
combine: 0.5.7 # 精确版本,兼容 Flutter 3.24.3(与 hiddify-app 相同)
|
||||
encrypt: ^5.0.0
|
||||
path: ^1.8.3
|
||||
path_provider: ^2.1.1
|
||||
@ -140,6 +140,7 @@ dev_dependencies:
|
||||
flutter:
|
||||
assets:
|
||||
- assets/images/
|
||||
- assets/geosite/
|
||||
- assets/translations/strings_en.i18n.json
|
||||
- assets/translations/strings_zh.i18n.json
|
||||
- assets/translations/strings_es.i18n.json
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user