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

(cherry picked from commit dcc07886f8ba73eb2630a14a81bda191468c7a1f)
This commit is contained in:
Rust 2025-10-30 02:50:00 -07:00 committed by speakeloudest
parent 8bba2441c2
commit 064a0a7402
8 changed files with 940 additions and 555 deletions

4
.gitignore vendored
View File

@ -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/

View File

@ -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}');

View File

@ -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');
});
}
//

View File

@ -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: 10macOS优化
/// initialDelay: 1000msmacOS优化
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');
// outboundsMobile.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');
// StoppedUI更新
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');
// StoppedUI更新
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();
}
///

View File

@ -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(

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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