修改flutter兼容版本,优化macos无法切换节点和协议不兼容等问题

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