Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f429e5546 |
@ -31,7 +31,7 @@ class KRDomain {
|
||||
// static String kr_currentDomain = "apicn.bearvpn.top";
|
||||
|
||||
static List<String> kr_baseDomains = ["api.hifast.biz", "api.airovpn.tel",];
|
||||
static String kr_currentDomain = "api.hifast.biz1";
|
||||
static String kr_currentDomain = "api.hifast.biz";
|
||||
|
||||
// 备用域名获取地址列表
|
||||
static List<String> kr_backupDomainUrls = [
|
||||
@ -427,12 +427,6 @@ class KRDomain {
|
||||
|
||||
/// 预检测域名可用性(在应用启动时调用)
|
||||
static Future<void> kr_preCheckDomains() async {
|
||||
// Debug 模式下跳过域名预检测
|
||||
// if (kDebugMode) {
|
||||
// KRLogUtil.kr_i('🐛 Debug 模式,跳过域名预检测', tag: 'KRDomain');
|
||||
// return;
|
||||
// }
|
||||
|
||||
KRLogUtil.kr_i('🚀 开始预检测域名可用性', tag: 'KRDomain');
|
||||
|
||||
// 异步预检测,不阻塞应用启动
|
||||
|
||||
@ -77,6 +77,24 @@ class KRAppRunData {
|
||||
return kr_account.value != null && kr_account.value!.startsWith('9000');
|
||||
}
|
||||
|
||||
/// 🔧 P1修复: 重置所有运行时状态(用于应用恢复/热重载)
|
||||
/// 注意: 不会清除持久化存储的数据,只重置内存状态
|
||||
Future<void> kr_resetRuntimeState() async {
|
||||
try {
|
||||
print('🔄 开始重置 KRAppRunData 运行时状态...');
|
||||
|
||||
// 重新从存储加载用户信息,确保状态同步
|
||||
await kr_initializeUserInfo();
|
||||
|
||||
print('✅ KRAppRunData 状态已重置');
|
||||
print(' - 登录状态: ${kr_isLogin.value}');
|
||||
print(' - 账号: ${kr_account.value}');
|
||||
print(' - Token存在: ${kr_token != null && kr_token!.isNotEmpty}');
|
||||
} catch (e) {
|
||||
print('⚠️ KRAppRunData 状态重置失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔧 修复2.1:验证Token格式是否有效
|
||||
/// 检查Token是否符合JWT格式(header.payload.signature)
|
||||
/// 这能有效防止被污染的或过期的Token数据
|
||||
|
||||
@ -16,7 +16,6 @@ import '../../../localization/app_translations.dart';
|
||||
import '../../../localization/kr_language_utils.dart';
|
||||
import '../../../model/business/kr_group_outbound_list.dart';
|
||||
import '../../../model/business/kr_outbound_item.dart';
|
||||
import '../../../services/kr_announcement_service.dart';
|
||||
import '../../../utils/kr_event_bus.dart';
|
||||
import '../../../utils/kr_update_util.dart';
|
||||
import '../../../widgets/dialogs/kr_dialog.dart';
|
||||
@ -87,6 +86,15 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
// 是否已连接
|
||||
final kr_isConnected = false.obs;
|
||||
|
||||
// Prevent rapid toggles from racing start/stop.
|
||||
final kr_isToggleBusy = false.obs;
|
||||
DateTime? _lastToggleRequestTime;
|
||||
// 🔧 关键修复:缩短防抖窗口从 1500ms → 300ms
|
||||
// 原因:防抖的真实目的是防止连击,不是防止反向操作
|
||||
// kr_isToggleBusy 已经防止了并发执行,防抖只需要防止单次按钮连击即可
|
||||
// Windows 上 DNS/proxy 恢复需要 2-4 秒,不能用防抖窗口来限制用户的反向操作
|
||||
static const Duration _toggleDebounceWindow = Duration(milliseconds: 300);
|
||||
|
||||
// 是否显示延迟
|
||||
final kr_isLatency = false.obs;
|
||||
|
||||
@ -116,6 +124,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
|
||||
// 🔧 保守修复:添加状态变化时间追踪,用于检测状态卡住
|
||||
DateTime? _lastStatusChangeTime;
|
||||
String? _lastStoppedErrorSignature;
|
||||
Timer? _statusWatchdogTimer;
|
||||
|
||||
// 当前选中的组
|
||||
@ -311,12 +320,19 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
// 绑定连接状态
|
||||
_bindConnectionStatus();
|
||||
|
||||
// ✅ 参考 Hiddify:监听模式切换并自动重连
|
||||
// 当用户改变连接模式(全局/规则)时,自动重新连接以应用新配置
|
||||
_setupConnectionModeListener();
|
||||
|
||||
// 注册应用生命周期监听
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
// 🔧 新增:恢复上次选择的节点显示
|
||||
_restoreSelectedNode();
|
||||
|
||||
// ✅ 改进:不再在启动时检查权限,避免弹窗
|
||||
// 权限检查延迟到用户尝试切换全局模式时进行
|
||||
|
||||
// 延迟同步连接状态,确保状态正确
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
kr_forceSyncConnectionStatus(true);
|
||||
@ -327,6 +343,10 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
_startStatusWatchdog();
|
||||
}
|
||||
|
||||
/// ✅ Windows 权限检查已移至 kr_updateConnectionType 方法
|
||||
/// 当用户尝试切换到全局模式(TUN)时进行权限检查
|
||||
/// 这样不会在启动时弹窗,只在用户需要时提示
|
||||
|
||||
/// 🔧 新增:恢复上次选择的节点显示
|
||||
Future<void> _restoreSelectedNode() async {
|
||||
try {
|
||||
@ -593,7 +613,6 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
kr_currentViewStatus.value = KRHomeViewsStatus.kr_loggedIn;
|
||||
KRLogUtil.kr_i('登录状态变化:设置为已登录', tag: 'HomeController');
|
||||
|
||||
KRAnnouncementService().kr_checkAnnouncement();
|
||||
|
||||
// 订阅服务已在 splash 页面初始化,此处无需重复初始化
|
||||
KRLogUtil.kr_i('订阅服务已在启动页初始化,跳过重复初始化', tag: 'HomeController');
|
||||
@ -748,7 +767,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
_lastStatusChangeTime = DateTime.now();
|
||||
|
||||
switch (status) {
|
||||
case SingboxStopped():
|
||||
case SingboxStopped(:final alert, :final message):
|
||||
KRLogUtil.kr_i('🔴 状态: 已停止', tag: 'HomeController');
|
||||
kr_connectText.value = AppTranslations.kr_home.disconnected;
|
||||
kr_stopConnectionTimer();
|
||||
@ -758,12 +777,17 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
kr_currentSpeed.value = "--";
|
||||
kr_isLatency.value = false;
|
||||
kr_isConnected.value = false;
|
||||
// ✅ 关键修复:kr_isToggleBusy 已在 .then() 中释放,这里作为双重保险
|
||||
kr_isToggleBusy.value = false;
|
||||
kr_currentNodeLatency.value = -2;
|
||||
// 强制刷新 isConnected 状态
|
||||
kr_isConnected.refresh();
|
||||
_maybeShowStoppedError(alert: alert, message: message);
|
||||
break;
|
||||
case SingboxStarting():
|
||||
KRLogUtil.kr_i('🟡 状态: 正在启动', tag: 'HomeController');
|
||||
// 新一次连接流程开始,允许再次弹出 stopped 错误提示
|
||||
_lastStoppedErrorSignature = null;
|
||||
kr_connectText.value = AppTranslations.kr_home.connecting;
|
||||
kr_currentSpeed.value = AppTranslations.kr_home.connecting;
|
||||
kr_currentNodeLatency.value = -1;
|
||||
@ -773,7 +797,6 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
break;
|
||||
case SingboxStarted():
|
||||
KRLogUtil.kr_i('🟢 状态: 已启动', tag: 'HomeController');
|
||||
if (kDebugMode) {}
|
||||
|
||||
// 取消连接超时处理
|
||||
_cancelConnectionTimeout();
|
||||
@ -782,13 +805,13 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
kr_updateConnectionInfo();
|
||||
kr_isLatency.value = false;
|
||||
kr_isConnected.value = true;
|
||||
// ✅ 关键修复:kr_isToggleBusy 已在 .then() 中释放,这里作为双重保险
|
||||
kr_isToggleBusy.value = false;
|
||||
|
||||
// 🔧 关键修复:如果延迟还是-1(连接中状态),立即设置为0(已连接但延迟未知)
|
||||
if (kr_currentNodeLatency.value == -1) {
|
||||
if (kDebugMode) {}
|
||||
kr_currentNodeLatency.value = 0;
|
||||
kr_currentNodeLatency.refresh(); // 强制刷新延迟值
|
||||
if (kDebugMode) {}
|
||||
}
|
||||
|
||||
// 🔧 修复:立即尝试更新延迟值
|
||||
@ -798,7 +821,6 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
kr_isConnected.refresh();
|
||||
// 强制更新UI
|
||||
update();
|
||||
if (kDebugMode) {}
|
||||
break;
|
||||
case SingboxStopping():
|
||||
KRLogUtil.kr_i('🟠 状态: 正在停止', tag: 'HomeController');
|
||||
@ -883,10 +905,89 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
});
|
||||
}
|
||||
|
||||
/// ✅ 参考 Hiddify 的自动重连机制(改进版)
|
||||
/// 当用户改变连接模式(全局/规则)时,自动重新连接以应用新配置
|
||||
/// 重要:添加了超时保护和权限检查,避免TUN权限问题导致卡顿
|
||||
bool _isReconnecting = false; // 防止并发重启
|
||||
KRConnectionType? _lastSuccessfulMode; // 记录上一次成功的模式,用于检测无限循环
|
||||
|
||||
void _setupConnectionModeListener() {
|
||||
ever(KRSingBoxImp.instance.kr_connectionType, (newType) async {
|
||||
KRLogUtil.kr_i('🔄 连接类型改变: $newType(前: $_lastSuccessfulMode)', tag: 'HomeController');
|
||||
|
||||
// 只有在已连接的情况下才重新连接
|
||||
if (!kr_isConnected.value) {
|
||||
KRLogUtil.kr_i('ℹ️ VPN未连接,仅更新配置,无需重新连接', tag: 'HomeController');
|
||||
_lastSuccessfulMode = newType; // 更新记录
|
||||
return;
|
||||
}
|
||||
|
||||
// ❌ 关键修复:检测回滚导致的重复触发
|
||||
// 如果新模式是最后一次失败前的模式,说明是 kr_updateConnectionType 中的回滚,不再重试
|
||||
if (_lastSuccessfulMode != null && newType == _lastSuccessfulMode) {
|
||||
KRLogUtil.kr_w('⚠️ 检测到回滚操作,不再重试(newType=$newType,lastSuccessful=$_lastSuccessfulMode)', tag: 'HomeController');
|
||||
return;
|
||||
}
|
||||
|
||||
// 防止并发重启
|
||||
if (_isReconnecting) {
|
||||
KRLogUtil.kr_w('⚠️ 已有重连操作进行中,忽略此次切换', tag: 'HomeController');
|
||||
return;
|
||||
}
|
||||
|
||||
_isReconnecting = true;
|
||||
|
||||
try {
|
||||
KRLogUtil.kr_i('✅ VPN已连接,正在应用新的连接模式...', tag: 'HomeController');
|
||||
KRCommonUtil.kr_showToast('正在切换连接模式...');
|
||||
|
||||
// 添加超时保护(10秒),避免卡顿
|
||||
await KRSingBoxImp.instance.kr_restart().timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () {
|
||||
KRLogUtil.kr_w('⏱️ 重连超时(10s),可能缺少权限', tag: 'HomeController');
|
||||
throw TimeoutException('连接模式切换超时,请检查是否需要管理员权限');
|
||||
},
|
||||
);
|
||||
|
||||
_lastSuccessfulMode = newType; // ✅ 切换成功,更新记录
|
||||
KRLogUtil.kr_i('✅ 连接模式切换完成', tag: 'HomeController');
|
||||
KRCommonUtil.kr_showToast('连接模式已切换');
|
||||
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ 连接模式切换失败: $e', tag: 'HomeController');
|
||||
|
||||
// 根据错误类型显示不同的提示
|
||||
final errorMsg = e.toString();
|
||||
if (errorMsg.contains('超时') || errorMsg.contains('权限') || errorMsg.contains('admin') || errorMsg.contains('Permission')) {
|
||||
KRCommonUtil.kr_showToast('全局模式需要管理员权限,请以管理员身份运行应用');
|
||||
} else {
|
||||
KRCommonUtil.kr_showToast('切换失败: ${e.toString().split('\n').first}');
|
||||
}
|
||||
} finally {
|
||||
_isReconnecting = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/// 🔧 重构: 参考 hiddify-app 的 toggleConnection 实现
|
||||
Future<void> kr_toggleSwitch(bool value) async {
|
||||
final currentStatus = KRSingBoxImp.instance.kr_status.value;
|
||||
|
||||
if (kr_isToggleBusy.value) {
|
||||
KRLogUtil.kr_i('toggleSwitch ignored: toggle is busy', tag: 'HomeController');
|
||||
return;
|
||||
}
|
||||
|
||||
final now = DateTime.now();
|
||||
if (_lastToggleRequestTime != null &&
|
||||
now.difference(_lastToggleRequestTime!) < _toggleDebounceWindow) {
|
||||
KRLogUtil.kr_i('toggleSwitch debounced: ignore rapid taps', tag: 'HomeController');
|
||||
return;
|
||||
}
|
||||
_lastToggleRequestTime = now;
|
||||
|
||||
KRLogUtil.kr_i(
|
||||
'🔵 toggleSwitch 被调用: value=$value, currentStatus=$currentStatus',
|
||||
tag: 'HomeController');
|
||||
@ -910,100 +1011,91 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
return;
|
||||
}
|
||||
}
|
||||
kr_isToggleBusy.value = true;
|
||||
// 🔧 保守修复: 记录状态变化时间
|
||||
_lastStatusChangeTime = DateTime.now();
|
||||
|
||||
try {
|
||||
// 🔧 保守修复: 记录状态变化时间
|
||||
_lastStatusChangeTime = DateTime.now();
|
||||
|
||||
if (value) {
|
||||
// 开启连接
|
||||
KRLogUtil.kr_i('🔄 开始连接...', tag: 'HomeController');
|
||||
if (kDebugMode) {}
|
||||
await _kr_prepareCountrySelectionBeforeStart();
|
||||
final selectedAfter =
|
||||
await KRSecureStorage().kr_readData(key: 'SELECTED_NODE_TAG');
|
||||
// KRLogUtil.kr_i('准备后 SELECTED_NODE_TAG: ${selectedAfter ?? ''}',
|
||||
// tag: 'HomeController');
|
||||
// KRLogUtil.kr_i('准备后 kr_currentNodeName: ${kr_currentNodeName.value}',
|
||||
// tag: 'HomeController');
|
||||
// KRLogUtil.kr_i('准备后 kr_cutTag: ${kr_cutTag.value}',
|
||||
// tag: 'HomeController');
|
||||
// KRLogUtil.kr_i('准备后 kr_cutSeletedTag: ${kr_cutSeletedTag.value}',
|
||||
// tag: 'HomeController');
|
||||
await kr_performNodeSwitch(selectedAfter!);
|
||||
await KRSingBoxImp.instance.kr_start();
|
||||
KRLogUtil.kr_i('✅ 连接命令已发送', tag: 'HomeController');
|
||||
if (kDebugMode) {}
|
||||
|
||||
// 🔧 修复: 等待状态更新,最多3秒
|
||||
await _waitForStatus(SingboxStatus.started().runtimeType,
|
||||
maxSeconds: 3);
|
||||
} else {
|
||||
// 关闭连接
|
||||
KRLogUtil.kr_i('🛑 开始断开连接...', tag: 'HomeController');
|
||||
if (kDebugMode) {}
|
||||
await KRSingBoxImp.instance.kr_stop().timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () {
|
||||
KRLogUtil.kr_e('⚠️ 停止操作超时', tag: 'HomeController');
|
||||
throw TimeoutException('Stop operation timeout');
|
||||
},
|
||||
);
|
||||
KRLogUtil.kr_i('✅ 断开命令已发送', tag: 'HomeController');
|
||||
if (kDebugMode) {}
|
||||
|
||||
// 🔧 保守修复: 等待状态更新,增加超时处理
|
||||
final success = await _waitForStatus(
|
||||
SingboxStatus.stopped().runtimeType,
|
||||
maxSeconds: 3,
|
||||
);
|
||||
if (!success) {
|
||||
// 停止超时,强制同步状态
|
||||
KRLogUtil.kr_w('⚠️ VPN 停止超时(3秒),强制同步状态', tag: 'HomeController');
|
||||
kr_forceSyncConnectionStatus();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ 切换失败: $e', tag: 'HomeController');
|
||||
if (kDebugMode) {}
|
||||
// 发生错误时强制同步状态
|
||||
kr_forceSyncConnectionStatus();
|
||||
}
|
||||
|
||||
if (kDebugMode) {}
|
||||
}
|
||||
|
||||
/// 🔧 等待状态达到预期值
|
||||
/// 🔧 保守修复: 返回 bool 表示是否成功达到预期状态
|
||||
Future<bool> _waitForStatus(Type expectedType, {int maxSeconds = 3}) async {
|
||||
if (kDebugMode) {}
|
||||
final startTime = DateTime.now();
|
||||
|
||||
while (DateTime.now().difference(startTime).inSeconds < maxSeconds) {
|
||||
final currentStatus = KRSingBoxImp.instance.kr_status.value;
|
||||
if (kDebugMode) {}
|
||||
|
||||
if (currentStatus.runtimeType == expectedType) {
|
||||
if (kDebugMode) {}
|
||||
// 强制同步确保 kr_isConnected 正确
|
||||
if (value) {
|
||||
// 开启连接
|
||||
KRLogUtil.kr_i('🔄 开始连接...', tag: 'HomeController');
|
||||
await _kr_prepareCountrySelectionBeforeStart();
|
||||
final selectedAfter =
|
||||
await KRSecureStorage().kr_readData(key: 'SELECTED_NODE_TAG');
|
||||
// KRLogUtil.kr_i('准备后 SELECTED_NODE_TAG: ${selectedAfter ?? ''}',
|
||||
// tag: 'HomeController');
|
||||
// KRLogUtil.kr_i('准备后 kr_currentNodeName: ${kr_currentNodeName.value}',
|
||||
// tag: 'HomeController');
|
||||
// KRLogUtil.kr_i('准备后 kr_cutTag: ${kr_cutTag.value}',
|
||||
// tag: 'HomeController');
|
||||
// KRLogUtil.kr_i('准备后 kr_cutSeletedTag: ${kr_cutSeletedTag.value}',
|
||||
// tag: 'HomeController');
|
||||
await kr_performNodeSwitch(selectedAfter!);
|
||||
// ✅ 关键修复:启动 VPN 时异步执行,使用 .then().catchError() 处理结果
|
||||
// 这样可以立即返回,不阻塞 UI 线程,同时能捕获异常
|
||||
KRSingBoxImp.instance.kr_start().then((_) {
|
||||
KRLogUtil.kr_i('✅ 启动命令已返回(可能还在进行中)', tag: 'HomeController');
|
||||
// ✅ 关键修复:立即释放 UI 加载状态,不等待 status stream
|
||||
// 这样用户可以立即看到按钮状态,不用等待 2-4 秒
|
||||
// 真实的连接状态由 status stream 驱动
|
||||
kr_isToggleBusy.value = false;
|
||||
}).catchError((e) {
|
||||
// ✅ 异步启动失败时立即释放锁并提示错误
|
||||
KRLogUtil.kr_e('❌ 启动 VPN 失败: $e', tag: 'HomeController');
|
||||
kr_isToggleBusy.value = false;
|
||||
kr_forceSyncConnectionStatus();
|
||||
// 🔧 更新状态变化时间
|
||||
_lastStatusChangeTime = DateTime.now();
|
||||
return true; // 成功
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
Get.snackbar(
|
||||
'加载失败',
|
||||
'切换 VPN 状态时出错${ e.toString()}',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
});
|
||||
|
||||
KRLogUtil.kr_i('✅ 连接命令已发送(异步执行)', tag: 'HomeController');
|
||||
|
||||
// ✅ 超时保护:即使启动失败也会通过 catchError 释放,这里是双重保险
|
||||
Future.delayed(const Duration(seconds: 30)).then((_) {
|
||||
if (kr_isToggleBusy.value) {
|
||||
KRLogUtil.kr_w('⚠️ 启动超时 30 秒,强制释放开关锁', tag: 'HomeController');
|
||||
kr_isToggleBusy.value = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 关闭连接
|
||||
KRLogUtil.kr_i('🛑 开始断开连接...', tag: 'HomeController');
|
||||
|
||||
// ✅ 关键修复:停止 VPN 时异步执行,使用 .then().catchError() 处理结果
|
||||
KRSingBoxImp.instance.kr_stop().then((_) {
|
||||
KRLogUtil.kr_i('✅ 停止命令已返回(DNS/proxy 恢复可能还在进行中)', tag: 'HomeController');
|
||||
// ✅ 关键修复:立即释放 UI 加载状态,不等待 status stream
|
||||
// Windows 上 DNS 恢复和 proxy 恢复会耗时 2-4 秒,但这些都是后台异步的
|
||||
// 用户应该立即看到按钮可用,而不用等待系统操作完成
|
||||
kr_isToggleBusy.value = false;
|
||||
}).catchError((e) {
|
||||
// ✅ 异步停止失败时立即释放锁并提示错误
|
||||
KRLogUtil.kr_e('❌ 停止 VPN 失败: $e', tag: 'HomeController');
|
||||
kr_isToggleBusy.value = false;
|
||||
kr_forceSyncConnectionStatus();
|
||||
|
||||
Get.snackbar(
|
||||
'dialog.error'.tr,
|
||||
'home.toggleVpnStatusError'.trParams({'error': e.toString()}),
|
||||
snackPosition: SnackPosition.TOP,
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
});
|
||||
|
||||
KRLogUtil.kr_i('✅ 断开命令已发送(异步执行)', tag: 'HomeController');
|
||||
|
||||
// ✅ 超时保护:即使停止失败也会通过 catchError 释放,这里是双重保险
|
||||
Future.delayed(const Duration(seconds: 15)).then((_) {
|
||||
if (kr_isToggleBusy.value) {
|
||||
KRLogUtil.kr_w('⚠️ 关闭超时 15 秒,强制释放开关锁', tag: 'HomeController');
|
||||
kr_isToggleBusy.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 🔧 保守修复: 超时后记录详细日志
|
||||
final finalStatus = KRSingBoxImp.instance.kr_status.value;
|
||||
KRLogUtil.kr_w(
|
||||
'⏱️ 等待状态超时: 期望=${expectedType.toString()}, 实际=${finalStatus.runtimeType}, 耗时=${maxSeconds}秒',
|
||||
tag: 'HomeController',
|
||||
);
|
||||
if (kDebugMode) {}
|
||||
kr_forceSyncConnectionStatus();
|
||||
return false; // 超时失败
|
||||
}
|
||||
|
||||
Future<void> _kr_prepareCountrySelectionBeforeStart() async {
|
||||
@ -1461,92 +1553,45 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
kr_currentNodeLatency.value = -1;
|
||||
kr_isLatency.value = true; // 显示加载动画
|
||||
|
||||
// 🔧 保存新节点选择
|
||||
KRLogUtil.kr_i('💾 保存新节点选择: $tag', tag: 'HomeController');
|
||||
await KRSecureStorage()
|
||||
.kr_saveData(key: 'SELECTED_NODE_TAG', value: tag);
|
||||
// 🚀 方案 A:统一调用路径
|
||||
// 使用 KRSingBoxImp.kr_selectOutbound() 而不是直接调用 selectOutbound API
|
||||
// 这样可以自动获得所有保护机制:
|
||||
// - 重试逻辑(3次)
|
||||
// - 节点验证
|
||||
// - 20秒定时器防止被 urltest 覆盖
|
||||
KRLogUtil.kr_i('🔄 [方案A] 调用 KRSingBoxImp.kr_selectOutbound()...', tag: 'HomeController');
|
||||
|
||||
// 🚀 核心改进:使用 selectOutbound 进行热切换(参考 hiddify-app)
|
||||
// 优势:不重启VPN,保持连接状态,切换瞬间完成,VPN开关不闪烁
|
||||
KRLogUtil.kr_i('🔄 [热切换] 调用 selectOutbound 切换节点...',
|
||||
tag: 'HomeController');
|
||||
|
||||
// 🔧 关键修复:确定正确的 selector 组 tag
|
||||
// selectOutbound(groupTag, outboundTag) - 第一个参数是组的tag,不是节点的tag
|
||||
final activeGroups = KRSingBoxImp.instance.kr_activeGroups;
|
||||
String selectorGroupTag = 'select'; // 默认值
|
||||
|
||||
// 查找 selector 类型的组
|
||||
for (var group in activeGroups) {
|
||||
if (group.type == ProxyType.selector) {
|
||||
selectorGroupTag = group.tag;
|
||||
KRLogUtil.kr_i('🔍 找到 selector 组: $selectorGroupTag',
|
||||
tag: 'HomeController');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
KRLogUtil.kr_i('📡 调用 selectOutbound("$selectorGroupTag", "$tag")',
|
||||
tag: 'HomeController');
|
||||
|
||||
// 调用 sing-box 的 selectOutbound API
|
||||
final result = await KRSingBoxImp.instance.kr_singBox
|
||||
.selectOutbound(selectorGroupTag, tag)
|
||||
.run();
|
||||
|
||||
// 处理切换结果
|
||||
result.fold(
|
||||
(error) {
|
||||
// 切换失败
|
||||
KRLogUtil.kr_e('❌ selectOutbound 调用失败: $error',
|
||||
tag: 'HomeController');
|
||||
throw Exception('节点切换失败: $error');
|
||||
},
|
||||
(_) {
|
||||
// 切换成功
|
||||
KRSingBoxImp.instance.kr_startNodeSelectionMonitor(tag);
|
||||
KRLogUtil.kr_i('✅ selectOutbound 调用成功', tag: 'HomeController');
|
||||
},
|
||||
);
|
||||
|
||||
// 后台切换成功,立即更新UI(乐观更新)
|
||||
// 立即进行乐观更新,保持UI响应性
|
||||
kr_cutSeletedTag.value = tag;
|
||||
kr_updateConnectionInfo();
|
||||
// kr_moveToSelectedNode();
|
||||
kr_moveToSelectedNode();
|
||||
|
||||
// 🔧 短暂等待以确保内核状态同步(相比重启,等待时间大幅缩短)
|
||||
KRLogUtil.kr_i('⏳ [热切换] 等待内核状态同步(200ms)...', tag: 'HomeController');
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
// 异步执行节点选择和保护机制(包含定时器)
|
||||
// 使用 .then() 避免阻塞UI
|
||||
KRSingBoxImp.instance.kr_selectOutbound(tag).then((_) {
|
||||
KRLogUtil.kr_i('✅ 节点热切换成功,已启动保护机制: $tag', tag: 'HomeController');
|
||||
// 更新延迟信息
|
||||
_kr_updateLatencyOnConnected();
|
||||
}).catchError((error) {
|
||||
KRLogUtil.kr_e('❌ 节点切换失败: $error', tag: 'HomeController');
|
||||
|
||||
// 🔍 验证节点是否真正切换成功
|
||||
KRLogUtil.kr_i('🔍 [验证] 检查节点切换结果...', tag: 'HomeController');
|
||||
try {
|
||||
final updatedGroups = KRSingBoxImp.instance.kr_activeGroups;
|
||||
final selectGroup = updatedGroups.firstWhere(
|
||||
(group) => group.type == ProxyType.selector,
|
||||
orElse: () => throw Exception('未找到 selector 组'),
|
||||
);
|
||||
// 切换失败时恢复到原节点
|
||||
KRLogUtil.kr_i('🔄 节点选择失败,恢复到原节点: $originalTag', tag: 'HomeController');
|
||||
kr_cutTag.value = originalTag;
|
||||
kr_currentNodeName.value = originalTag;
|
||||
kr_cutSeletedTag.value = originalTag;
|
||||
|
||||
KRLogUtil.kr_i(
|
||||
'📊 [验证] ${selectGroup.tag}组当前选中: ${selectGroup.selected}',
|
||||
tag: 'HomeController');
|
||||
KRLogUtil.kr_i('📊 [验证] 目标节点: $tag', tag: 'HomeController');
|
||||
|
||||
if (selectGroup.selected != tag) {
|
||||
KRLogUtil.kr_w('⚠️ [验证] 节点选择验证失败,实际选中: ${selectGroup.selected}',
|
||||
tag: 'HomeController');
|
||||
// 不抛出异常,但记录警告
|
||||
} else {
|
||||
KRLogUtil.kr_i('✅ [验证] 节点选择验证成功!', tag: 'HomeController');
|
||||
// 尝试将存储中的节点恢复为原节点
|
||||
try {
|
||||
KRSecureStorage().kr_saveData(key: 'SELECTED_NODE_TAG', value: originalTag);
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ 恢复节点选择失败: $e', tag: 'HomeController');
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('⚠️ [验证] 节点验证过程出错: $e', tag: 'HomeController');
|
||||
}
|
||||
|
||||
// 更新延迟信息
|
||||
_kr_updateLatencyOnConnected();
|
||||
KRCommonUtil.kr_showToast('节点切换失败,已恢复为: $originalTag');
|
||||
});
|
||||
|
||||
KRLogUtil.kr_i('✅ 节点热切换成功,VPN保持连接: $tag', tag: 'HomeController');
|
||||
KRLogUtil.kr_i('✅ 节点热切换请求已发送,VPN保持连接: $tag', tag: 'HomeController');
|
||||
return true;
|
||||
} catch (switchError) {
|
||||
// 后台切换失败,恢复到原节点
|
||||
@ -2556,11 +2601,55 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 🔧 新增:检查订阅是否过期,如果过期则重新拉取
|
||||
_kr_checkAndRefreshExpiredSubscribe();
|
||||
});
|
||||
|
||||
KRLogUtil.kr_i('✅ 状态监控定时器已启动', tag: 'HomeController');
|
||||
}
|
||||
|
||||
/// 🔧 新增:检查订阅是否过期,若过期则自动重新拉取
|
||||
void _kr_checkAndRefreshExpiredSubscribe() {
|
||||
try {
|
||||
final currentSubscribe = kr_subscribeService.kr_currentSubscribe.value;
|
||||
if (currentSubscribe == null) {
|
||||
// 当前无订阅,可能已过期或未设置,尝试重新拉取
|
||||
KRLogUtil.kr_w('⚠️ 检测到无有效订阅,准备重新拉取订阅数据', tag: 'HomeController');
|
||||
kr_refreshAll();
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查订阅是否过期
|
||||
if (currentSubscribe.expireTime.isEmpty) {
|
||||
return; // 无过期时间,不处理
|
||||
}
|
||||
|
||||
try {
|
||||
final expireTime = DateTime.parse(currentSubscribe.expireTime);
|
||||
// 特殊处理 1970 年的情况(永久有效)
|
||||
if (expireTime.year == 1970) {
|
||||
return; // 永久有效,不处理
|
||||
}
|
||||
|
||||
final now = DateTime.now();
|
||||
final isExpired = !expireTime.isAfter(now);
|
||||
|
||||
if (isExpired) {
|
||||
KRLogUtil.kr_w(
|
||||
'⚠️ 检测到订阅已过期: ${currentSubscribe.name}, expire=${currentSubscribe.expireTime}',
|
||||
tag: 'HomeController',
|
||||
);
|
||||
// 订阅已过期,重新拉取最新的订阅数据
|
||||
kr_refreshAll();
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_d('解析过期时间失败: $e', tag: 'HomeController');
|
||||
}
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_d('订阅过期检查异常: $e', tag: 'HomeController');
|
||||
}
|
||||
}
|
||||
|
||||
/// 连接超时处理
|
||||
Timer? _connectionTimeoutTimer;
|
||||
|
||||
@ -2591,6 +2680,46 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void _maybeShowStoppedError({SingboxAlert? alert, String? message}) {
|
||||
final messageText = message?.trim();
|
||||
if (alert == null && (messageText == null || messageText.isEmpty)) return;
|
||||
|
||||
final signature = '${alert?.name ?? ""}|${messageText ?? ""}';
|
||||
if (signature == _lastStoppedErrorSignature) return;
|
||||
_lastStoppedErrorSignature = signature;
|
||||
|
||||
final detail = _presentSingboxStoppedReason(alert: alert, message: messageText);
|
||||
Get.snackbar(
|
||||
'network.status.failed'.tr,
|
||||
detail,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
duration: const Duration(seconds: 4),
|
||||
);
|
||||
}
|
||||
|
||||
String _presentSingboxStoppedReason({required SingboxAlert? alert, String? message}) {
|
||||
final parts = <String>[];
|
||||
if (alert != null) {
|
||||
parts.add(_presentSingboxAlert(alert));
|
||||
}
|
||||
if (message != null && message.trim().isNotEmpty) {
|
||||
parts.add(message.trim());
|
||||
}
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
String _presentSingboxAlert(SingboxAlert alert) {
|
||||
return switch (alert) {
|
||||
SingboxAlert.requestVPNPermission => 'failure.connectivity.missingVpnPermission'.tr,
|
||||
SingboxAlert.requestNotificationPermission => 'failure.connectivity.missingNotificationPermission'.tr,
|
||||
SingboxAlert.emptyConfiguration => 'failure.singbox.invalidConfig'.tr,
|
||||
SingboxAlert.startCommandServer => 'failure.singbox.start'.tr,
|
||||
SingboxAlert.createService => 'failure.singbox.create'.tr,
|
||||
SingboxAlert.startService => 'failure.singbox.start'.tr,
|
||||
};
|
||||
}
|
||||
|
||||
void _cancelConnectionTimeout() {
|
||||
_connectionTimeoutTimer?.cancel();
|
||||
_connectionTimeoutTimer = null;
|
||||
@ -2660,7 +2789,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
return delays;
|
||||
}
|
||||
|
||||
/// 尝试从活动组更新延迟值
|
||||
/// 尝试从活动组更新延迟值(优化版:reduce loops and logs)
|
||||
bool _kr_tryUpdateDelayFromActiveGroups() {
|
||||
try {
|
||||
final activeGroups = KRSingBoxImp.instance.kr_activeGroups;
|
||||
@ -2670,19 +2799,30 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 查找 selector 类型的组
|
||||
// 确定目标节点(auto 或手动选择的)
|
||||
final targetTag = kr_cutTag.value == "auto" ? "auto" : kr_cutTag.value;
|
||||
|
||||
// 查找 selector 类型的组并提前退出
|
||||
for (var group in activeGroups) {
|
||||
if (group.type == ProxyType.selector) {
|
||||
KRLogUtil.kr_d('找到 selector 组: ${group.tag}, 选中: ${group.selected}',
|
||||
tag: 'HomeController');
|
||||
try {
|
||||
// 使用 firstWhere 替代嵌套循环,找到匹配项后立即返回
|
||||
final item = group.items.firstWhere(
|
||||
(item) => item.tag == targetTag && item.urlTestDelay != 0,
|
||||
);
|
||||
|
||||
for (var item in group.items) {
|
||||
if (item.tag == kr_cutTag.value && item.urlTestDelay != 0) {
|
||||
kr_currentNodeLatency.value = item.urlTestDelay;
|
||||
KRLogUtil.kr_i('✅ 延迟值: ${item.urlTestDelay}ms',
|
||||
tag: 'HomeController');
|
||||
return true;
|
||||
kr_currentNodeLatency.value = item.urlTestDelay;
|
||||
|
||||
if (kDebugMode) {
|
||||
KRLogUtil.kr_d(
|
||||
'✅ 延迟更新: $targetTag = ${item.urlTestDelay}ms',
|
||||
tag: 'HomeController',
|
||||
);
|
||||
}
|
||||
return true; // 找到后立即返回
|
||||
} catch (_) {
|
||||
// 未找到匹配项,继续查找下一个组
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,8 +85,8 @@ class HttpUtil {
|
||||
client.findProxy = (url) {
|
||||
try {
|
||||
// 检查 SingBox 是否正在运行
|
||||
final singBoxStatus = KRSingBoxImp.instance.kr_status;
|
||||
final isProxyAvailable = singBoxStatus == SingboxStatus.started();
|
||||
final singBoxStatus = KRSingBoxImp.instance.kr_status.value;
|
||||
final isProxyAvailable = singBoxStatus is SingboxStarted;;
|
||||
|
||||
if (!isProxyAvailable) {
|
||||
// 代理未运行,直接使用直连
|
||||
|
||||
@ -225,17 +225,21 @@ class KRDeviceInfoService {
|
||||
}
|
||||
|
||||
/// Windows设备ID - 使用机器GUID
|
||||
/// 🔧 修复:不使用 flutter_udid,因为它会调用 wmic 命令弹出黑窗口
|
||||
Future<String> _getWindowsDeviceId() async {
|
||||
try {
|
||||
final windowsInfo = await _deviceInfo.windowsInfo;
|
||||
|
||||
// 优先使用 flutter_udid
|
||||
String udid = await FlutterUdid.consistentUdid;
|
||||
// 🔧 修复:不使用 FlutterUdid.consistentUdid
|
||||
// 因为它在 Windows 上会调用 "cmd.exe /c wmic csproduct get UUID"
|
||||
// 这个调用没有使用 CREATE_NO_WINDOW 标志,会弹出黑色命令行窗口
|
||||
|
||||
// 直接使用 device_info_plus 提供的信息构建唯一标识
|
||||
// windowsInfo.deviceId 已经是一个稳定的设备标识
|
||||
|
||||
// 构建多因子字符串
|
||||
final factors = [
|
||||
udid,
|
||||
windowsInfo.deviceId, // 设备ID
|
||||
windowsInfo.deviceId, // 设备ID (最稳定,来自注册表 MachineGuid)
|
||||
windowsInfo.computerName, // 计算机名
|
||||
windowsInfo.productName, // 产品名
|
||||
windowsInfo.numberOfCores.toString(), // CPU核心数
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -15,11 +15,31 @@ class KRThemeService extends GetxService {
|
||||
final String _key = 'themeOption'; // 存储主题选项的键
|
||||
late ThemeMode _currentThemeOption = ThemeMode.light; // 当前主题选项
|
||||
|
||||
// 🔧 P0修复: 添加初始化状态标记,防止未初始化就使用
|
||||
bool _isInitialized = false;
|
||||
|
||||
/// 初始化时从存储中加载主题设置
|
||||
Future<void> init() async {
|
||||
_currentThemeOption = await kr_loadThemeOptionFromStorage();
|
||||
try {
|
||||
_currentThemeOption = await kr_loadThemeOptionFromStorage();
|
||||
_isInitialized = true;
|
||||
} catch (e) {
|
||||
// 初始化失败时使用默认主题
|
||||
_currentThemeOption = ThemeMode.light;
|
||||
_isInitialized = true;
|
||||
print('⚠️ 主题初始化失败,使用默认主题: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔧 P0修复: 重置主题服务状态(用于热重载/应用恢复)
|
||||
Future<void> reset() async {
|
||||
_isInitialized = false;
|
||||
await init();
|
||||
}
|
||||
|
||||
/// 🔧 P0修复: 检查是否已初始化
|
||||
bool get isInitialized => _isInitialized;
|
||||
|
||||
/// 获取当前主题模式
|
||||
ThemeMode get kr_Theme {
|
||||
switch (_currentThemeOption) {
|
||||
@ -82,7 +102,7 @@ class KRThemeService extends GetxService {
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style:
|
||||
ElevatedButton.styleFrom(backgroundColor: Colors.green), // 自定义的按钮颜色
|
||||
ElevatedButton.styleFrom(backgroundColor: Colors.green), // 自定义的按钮颜色
|
||||
),
|
||||
switchTheme: SwitchThemeData(
|
||||
thumbColor: WidgetStateProperty.all(Colors.white), // 开关按钮颜色
|
||||
@ -100,10 +120,10 @@ class KRThemeService extends GetxService {
|
||||
// class KRColors {
|
||||
// // 1. 底部导航栏背景色(稍深的蓝黑色)
|
||||
// static const Color kr_bottomNavBackground = Color(0xFF161920);
|
||||
|
||||
|
||||
// // 2. 列表整体背景色(深蓝黑色)
|
||||
// static const Color kr_listBackground = Color(0xFF1A1D24);
|
||||
|
||||
|
||||
// // 3. 列表项背景色(略浅于列表背景的蓝黑色)
|
||||
// static const Color kr_listItemBackground = Color(0xFF1E2128);
|
||||
// }
|
||||
@ -142,18 +162,6 @@ class KRThemeService extends GetxService {
|
||||
return Colors.transparent; // 打开时不显示边框
|
||||
}),
|
||||
),
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
backgroundColor: Colors.black, // 黑色背景
|
||||
contentTextStyle: TextStyle(
|
||||
color: Colors.white, // 白色文字
|
||||
fontSize: 14,
|
||||
),
|
||||
actionTextColor: Colors.white, // 操作按钮文字颜色
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8), // 圆角
|
||||
),
|
||||
behavior: SnackBarBehavior.floating, // 浮动样式
|
||||
),
|
||||
// 其他自定义颜色
|
||||
);
|
||||
}
|
||||
|
||||
281
lib/app/utils/kr_debounce_throttle_util.dart
Normal file
281
lib/app/utils/kr_debounce_throttle_util.dart
Normal file
@ -0,0 +1,281 @@
|
||||
/// 🔧 防抖和限流工具类 - 用于处理快速点击导致的重复请求
|
||||
///
|
||||
/// 使用场景:
|
||||
/// 1. 防抖(Debounce):用户快速点击按钮,等用户停止点击后再执行一次
|
||||
/// - 模式切换、搜索、自动保存
|
||||
/// 2. 限流(Throttle):在给定时间内最多执行一次
|
||||
/// - 节点切换、数据刷新、VPN 启动/停止
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class KRDebounceThrottleUtil {
|
||||
// 防抖计时器缓存(防止多个防抖冲突)
|
||||
static final Map<String, Timer> _debounceTimers = {};
|
||||
|
||||
// 限流时间戳缓存
|
||||
static final Map<String, DateTime> _throttleTimestamps = {};
|
||||
|
||||
/// 🔧 防抖函数:等待 delay 时间无新请求后才执行
|
||||
///
|
||||
/// 适用于:模式切换、搜索、自动保存等
|
||||
///
|
||||
/// 例子:
|
||||
/// ```dart
|
||||
/// KRDebounceThrottleUtil.debounce(
|
||||
/// key: 'mode_switch',
|
||||
/// delay: Duration(milliseconds: 300),
|
||||
/// action: () {
|
||||
/// controller.kr_updateConnectionType(newType);
|
||||
/// },
|
||||
/// );
|
||||
/// ```
|
||||
static void debounce({
|
||||
required String key,
|
||||
required Duration delay,
|
||||
required VoidCallback action,
|
||||
}) {
|
||||
// 如果有旧的待执行任务,取消它
|
||||
_debounceTimers[key]?.cancel();
|
||||
|
||||
// 设置新的延迟任务
|
||||
_debounceTimers[key] = Timer(delay, () {
|
||||
action();
|
||||
_debounceTimers.remove(key);
|
||||
});
|
||||
}
|
||||
|
||||
/// 🔧 异步防抖函数(支持 Future)
|
||||
///
|
||||
/// 适用于需要等待异步操作的场景
|
||||
///
|
||||
/// 例子:
|
||||
/// ```dart
|
||||
/// await KRDebounceThrottleUtil.debounceAsync(
|
||||
/// key: 'node_switch',
|
||||
/// delay: Duration(milliseconds: 500),
|
||||
/// action: () async {
|
||||
/// await controller.kr_performNodeSwitch(tag);
|
||||
/// },
|
||||
/// );
|
||||
/// ```
|
||||
static Future<void> debounceAsync({
|
||||
required String key,
|
||||
required Duration delay,
|
||||
required Future<void> Function() action,
|
||||
}) async {
|
||||
_debounceTimers[key]?.cancel();
|
||||
|
||||
return Future.delayed(delay).then((_) async {
|
||||
await action();
|
||||
_debounceTimers.remove(key);
|
||||
});
|
||||
}
|
||||
|
||||
/// 🔧 限流函数:在指定时间内最多执行一次
|
||||
///
|
||||
/// 返回值:true 表示执行成功,false 表示被限流(仍在冷却中)
|
||||
///
|
||||
/// 适用于:节点切换、数据刷新、VPN 启动/停止等频繁操作
|
||||
///
|
||||
/// 例子:
|
||||
/// ```dart
|
||||
/// final canExecute = KRDebounceThrottleUtil.throttle(
|
||||
/// key: 'refresh',
|
||||
/// duration: Duration(seconds: 2),
|
||||
/// );
|
||||
///
|
||||
/// if (canExecute) {
|
||||
/// await controller.kr_refreshAll();
|
||||
/// } else {
|
||||
/// showToast('操作过于频繁,请稍后再试');
|
||||
/// }
|
||||
/// ```
|
||||
static bool throttle({
|
||||
required String key,
|
||||
required Duration duration,
|
||||
}) {
|
||||
final now = DateTime.now();
|
||||
final lastExecuteTime = _throttleTimestamps[key];
|
||||
|
||||
// 如果这是第一次执行或已经过了冷却时间
|
||||
if (lastExecuteTime == null ||
|
||||
now.difference(lastExecuteTime).inMilliseconds >= duration.inMilliseconds) {
|
||||
_throttleTimestamps[key] = now;
|
||||
return true; // 允许执行
|
||||
}
|
||||
|
||||
return false; // 仍在冷却期,拒绝执行
|
||||
}
|
||||
|
||||
/// 🔧 异步限流函数
|
||||
///
|
||||
/// 返回值:true 表示执行成功,false 表示被限流
|
||||
///
|
||||
/// 例子:
|
||||
/// ```dart
|
||||
/// final success = await KRDebounceThrottleUtil.throttleAsync(
|
||||
/// key: 'node_switch',
|
||||
/// duration: Duration(milliseconds: 2000),
|
||||
/// action: () async {
|
||||
/// await controller.kr_performNodeSwitch(tag);
|
||||
/// },
|
||||
/// );
|
||||
/// ```
|
||||
static Future<bool> throttleAsync({
|
||||
required String key,
|
||||
required Duration duration,
|
||||
required Future<void> Function() action,
|
||||
}) async {
|
||||
if (throttle(key: key, duration: duration)) {
|
||||
try {
|
||||
await action();
|
||||
return true;
|
||||
} catch (e) {
|
||||
// 如果执行失败,重置时间戳以允许重试
|
||||
_throttleTimestamps.remove(key);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// 🔧 获取某个 key 的剩余冷却时间(毫秒)
|
||||
///
|
||||
/// 返回值:
|
||||
/// - 0 或负数:可以执行
|
||||
/// - 正数:还需等待的毫秒数
|
||||
static int getRemainingThrottleTime({
|
||||
required String key,
|
||||
required Duration duration,
|
||||
}) {
|
||||
final lastExecuteTime = _throttleTimestamps[key];
|
||||
if (lastExecuteTime == null) return 0;
|
||||
|
||||
final remaining = duration.inMilliseconds -
|
||||
DateTime.now().difference(lastExecuteTime).inMilliseconds;
|
||||
return remaining > 0 ? remaining : 0;
|
||||
}
|
||||
|
||||
/// 🔧 清除指定 key 的所有计时器(调试用)
|
||||
static void clear({String? key}) {
|
||||
if (key != null) {
|
||||
_debounceTimers[key]?.cancel();
|
||||
_debounceTimers.remove(key);
|
||||
_throttleTimestamps.remove(key);
|
||||
} else {
|
||||
// 清除所有
|
||||
for (var timer in _debounceTimers.values) {
|
||||
timer.cancel();
|
||||
}
|
||||
_debounceTimers.clear();
|
||||
_throttleTimestamps.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔧 获取防抖和限流的统计信息(调试用)
|
||||
static Map<String, dynamic> getStats() {
|
||||
return {
|
||||
'activeDebounces': _debounceTimers.keys.toList(),
|
||||
'activeThrottles': _throttleTimestamps.keys.toList(),
|
||||
'totalActiveTimers': _debounceTimers.length + _throttleTimestamps.length,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔧 防抖辅助类 - 用于在 Controller 中创建防抖版本的方法
|
||||
///
|
||||
/// 例子:
|
||||
/// ```dart
|
||||
/// class MyController extends GetxController {
|
||||
/// late final _debouncer = KRDebouncedMethod(
|
||||
/// key: 'mode_switch',
|
||||
/// delay: Duration(milliseconds: 300),
|
||||
/// );
|
||||
///
|
||||
/// void kr_updateConnectionType(KRConnectionType type) {
|
||||
/// _debouncer.call(() async {
|
||||
/// // 实际的业务逻辑
|
||||
/// });
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
class KRDebouncedMethod {
|
||||
final String key;
|
||||
final Duration delay;
|
||||
|
||||
KRDebouncedMethod({
|
||||
required this.key,
|
||||
this.delay = const Duration(milliseconds: 300),
|
||||
});
|
||||
|
||||
void call(VoidCallback action) {
|
||||
KRDebounceThrottleUtil.debounce(
|
||||
key: key,
|
||||
delay: delay,
|
||||
action: action,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> callAsync(Future<void> Function() action) async {
|
||||
return KRDebounceThrottleUtil.debounceAsync(
|
||||
key: key,
|
||||
delay: delay,
|
||||
action: action,
|
||||
);
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
KRDebounceThrottleUtil.clear(key: key);
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔧 限流辅助类 - 用于在 Controller 中创建限流版本的方法
|
||||
///
|
||||
/// 例子:
|
||||
/// ```dart
|
||||
/// class MyController extends GetxController {
|
||||
/// late final _throttler = KRThrottledMethod(
|
||||
/// key: 'refresh',
|
||||
/// duration: Duration(seconds: 2),
|
||||
/// );
|
||||
///
|
||||
/// void kr_refreshAll() {
|
||||
/// if (_throttler.canExecute()) {
|
||||
/// // 执行刷新逻辑
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
class KRThrottledMethod {
|
||||
final String key;
|
||||
final Duration duration;
|
||||
|
||||
KRThrottledMethod({
|
||||
required this.key,
|
||||
required this.duration,
|
||||
});
|
||||
|
||||
bool canExecute() {
|
||||
return KRDebounceThrottleUtil.throttle(key: key, duration: duration);
|
||||
}
|
||||
|
||||
Future<bool> executeAsync(Future<void> Function() action) async {
|
||||
return KRDebounceThrottleUtil.throttleAsync(
|
||||
key: key,
|
||||
duration: duration,
|
||||
action: action,
|
||||
);
|
||||
}
|
||||
|
||||
int getRemainingTime() {
|
||||
return KRDebounceThrottleUtil.getRemainingThrottleTime(
|
||||
key: key,
|
||||
duration: duration,
|
||||
);
|
||||
}
|
||||
|
||||
void reset() {
|
||||
KRDebounceThrottleUtil.clear(key: key);
|
||||
}
|
||||
}
|
||||
82
lib/app/utils/kr_file_logger.dart
Normal file
82
lib/app/utils/kr_file_logger.dart
Normal file
@ -0,0 +1,82 @@
|
||||
import 'dart:io';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// 文件日志工具类 - 用于诊断 UI 卡死问题
|
||||
///
|
||||
/// 将关键操作的时间戳写入 日志.log 文件,方便排查问题
|
||||
/// 使用全局开关控制是否启用日志写入
|
||||
class KRFileLogger {
|
||||
static const String _logFileName = '日志.log';
|
||||
|
||||
/// 🔧 全局日志开关 - 修改为 true 可启用文件日志写入
|
||||
static const bool _enableFileLogging = true; // ⚠️ 调试模式:开启日志分析UI阻塞 // ← 诊断完成后改回 false
|
||||
|
||||
static File? _logFile;
|
||||
|
||||
/// 初始化日志文件(应用启动时调用)
|
||||
static Future<void> initialize() async {
|
||||
if (!_enableFileLogging) return;
|
||||
|
||||
try {
|
||||
// 获取应用根目录(Windows 下通常是 exe 所在目录)
|
||||
final appDir = Directory.current;
|
||||
final logPath = p.join(appDir.path, _logFileName);
|
||||
_logFile = File(logPath);
|
||||
|
||||
// 如果日志文件已存在且大于 5MB,清空它
|
||||
if (await _logFile!.exists()) {
|
||||
final stat = await _logFile!.stat();
|
||||
if (stat.size > 5 * 1024 * 1024) { // 5MB
|
||||
await _logFile!.writeAsString('');
|
||||
await _writeRaw('=== 日志文件已清空(超过 5MB)===\n');
|
||||
}
|
||||
}
|
||||
|
||||
await _writeRaw('=== 日志系统初始化 - ${DateTime.now().toIso8601String()} ===\n');
|
||||
} catch (e) {
|
||||
// 初始化失败,静默处理,不影响应用
|
||||
}
|
||||
}
|
||||
|
||||
/// 写入日志(带时间戳)
|
||||
static Future<void> log(String message) async {
|
||||
if (!_enableFileLogging || _logFile == null) return;
|
||||
|
||||
try {
|
||||
final timestamp = DateTime.now().toIso8601String();
|
||||
final logLine = '[$timestamp] $message\n';
|
||||
await _logFile!.writeAsString(logLine, mode: FileMode.append);
|
||||
} catch (e) {
|
||||
// 日志写入失败,静默处理,不要破坏主流程
|
||||
}
|
||||
}
|
||||
|
||||
/// 直接写入原始内容(用于特殊格式)
|
||||
static Future<void> _writeRaw(String content) async {
|
||||
if (!_enableFileLogging || _logFile == null) return;
|
||||
|
||||
try {
|
||||
await _logFile!.writeAsString(content, mode: FileMode.append);
|
||||
} catch (e) {
|
||||
// 日志写入失败,静默处理
|
||||
}
|
||||
}
|
||||
|
||||
/// 写入分隔线(用于区分不同的操作)
|
||||
static Future<void> separator() async {
|
||||
if (!_enableFileLogging) return;
|
||||
await _writeRaw('\n---\n');
|
||||
}
|
||||
|
||||
/// 清空日志文件
|
||||
static Future<void> clear() async {
|
||||
if (!_enableFileLogging || _logFile == null) return;
|
||||
|
||||
try {
|
||||
await _logFile!.writeAsString('');
|
||||
await _writeRaw('=== 日志已清空 - ${DateTime.now().toIso8601String()} ===\n');
|
||||
} catch (e) {
|
||||
// 清空失败,静默处理
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -26,11 +26,11 @@ class KRWindowManager with WindowListener, TrayListener {
|
||||
|
||||
const WindowOptions windowOptions = WindowOptions(
|
||||
size: Size(800, 668),
|
||||
minimumSize: Size(400, 334),
|
||||
minimumSize: Size(800, 668),
|
||||
center: true,
|
||||
backgroundColor: Colors.white,
|
||||
skipTaskbar: false,
|
||||
title: 'Hi快VPN',
|
||||
title: 'Kaer VPN',
|
||||
titleBarStyle: TitleBarStyle.normal,
|
||||
windowButtonVisibility: true,
|
||||
);
|
||||
@ -47,16 +47,17 @@ class KRWindowManager with WindowListener, TrayListener {
|
||||
await windowManager.setTitleBarStyle(TitleBarStyle.normal);
|
||||
await windowManager.setTitle('HiFastVPN');
|
||||
await windowManager.setSize(const Size(800, 668));
|
||||
await windowManager.setMinimumSize(const Size(400, 334));
|
||||
await windowManager.setMinimumSize(const Size(800, 668));
|
||||
await windowManager.center();
|
||||
await windowManager.show();
|
||||
// 阻止窗口关闭
|
||||
await windowManager.setPreventClose(true);
|
||||
} else {
|
||||
await windowManager.setTitle('HiFastVPN');
|
||||
await windowManager.setTitle('Kaer VPN');
|
||||
await windowManager.setSize(const Size(800, 668));
|
||||
await windowManager.setMinimumSize(const Size(400, 334));
|
||||
await windowManager.setMinimumSize(const Size(800, 668));
|
||||
await windowManager.center();
|
||||
await windowManager.show(); // macOS 也需要显式显示窗口
|
||||
}
|
||||
|
||||
// 初始化托盘
|
||||
@ -94,7 +95,7 @@ class KRWindowManager with WindowListener, TrayListener {
|
||||
/// 初始化平台通道
|
||||
void _initPlatformChannel() {
|
||||
if (Platform.isMacOS) {
|
||||
const platform = MethodChannel('hifast_vpn/terminate');
|
||||
const platform = MethodChannel('kaer_vpn/terminate');
|
||||
platform.setMethodCallHandler((call) async {
|
||||
if (call.method == 'onTerminate') {
|
||||
KRLogUtil.kr_i('收到应用终止通知');
|
||||
@ -144,13 +145,71 @@ class KRWindowManager with WindowListener, TrayListener {
|
||||
final String kr_port = KRSingBoxImp.instance.kr_port.toString();
|
||||
final String proxyText =
|
||||
'export https_proxy=http://127.0.0.1:$kr_port http_proxy=http://127.0.0.1:$kr_port all_proxy=socks5://127.0.0.1:$kr_port';
|
||||
|
||||
|
||||
await Clipboard.setData(ClipboardData(text: proxyText));
|
||||
}
|
||||
|
||||
/// 退出应用
|
||||
/// ✅ 改进:先恢复窗口(如果最小化),再显示对话框
|
||||
Future<void> _exitApp() async {
|
||||
KRLogUtil.kr_i('_exitApp: 退出应用');
|
||||
|
||||
// ✅ 关键修复:先恢复窗口(从最小化状态)
|
||||
// 这样可以确保对话框可见
|
||||
try {
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
await windowManager.setAlwaysOnTop(true);
|
||||
KRLogUtil.kr_i('✅ 窗口已恢复,准备显示对话框', tag: 'WindowManager');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('⚠️ 恢复窗口失败(可能已显示): $e', tag: 'WindowManager');
|
||||
}
|
||||
|
||||
// 🔧 修复:检查 VPN 是否在运行,如果运行则弹窗提醒用户
|
||||
if (KRSingBoxImp.instance.kr_status.value is! SingboxStopped) {
|
||||
KRLogUtil.kr_w('⚠️ VPN 正在运行,询问用户是否关闭', tag: 'WindowManager');
|
||||
|
||||
// 显示确认对话框
|
||||
final shouldExit = await Get.dialog<bool>(
|
||||
AlertDialog(
|
||||
title: Text('关闭 VPN'),
|
||||
content: Text("VPN 代理正在运行。\n\n是否现在关闭 VPN 并退出应用?\n\n(应用将等待 VPN 优雅关闭,预计 3-5 秒)"),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: false),
|
||||
child: Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Get.back(result: true),
|
||||
child: Text('关闭并退出', style: const TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
),
|
||||
barrierDismissible: false,
|
||||
) ?? false;
|
||||
|
||||
// ✅ 关键修复:对话框关闭后,恢复窗口的 AlwaysOnTop 状态
|
||||
try {
|
||||
await windowManager.setAlwaysOnTop(false);
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('⚠️ 恢复 AlwaysOnTop 失败: $e', tag: 'WindowManager');
|
||||
}
|
||||
|
||||
if (!shouldExit) {
|
||||
KRLogUtil.kr_i('_exitApp: 用户取消退出');
|
||||
return;
|
||||
}
|
||||
|
||||
KRLogUtil.kr_i('_exitApp: 用户确认关闭 VPN 并退出');
|
||||
} else {
|
||||
// ✅ VPN 未运行,也要恢复 AlwaysOnTop 状态
|
||||
try {
|
||||
await windowManager.setAlwaysOnTop(false);
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('⚠️ 恢复 AlwaysOnTop 失败: $e', tag: 'WindowManager');
|
||||
}
|
||||
}
|
||||
|
||||
await _handleTerminate();
|
||||
await windowManager.destroy();
|
||||
}
|
||||
@ -159,9 +218,9 @@ class KRWindowManager with WindowListener, TrayListener {
|
||||
Future<void> _showWindow() async {
|
||||
KRLogUtil.kr_i('_showWindow: 开始显示窗口');
|
||||
try {
|
||||
await windowManager.setSkipTaskbar(false);
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
await windowManager.setSkipTaskbar(false);
|
||||
await windowManager.setAlwaysOnTop(true);
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
await windowManager.setAlwaysOnTop(false);
|
||||
@ -180,6 +239,7 @@ class KRWindowManager with WindowListener, TrayListener {
|
||||
@override
|
||||
void onWindowClose() async {
|
||||
if (Platform.isWindows) {
|
||||
await windowManager.setSkipTaskbar(true);
|
||||
await windowManager.hide();
|
||||
} else if (Platform.isMacOS) {
|
||||
await windowManager.hide();
|
||||
@ -216,9 +276,29 @@ class KRWindowManager with WindowListener, TrayListener {
|
||||
/// 处理应用终止
|
||||
Future<void> _handleTerminate() async {
|
||||
KRLogUtil.kr_i('_handleTerminate: 处理应用终止');
|
||||
if (KRSingBoxImp.instance.kr_status == SingboxStatus.started()) {
|
||||
await KRSingBoxImp.instance.kr_stop();
|
||||
|
||||
// 🔧 修复 BUG:正确检查 VPN 状态而不是直接比较 Rx 对象
|
||||
// 之前的代码:if (KRSingBoxImp.instance.kr_status == SingboxStatus.started())
|
||||
// 问题:kr_status 是 Rx<SingboxStatus> 对象,不能直接与 SingboxStatus.started() 比较
|
||||
// 结果:该条件总是 false,导致 kr_stop() 从不被调用,VPN 不会关闭
|
||||
if (KRSingBoxImp.instance.kr_status.value is SingboxStarted) {
|
||||
KRLogUtil.kr_i('🛑 VPN 正在运行,开始关闭...', tag: 'WindowManager');
|
||||
try {
|
||||
await KRSingBoxImp.instance.kr_stop();
|
||||
KRLogUtil.kr_i('✅ VPN 已关闭', tag: 'WindowManager');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ VPN 关闭出错: $e', tag: 'WindowManager');
|
||||
}
|
||||
} else {
|
||||
KRLogUtil.kr_i('✅ VPN 未运行,无需关闭', tag: 'WindowManager');
|
||||
}
|
||||
|
||||
// 销毁托盘
|
||||
try {
|
||||
await trayManager.destroy();
|
||||
KRLogUtil.kr_i('✅ 托盘已销毁', tag: 'WindowManager');
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_w('⚠️ 销毁托盘出错: $e', tag: 'WindowManager');
|
||||
}
|
||||
await trayManager.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
||||
import 'package:kaer_with_panels/app/utils/kr_windows_process_util.dart';
|
||||
|
||||
/// Windows DNS 管理工具类
|
||||
///
|
||||
@ -38,28 +39,48 @@ class KRWindowsDnsUtil {
|
||||
}
|
||||
|
||||
try {
|
||||
KRLogUtil.kr_i('📦 开始备份 Windows DNS 设置...', tag: 'WindowsDNS');
|
||||
// 🔒 添加5秒超时保护
|
||||
return await Future.value(() async {
|
||||
KRLogUtil.kr_i('📦 开始备份 Windows DNS 设置...', tag: 'WindowsDNS');
|
||||
|
||||
// 1. 获取主网络接口
|
||||
final interfaceName = await _kr_getPrimaryNetworkInterface();
|
||||
if (interfaceName == null) {
|
||||
KRLogUtil.kr_e('❌ 无法获取主网络接口', tag: 'WindowsDNS');
|
||||
return false;
|
||||
}
|
||||
_primaryInterfaceName = interfaceName;
|
||||
KRLogUtil.kr_i('🔍 主网络接口: $_primaryInterfaceName', tag: 'WindowsDNS');
|
||||
// 1. 获取主网络接口
|
||||
final interfaceName = await _kr_getPrimaryNetworkInterface();
|
||||
if (interfaceName == null) {
|
||||
KRLogUtil.kr_e('❌ 无法获取主网络接口', tag: 'WindowsDNS');
|
||||
return false;
|
||||
}
|
||||
_primaryInterfaceName = interfaceName;
|
||||
KRLogUtil.kr_i('🔍 主网络接口: $_primaryInterfaceName', tag: 'WindowsDNS');
|
||||
|
||||
// 2. 获取当前 DNS 服务器
|
||||
final dnsServers = await _kr_getCurrentDnsServers(interfaceName);
|
||||
if (dnsServers.isEmpty) {
|
||||
KRLogUtil.kr_w('⚠️ 当前 DNS 为空,可能是自动获取', tag: 'WindowsDNS');
|
||||
_originalDnsServers = []; // 空列表表示 DHCP 自动获取
|
||||
} else {
|
||||
_originalDnsServers = dnsServers;
|
||||
KRLogUtil.kr_i('✅ 已备份 DNS: ${dnsServers.join(", ")}', tag: 'WindowsDNS');
|
||||
}
|
||||
// 2. 获取当前 DNS 服务器
|
||||
final dnsServers = await _kr_getCurrentDnsServers(interfaceName);
|
||||
|
||||
return true;
|
||||
// 🔧 P0修复1: 过滤掉 127.0.0.1 (sing-box 的本地 DNS)
|
||||
// 原因:如果备份了 127.0.0.1,关闭 VPN 后恢复为 127.0.0.1,但 sing-box 已停止,导致 DNS 无法解析
|
||||
final validDnsServers = dnsServers.where((dns) => !dns.startsWith('127.')).toList();
|
||||
|
||||
if (validDnsServers.isEmpty) {
|
||||
KRLogUtil.kr_w('⚠️ 当前 DNS 为空或全是本地地址,设为 DHCP 自动获取', tag: 'WindowsDNS');
|
||||
if (dnsServers.isNotEmpty) {
|
||||
KRLogUtil.kr_i(' (已过滤的本地DNS: ${dnsServers.join(", ")})', tag: 'WindowsDNS');
|
||||
}
|
||||
_originalDnsServers = []; // 空列表表示 DHCP 自动获取
|
||||
} else {
|
||||
_originalDnsServers = validDnsServers;
|
||||
KRLogUtil.kr_i('✅ 已备份有效 DNS: ${validDnsServers.join(", ")}', tag: 'WindowsDNS');
|
||||
if (dnsServers.length != validDnsServers.length) {
|
||||
KRLogUtil.kr_i(' (已过滤掉 ${dnsServers.length - validDnsServers.length} 个本地地址)', tag: 'WindowsDNS');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}()).timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () {
|
||||
KRLogUtil.kr_w('⏱️ DNS 备份操作超时(5秒),跳过备份', tag: 'WindowsDNS');
|
||||
return false;
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ 备份 DNS 设置失败: $e', tag: 'WindowsDNS');
|
||||
return false;
|
||||
@ -80,22 +101,33 @@ class KRWindowsDnsUtil {
|
||||
try {
|
||||
KRLogUtil.kr_i('🔄 开始恢复 Windows DNS 设置...', tag: 'WindowsDNS');
|
||||
|
||||
// 1. 检查是否有备份
|
||||
if (_primaryInterfaceName == null) {
|
||||
KRLogUtil.kr_w('⚠️ 没有备份的网络接口,尝试自动检测', tag: 'WindowsDNS');
|
||||
_primaryInterfaceName = await _kr_getPrimaryNetworkInterface();
|
||||
if (_primaryInterfaceName == null) {
|
||||
KRLogUtil.kr_e('❌ 无法检测网络接口,执行兜底恢复', tag: 'WindowsDNS');
|
||||
return await _kr_fallbackRestoreDns();
|
||||
}
|
||||
// 🔧 P1修复: 恢复时重新检测主接口,防止网络切换导致恢复错误接口
|
||||
final currentInterface = await _kr_getPrimaryNetworkInterface();
|
||||
if (currentInterface == null) {
|
||||
KRLogUtil.kr_e('❌ 无法检测当前网络接口,执行兜底恢复', tag: 'WindowsDNS');
|
||||
return await _kr_fallbackRestoreDns();
|
||||
}
|
||||
|
||||
// 2. 恢复原始 DNS
|
||||
// 检查接口是否变化
|
||||
if (_primaryInterfaceName != null && _primaryInterfaceName != currentInterface) {
|
||||
KRLogUtil.kr_w('⚠️ 网络接口已变化: $_primaryInterfaceName → $currentInterface', tag: 'WindowsDNS');
|
||||
KRLogUtil.kr_w(' 执行兜底恢复以确保当前接口DNS正常', tag: 'WindowsDNS');
|
||||
_primaryInterfaceName = currentInterface; // 更新为当前接口
|
||||
return await _kr_fallbackRestoreDns();
|
||||
}
|
||||
|
||||
// 使用当前检测到的接口
|
||||
_primaryInterfaceName = currentInterface;
|
||||
KRLogUtil.kr_i('🔍 当前网络接口: $_primaryInterfaceName', tag: 'WindowsDNS');
|
||||
|
||||
// 1. 检查是否有备份的DNS
|
||||
if (_originalDnsServers == null) {
|
||||
KRLogUtil.kr_w('⚠️ 没有备份的 DNS,执行兜底恢复', tag: 'WindowsDNS');
|
||||
return await _kr_fallbackRestoreDns();
|
||||
}
|
||||
|
||||
// 2. 恢复原始 DNS
|
||||
|
||||
if (_originalDnsServers!.isEmpty) {
|
||||
// 原本是 DHCP 自动获取
|
||||
KRLogUtil.kr_i('🔄 恢复为 DHCP 自动获取 DNS', tag: 'WindowsDNS');
|
||||
@ -130,6 +162,15 @@ class KRWindowsDnsUtil {
|
||||
return await _kr_fallbackRestoreDns();
|
||||
}
|
||||
|
||||
// 🔧 P2优化: 测试 DNS 解析是否真正可用
|
||||
KRLogUtil.kr_i('🧪 测试 DNS 解析功能...', tag: 'WindowsDNS');
|
||||
final canResolve = await _kr_testDnsResolution();
|
||||
if (!canResolve) {
|
||||
KRLogUtil.kr_w('⚠️ DNS 解析测试失败,执行兜底恢复', tag: 'WindowsDNS');
|
||||
return await _kr_fallbackRestoreDns();
|
||||
}
|
||||
KRLogUtil.kr_i('✅ DNS 解析测试通过', tag: 'WindowsDNS');
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ 恢复 DNS 设置失败: $e', tag: 'WindowsDNS');
|
||||
@ -187,7 +228,7 @@ class KRWindowsDnsUtil {
|
||||
Future<String?> _kr_getPrimaryNetworkInterface() async {
|
||||
try {
|
||||
// 使用 netsh 获取接口列表
|
||||
final result = await Process.run('netsh', ['interface', 'show', 'interface']);
|
||||
final result = await KRWindowsProcessUtil.runHidden('netsh', ['interface', 'show', 'interface']);
|
||||
|
||||
if (result.exitCode != 0) {
|
||||
KRLogUtil.kr_e('❌ 获取网络接口失败: ${result.stderr}', tag: 'WindowsDNS');
|
||||
@ -271,7 +312,7 @@ class KRWindowsDnsUtil {
|
||||
/// 返回:DNS 服务器列表
|
||||
Future<List<String>> _kr_getCurrentDnsServers(String interfaceName) async {
|
||||
try {
|
||||
final result = await Process.run('netsh', [
|
||||
final result = await KRWindowsProcessUtil.runHidden('netsh', [
|
||||
'interface',
|
||||
'ipv4',
|
||||
'show',
|
||||
@ -294,10 +335,9 @@ class KRWindowsDnsUtil {
|
||||
final ipMatch = RegExp(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b').firstMatch(line);
|
||||
if (ipMatch != null) {
|
||||
final ip = ipMatch.group(0)!;
|
||||
// 排除本地回环地址
|
||||
if (!ip.startsWith('127.')) {
|
||||
dnsServers.add(ip);
|
||||
}
|
||||
// 🔧 关键修复:不过滤127.0.0.1,以便正确检测DNS是否还在使用sing-box的本地DNS
|
||||
// 这样在恢复DNS时,第126行的验证才能正确检测到127.0.0.1并触发兜底恢复
|
||||
dnsServers.add(ip);
|
||||
}
|
||||
}
|
||||
|
||||
@ -325,7 +365,7 @@ class KRWindowsDnsUtil {
|
||||
|
||||
// 1. 设置主 DNS
|
||||
KRLogUtil.kr_i('🔧 设置主 DNS: ${dnsServers[0]}', tag: 'WindowsDNS');
|
||||
var result = await Process.run('netsh', [
|
||||
var result = await KRWindowsProcessUtil.runHidden('netsh', [
|
||||
'interface',
|
||||
'ipv4',
|
||||
'set',
|
||||
@ -345,7 +385,7 @@ class KRWindowsDnsUtil {
|
||||
if (dnsServers.length > 1) {
|
||||
for (int i = 1; i < dnsServers.length; i++) {
|
||||
KRLogUtil.kr_i('🔧 设置备用 DNS ${i}: ${dnsServers[i]}', tag: 'WindowsDNS');
|
||||
result = await Process.run('netsh', [
|
||||
result = await KRWindowsProcessUtil.runHidden('netsh', [
|
||||
'interface',
|
||||
'ipv4',
|
||||
'add',
|
||||
@ -384,7 +424,7 @@ class KRWindowsDnsUtil {
|
||||
try {
|
||||
KRLogUtil.kr_i('🔧 设置 DNS 为自动获取 (DHCP)', tag: 'WindowsDNS');
|
||||
|
||||
final result = await Process.run('netsh', [
|
||||
final result = await KRWindowsProcessUtil.runHidden('netsh', [
|
||||
'interface',
|
||||
'ipv4',
|
||||
'set',
|
||||
@ -416,7 +456,7 @@ class KRWindowsDnsUtil {
|
||||
try {
|
||||
KRLogUtil.kr_i('🔄 刷新 DNS 缓存...', tag: 'WindowsDNS');
|
||||
|
||||
final result = await Process.run('ipconfig', ['/flushdns']);
|
||||
final result = await KRWindowsProcessUtil.runHidden('ipconfig', ['/flushdns']);
|
||||
|
||||
if (result.exitCode == 0) {
|
||||
KRLogUtil.kr_i('✅ DNS 缓存已刷新', tag: 'WindowsDNS');
|
||||
@ -428,6 +468,51 @@ class KRWindowsDnsUtil {
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔧 P2优化: 测试 DNS 解析是否真正可用
|
||||
///
|
||||
/// 通过 nslookup 测试常见域名解析
|
||||
/// 返回:true 表示 DNS 可用,false 表示 DNS 不可用
|
||||
Future<bool> _kr_testDnsResolution() async {
|
||||
try {
|
||||
// 测试多个常见域名,提高成功率
|
||||
final testDomains = ['www.baidu.com', 'www.qq.com', 'dns.alidns.com'];
|
||||
|
||||
for (var domain in testDomains) {
|
||||
try {
|
||||
// 使用 nslookup 测试 DNS 解析,设置 2 秒超时
|
||||
final result = await KRWindowsProcessUtil.runHidden(
|
||||
'nslookup',
|
||||
[domain],
|
||||
).timeout(
|
||||
const Duration(seconds: 2),
|
||||
onTimeout: () {
|
||||
return ProcessResult(0, 1, '', 'Timeout');
|
||||
},
|
||||
);
|
||||
|
||||
if (result.exitCode == 0) {
|
||||
final output = result.stdout.toString();
|
||||
// 检查输出是否包含 IP 地址(简单验证)
|
||||
if (output.contains('Address:') || output.contains('地址:')) {
|
||||
KRLogUtil.kr_i('✅ DNS 解析测试通过: $domain', tag: 'WindowsDNS');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 单个域名失败,继续测试下一个
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 所有域名都解析失败
|
||||
KRLogUtil.kr_w('⚠️ 所有测试域名解析均失败', tag: 'WindowsDNS');
|
||||
return false;
|
||||
} catch (e) {
|
||||
KRLogUtil.kr_e('❌ DNS 解析测试异常: $e', tag: 'WindowsDNS');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 清除备份数据
|
||||
///
|
||||
/// 在应用退出或不需要时调用
|
||||
|
||||
602
lib/app/utils/kr_windows_process_util.dart
Normal file
602
lib/app/utils/kr_windows_process_util.dart
Normal file
@ -0,0 +1,602 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'kr_file_logger.dart';
|
||||
|
||||
class KRWindowsProcessUtil {
|
||||
/// 🔍 调试标志:设为 true 可以追踪所有命令执行(用于排查黑窗问题)
|
||||
static const bool _debugCommandExecution = true; // ← 开启调试
|
||||
|
||||
static Future<ProcessResult> runHidden(String executable, List<String> arguments) async {
|
||||
final timestamp = DateTime.now().toString();
|
||||
// 🔧 使用非阻塞日志(不 await),避免影响执行时序
|
||||
KRFileLogger.log('[黑屏调试] [KRWindowsProcessUtil] [$timestamp] 🔧 runHidden 调用: $executable ${arguments.join(" ")}');
|
||||
|
||||
if (!Platform.isWindows) {
|
||||
KRFileLogger.log('[黑屏调试] [KRWindowsProcessUtil] [$timestamp] ⚠️ 非 Windows,使用 Process.run(可能黑窗)');
|
||||
return Process.run(executable, arguments);
|
||||
}
|
||||
|
||||
KRFileLogger.log('[黑屏调试] [KRWindowsProcessUtil] [$timestamp] ✅ Windows,使用 CreateProcessW(无黑窗)');
|
||||
final result = await _runHiddenWindows(executable, arguments);
|
||||
KRFileLogger.log('[黑屏调试] [KRWindowsProcessUtil] [$timestamp] 📤 执行完成: exitCode=${result.exitCode}');
|
||||
return result;
|
||||
}
|
||||
|
||||
static Future<int> startHidden(String executable, List<String> arguments) async {
|
||||
if (!Platform.isWindows) {
|
||||
final process = await Process.start(executable, arguments);
|
||||
return process.pid;
|
||||
}
|
||||
return _startHiddenWindows(executable, arguments);
|
||||
}
|
||||
|
||||
static String _buildCommandLine(String executable, List<String> arguments) {
|
||||
final parts = <String>[_quoteArgument(executable)];
|
||||
for (final arg in arguments) {
|
||||
parts.add(_quoteArgument(arg));
|
||||
}
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
static bool _shouldSearchPath(String executable) {
|
||||
if (executable.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
return !(executable.contains('\\') || executable.contains('/') || executable.contains(':'));
|
||||
}
|
||||
|
||||
static String _quoteArgument(String value) {
|
||||
if (value.isEmpty) {
|
||||
return '""';
|
||||
}
|
||||
final needsQuotes = value.contains(' ') || value.contains('\t') || value.contains('"');
|
||||
if (!needsQuotes) {
|
||||
return value;
|
||||
}
|
||||
final buffer = StringBuffer('"');
|
||||
var backslashes = 0;
|
||||
for (var i = 0; i < value.length; i++) {
|
||||
final char = value[i];
|
||||
if (char == '\\') {
|
||||
backslashes++;
|
||||
continue;
|
||||
}
|
||||
if (char == '"') {
|
||||
buffer.write('\\' * (backslashes * 2 + 1));
|
||||
buffer.write('"');
|
||||
backslashes = 0;
|
||||
continue;
|
||||
}
|
||||
if (backslashes > 0) {
|
||||
buffer.write('\\' * backslashes);
|
||||
backslashes = 0;
|
||||
}
|
||||
buffer.write(char);
|
||||
}
|
||||
if (backslashes > 0) {
|
||||
buffer.write('\\' * (backslashes * 2));
|
||||
}
|
||||
buffer.write('"');
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
static Future<ProcessResult> _runHiddenWindows(String executable, List<String> arguments) async {
|
||||
final stdoutPipe = _createPipe();
|
||||
final stderrPipe = _createPipe();
|
||||
|
||||
final startupInfo = calloc<STARTUPINFO>();
|
||||
final processInfo = calloc<PROCESS_INFORMATION>();
|
||||
final commandLine = _buildCommandLine(executable, arguments).toNativeUtf16();
|
||||
final applicationName = _shouldSearchPath(executable) ? nullptr : executable.toNativeUtf16();
|
||||
final stdInput = _getStdInputHandle();
|
||||
|
||||
startupInfo.ref
|
||||
..cb = sizeOf<STARTUPINFO>()
|
||||
..dwFlags = STARTF_USESTDHANDLES
|
||||
..hStdInput = stdInput
|
||||
..hStdOutput = stdoutPipe.write
|
||||
..hStdError = stderrPipe.write;
|
||||
|
||||
final created = _CreateProcessW(
|
||||
applicationName,
|
||||
commandLine,
|
||||
nullptr,
|
||||
nullptr,
|
||||
TRUE,
|
||||
CREATE_NO_WINDOW,
|
||||
nullptr,
|
||||
nullptr,
|
||||
startupInfo,
|
||||
processInfo,
|
||||
);
|
||||
|
||||
calloc.free(commandLine);
|
||||
if (applicationName != nullptr) {
|
||||
calloc.free(applicationName);
|
||||
}
|
||||
|
||||
if (created == 0) {
|
||||
_closeHandle(stdoutPipe.read);
|
||||
_closeHandle(stdoutPipe.write);
|
||||
_closeHandle(stderrPipe.read);
|
||||
_closeHandle(stderrPipe.write);
|
||||
calloc.free(startupInfo);
|
||||
calloc.free(processInfo);
|
||||
throw Exception('CreateProcessW failed: ${_GetLastError()}');
|
||||
}
|
||||
|
||||
_closeHandle(stdoutPipe.write);
|
||||
_closeHandle(stderrPipe.write);
|
||||
|
||||
final output = await _collectOutput(processInfo.ref.hProcess, stdoutPipe.read, stderrPipe.read);
|
||||
final exitCode = _getExitCode(processInfo.ref.hProcess);
|
||||
|
||||
_closeHandle(stdoutPipe.read);
|
||||
_closeHandle(stderrPipe.read);
|
||||
_closeHandle(processInfo.ref.hThread);
|
||||
_closeHandle(processInfo.ref.hProcess);
|
||||
|
||||
final pid = processInfo.ref.dwProcessId;
|
||||
calloc.free(startupInfo);
|
||||
calloc.free(processInfo);
|
||||
|
||||
return ProcessResult(pid, exitCode, output.stdout, output.stderr);
|
||||
}
|
||||
|
||||
static Future<int> _startHiddenWindows(String executable, List<String> arguments) async {
|
||||
final startupInfo = calloc<STARTUPINFO>();
|
||||
final processInfo = calloc<PROCESS_INFORMATION>();
|
||||
final commandLine = _buildCommandLine(executable, arguments).toNativeUtf16();
|
||||
final applicationName = _shouldSearchPath(executable) ? nullptr : executable.toNativeUtf16();
|
||||
|
||||
startupInfo.ref.cb = sizeOf<STARTUPINFO>();
|
||||
|
||||
final created = _CreateProcessW(
|
||||
applicationName,
|
||||
commandLine,
|
||||
nullptr,
|
||||
nullptr,
|
||||
FALSE,
|
||||
CREATE_NO_WINDOW,
|
||||
nullptr,
|
||||
nullptr,
|
||||
startupInfo,
|
||||
processInfo,
|
||||
);
|
||||
|
||||
calloc.free(commandLine);
|
||||
if (applicationName != nullptr) {
|
||||
calloc.free(applicationName);
|
||||
}
|
||||
|
||||
if (created == 0) {
|
||||
calloc.free(startupInfo);
|
||||
calloc.free(processInfo);
|
||||
throw Exception('CreateProcessW failed: ${_GetLastError()}');
|
||||
}
|
||||
|
||||
final pid = processInfo.ref.dwProcessId;
|
||||
|
||||
_closeHandle(processInfo.ref.hThread);
|
||||
_closeHandle(processInfo.ref.hProcess);
|
||||
calloc.free(startupInfo);
|
||||
calloc.free(processInfo);
|
||||
|
||||
return pid;
|
||||
}
|
||||
|
||||
static _Pipe _createPipe() {
|
||||
final readHandle = calloc<Pointer<Void>>();
|
||||
final writeHandle = calloc<Pointer<Void>>();
|
||||
final securityAttributes = calloc<SECURITY_ATTRIBUTES>();
|
||||
securityAttributes.ref
|
||||
..nLength = sizeOf<SECURITY_ATTRIBUTES>()
|
||||
..bInheritHandle = TRUE
|
||||
..lpSecurityDescriptor = nullptr;
|
||||
|
||||
final created = _CreatePipe(readHandle, writeHandle, securityAttributes, 0);
|
||||
calloc.free(securityAttributes);
|
||||
if (created == 0) {
|
||||
calloc.free(readHandle);
|
||||
calloc.free(writeHandle);
|
||||
throw Exception('CreatePipe failed: ${_GetLastError()}');
|
||||
}
|
||||
|
||||
final readValue = readHandle.value;
|
||||
final writeValue = writeHandle.value;
|
||||
calloc.free(readHandle);
|
||||
calloc.free(writeHandle);
|
||||
|
||||
final infoResult = _SetHandleInformation(readValue, HANDLE_FLAG_INHERIT, 0);
|
||||
if (infoResult == 0) {
|
||||
_closeHandle(readValue);
|
||||
_closeHandle(writeValue);
|
||||
throw Exception('SetHandleInformation failed: ${_GetLastError()}');
|
||||
}
|
||||
|
||||
return _Pipe(readValue, writeValue);
|
||||
}
|
||||
|
||||
static Pointer<Void> _getStdInputHandle() {
|
||||
final handle = _GetStdHandle(STD_INPUT_HANDLE);
|
||||
if (handle == INVALID_HANDLE_VALUE || handle == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
return Pointer<Void>.fromAddress(handle);
|
||||
}
|
||||
|
||||
static Future<_ProcessOutput> _collectOutput(
|
||||
Pointer<Void> process,
|
||||
Pointer<Void> stdoutHandle,
|
||||
Pointer<Void> stderrHandle,
|
||||
) async {
|
||||
final stdoutBuilder = BytesBuilder();
|
||||
final stderrBuilder = BytesBuilder();
|
||||
|
||||
while (true) {
|
||||
final stdoutRead = _drainPipe(stdoutHandle, stdoutBuilder);
|
||||
final stderrRead = _drainPipe(stderrHandle, stderrBuilder);
|
||||
final waitResult = _WaitForSingleObject(process, 0);
|
||||
if (waitResult == WAIT_OBJECT_0) {
|
||||
break;
|
||||
}
|
||||
if (waitResult == WAIT_FAILED) {
|
||||
throw Exception('WaitForSingleObject failed: ${_GetLastError()}');
|
||||
}
|
||||
if (!stdoutRead && !stderrRead) {
|
||||
await Future.delayed(const Duration(milliseconds: 10));
|
||||
} else {
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
}
|
||||
}
|
||||
|
||||
while (_drainPipe(stdoutHandle, stdoutBuilder) || _drainPipe(stderrHandle, stderrBuilder)) {
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
}
|
||||
|
||||
return _ProcessOutput(
|
||||
_decodeOutput(stdoutBuilder),
|
||||
_decodeOutput(stderrBuilder),
|
||||
);
|
||||
}
|
||||
|
||||
static bool _drainPipe(Pointer<Void> handle, BytesBuilder builder) {
|
||||
final buffer = calloc<Uint8>(4096);
|
||||
final bytesRead = calloc<Uint32>();
|
||||
final available = calloc<Uint32>();
|
||||
var didRead = false;
|
||||
|
||||
while (true) {
|
||||
final peekOk = _PeekNamedPipe(handle, nullptr, 0, nullptr, available, nullptr);
|
||||
if (peekOk == 0 || available.value == 0) {
|
||||
break;
|
||||
}
|
||||
final toRead = available.value < 4096 ? available.value : 4096;
|
||||
final ok = _ReadFile(handle, buffer.cast<Void>(), toRead, bytesRead, nullptr);
|
||||
final read = ok == 0 ? 0 : bytesRead.value;
|
||||
if (read == 0) {
|
||||
break;
|
||||
}
|
||||
builder.add(buffer.asTypedList(read));
|
||||
didRead = true;
|
||||
}
|
||||
|
||||
calloc.free(buffer);
|
||||
calloc.free(bytesRead);
|
||||
calloc.free(available);
|
||||
|
||||
return didRead;
|
||||
}
|
||||
|
||||
static String _decodeOutput(BytesBuilder builder) {
|
||||
if (builder.length == 0) {
|
||||
return '';
|
||||
}
|
||||
final bytes = builder.toBytes();
|
||||
try {
|
||||
return systemEncoding.decode(bytes);
|
||||
} catch (_) {
|
||||
return utf8.decode(bytes, allowMalformed: true);
|
||||
}
|
||||
}
|
||||
|
||||
static int _getExitCode(Pointer<Void> process) {
|
||||
final exitCode = calloc<Uint32>();
|
||||
final ok = _GetExitCodeProcess(process, exitCode);
|
||||
final code = ok == 0 ? -1 : exitCode.value;
|
||||
calloc.free(exitCode);
|
||||
return code;
|
||||
}
|
||||
|
||||
static void _closeHandle(Pointer<Void> handle) {
|
||||
if (handle == nullptr) {
|
||||
return;
|
||||
}
|
||||
_CloseHandle(handle);
|
||||
}
|
||||
|
||||
// 🔧 WinINet API helpers for proxy settings
|
||||
/// 查询当前系统代理设置
|
||||
static String? queryWindowsProxyServer() {
|
||||
if (!Platform.isWindows) return null;
|
||||
|
||||
try {
|
||||
final bufferSize = calloc<Uint32>();
|
||||
bufferSize.value = sizeOf<INTERNET_PROXY_INFO>();
|
||||
|
||||
final proxyInfo = calloc<INTERNET_PROXY_INFO>();
|
||||
|
||||
final result = _InternetQueryOptionW(nullptr, INTERNET_OPTION_PROXY, proxyInfo.cast<Void>(), bufferSize);
|
||||
if (result == 0) {
|
||||
calloc.free(bufferSize);
|
||||
calloc.free(proxyInfo);
|
||||
return null;
|
||||
}
|
||||
|
||||
final proxyServer = proxyInfo.ref.lpszProxy.toDartString();
|
||||
calloc.free(bufferSize);
|
||||
calloc.free(proxyInfo);
|
||||
|
||||
return proxyServer.isEmpty ? null : proxyServer;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置系统代理
|
||||
static bool setWindowsProxyServer(String? server) {
|
||||
if (!Platform.isWindows) return false;
|
||||
|
||||
try {
|
||||
final proxyInfo = calloc<INTERNET_PROXY_INFO>();
|
||||
|
||||
if (server != null && server.isNotEmpty) {
|
||||
// 设置代理模式
|
||||
proxyInfo.ref.dwAccessType = INTERNET_OPEN_TYPE_PROXY;
|
||||
proxyInfo.ref.lpszProxy = server.toNativeUtf16();
|
||||
proxyInfo.ref.lpszProxyBypass = ''.toNativeUtf16();
|
||||
} else {
|
||||
// 禁用代理
|
||||
proxyInfo.ref.dwAccessType = INTERNET_OPEN_TYPE_DIRECT;
|
||||
proxyInfo.ref.lpszProxy = nullptr;
|
||||
proxyInfo.ref.lpszProxyBypass = nullptr;
|
||||
}
|
||||
|
||||
final result = _InternetSetOptionW(
|
||||
nullptr,
|
||||
INTERNET_OPTION_PROXY,
|
||||
proxyInfo.cast<Void>(),
|
||||
sizeOf<INTERNET_PROXY_INFO>(),
|
||||
);
|
||||
|
||||
if (server != null && server.isNotEmpty) {
|
||||
calloc.free(proxyInfo.ref.lpszProxy);
|
||||
calloc.free(proxyInfo.ref.lpszProxyBypass);
|
||||
}
|
||||
calloc.free(proxyInfo);
|
||||
|
||||
return result != 0;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 禁用系统代理
|
||||
static bool disableWindowsProxy() {
|
||||
return setWindowsProxyServer(null);
|
||||
}
|
||||
}
|
||||
|
||||
class _Pipe {
|
||||
final Pointer<Void> read;
|
||||
final Pointer<Void> write;
|
||||
|
||||
_Pipe(this.read, this.write);
|
||||
}
|
||||
|
||||
class _ProcessOutput {
|
||||
final String stdout;
|
||||
final String stderr;
|
||||
|
||||
_ProcessOutput(this.stdout, this.stderr);
|
||||
}
|
||||
|
||||
const int TRUE = 1;
|
||||
const int FALSE = 0;
|
||||
|
||||
const int STARTF_USESTDHANDLES = 0x00000100;
|
||||
const int CREATE_NO_WINDOW = 0x08000000;
|
||||
const int HANDLE_FLAG_INHERIT = 0x00000001;
|
||||
const int WAIT_OBJECT_0 = 0x00000000;
|
||||
const int WAIT_FAILED = 0xFFFFFFFF;
|
||||
const int STD_INPUT_HANDLE = -10;
|
||||
const int INVALID_HANDLE_VALUE = -1;
|
||||
|
||||
final class SECURITY_ATTRIBUTES extends Struct {
|
||||
@Uint32()
|
||||
external int nLength;
|
||||
|
||||
external Pointer<Void> lpSecurityDescriptor;
|
||||
|
||||
@Int32()
|
||||
external int bInheritHandle;
|
||||
}
|
||||
|
||||
final class STARTUPINFO extends Struct {
|
||||
@Uint32()
|
||||
external int cb;
|
||||
|
||||
external Pointer<Utf16> lpReserved;
|
||||
|
||||
external Pointer<Utf16> lpDesktop;
|
||||
|
||||
external Pointer<Utf16> lpTitle;
|
||||
|
||||
@Uint32()
|
||||
external int dwX;
|
||||
|
||||
@Uint32()
|
||||
external int dwY;
|
||||
|
||||
@Uint32()
|
||||
external int dwXSize;
|
||||
|
||||
@Uint32()
|
||||
external int dwYSize;
|
||||
|
||||
@Uint32()
|
||||
external int dwXCountChars;
|
||||
|
||||
@Uint32()
|
||||
external int dwYCountChars;
|
||||
|
||||
@Uint32()
|
||||
external int dwFillAttribute;
|
||||
|
||||
@Uint32()
|
||||
external int dwFlags;
|
||||
|
||||
@Uint16()
|
||||
external int wShowWindow;
|
||||
|
||||
@Uint16()
|
||||
external int cbReserved2;
|
||||
|
||||
external Pointer<Uint8> lpReserved2;
|
||||
|
||||
external Pointer<Void> hStdInput;
|
||||
|
||||
external Pointer<Void> hStdOutput;
|
||||
|
||||
external Pointer<Void> hStdError;
|
||||
}
|
||||
|
||||
final class PROCESS_INFORMATION extends Struct {
|
||||
external Pointer<Void> hProcess;
|
||||
|
||||
external Pointer<Void> hThread;
|
||||
|
||||
@Uint32()
|
||||
external int dwProcessId;
|
||||
|
||||
@Uint32()
|
||||
external int dwThreadId;
|
||||
}
|
||||
|
||||
final DynamicLibrary _kernel32 = DynamicLibrary.open('kernel32.dll');
|
||||
|
||||
final _CreatePipe = _kernel32.lookupFunction<
|
||||
Int32 Function(Pointer<Pointer<Void>>, Pointer<Pointer<Void>>, Pointer<SECURITY_ATTRIBUTES>, Uint32),
|
||||
int Function(Pointer<Pointer<Void>>, Pointer<Pointer<Void>>, Pointer<SECURITY_ATTRIBUTES>, int)>(
|
||||
'CreatePipe',
|
||||
);
|
||||
|
||||
final _SetHandleInformation = _kernel32.lookupFunction<
|
||||
Int32 Function(Pointer<Void>, Uint32, Uint32),
|
||||
int Function(Pointer<Void>, int, int)>(
|
||||
'SetHandleInformation',
|
||||
);
|
||||
|
||||
final _CreateProcessW = _kernel32.lookupFunction<
|
||||
Int32 Function(
|
||||
Pointer<Utf16>,
|
||||
Pointer<Utf16>,
|
||||
Pointer<SECURITY_ATTRIBUTES>,
|
||||
Pointer<SECURITY_ATTRIBUTES>,
|
||||
Int32,
|
||||
Uint32,
|
||||
Pointer<Void>,
|
||||
Pointer<Utf16>,
|
||||
Pointer<STARTUPINFO>,
|
||||
Pointer<PROCESS_INFORMATION>,
|
||||
),
|
||||
int Function(
|
||||
Pointer<Utf16>,
|
||||
Pointer<Utf16>,
|
||||
Pointer<SECURITY_ATTRIBUTES>,
|
||||
Pointer<SECURITY_ATTRIBUTES>,
|
||||
int,
|
||||
int,
|
||||
Pointer<Void>,
|
||||
Pointer<Utf16>,
|
||||
Pointer<STARTUPINFO>,
|
||||
Pointer<PROCESS_INFORMATION>,
|
||||
)>(
|
||||
'CreateProcessW',
|
||||
);
|
||||
|
||||
final _PeekNamedPipe = _kernel32.lookupFunction<
|
||||
Int32 Function(Pointer<Void>, Pointer<Void>, Uint32, Pointer<Uint32>, Pointer<Uint32>, Pointer<Uint32>),
|
||||
int Function(Pointer<Void>, Pointer<Void>, int, Pointer<Uint32>, Pointer<Uint32>, Pointer<Uint32>)>(
|
||||
'PeekNamedPipe',
|
||||
);
|
||||
|
||||
final _ReadFile = _kernel32.lookupFunction<
|
||||
Int32 Function(Pointer<Void>, Pointer<Void>, Uint32, Pointer<Uint32>, Pointer<Void>),
|
||||
int Function(Pointer<Void>, Pointer<Void>, int, Pointer<Uint32>, Pointer<Void>)>(
|
||||
'ReadFile',
|
||||
);
|
||||
|
||||
final _CloseHandle = _kernel32.lookupFunction<
|
||||
Int32 Function(Pointer<Void>),
|
||||
int Function(Pointer<Void>)>(
|
||||
'CloseHandle',
|
||||
);
|
||||
|
||||
final _WaitForSingleObject = _kernel32.lookupFunction<
|
||||
Uint32 Function(Pointer<Void>, Uint32),
|
||||
int Function(Pointer<Void>, int)>(
|
||||
'WaitForSingleObject',
|
||||
);
|
||||
|
||||
final _GetStdHandle = _kernel32.lookupFunction<
|
||||
IntPtr Function(Int32),
|
||||
int Function(int)>(
|
||||
'GetStdHandle',
|
||||
);
|
||||
|
||||
final _GetExitCodeProcess = _kernel32.lookupFunction<
|
||||
Int32 Function(Pointer<Void>, Pointer<Uint32>),
|
||||
int Function(Pointer<Void>, Pointer<Uint32>)>(
|
||||
'GetExitCodeProcess',
|
||||
);
|
||||
|
||||
final _GetLastError = _kernel32.lookupFunction<
|
||||
Uint32 Function(),
|
||||
int Function()>(
|
||||
'GetLastError',
|
||||
);
|
||||
|
||||
// 🔧 WinINet API for proxy settings - 用于替代 reg 命令,消除黑屏
|
||||
final DynamicLibrary _wininet = DynamicLibrary.open('wininet.dll');
|
||||
|
||||
const int INTERNET_OPTION_PROXY = 38;
|
||||
const int INTERNET_OPEN_TYPE_PROXY = 3;
|
||||
const int INTERNET_OPEN_TYPE_DIRECT = 1;
|
||||
|
||||
final class INTERNET_PROXY_INFO extends Struct {
|
||||
@Int32()
|
||||
external int dwAccessType;
|
||||
|
||||
external Pointer<Utf16> lpszProxy;
|
||||
|
||||
external Pointer<Utf16> lpszProxyBypass;
|
||||
}
|
||||
|
||||
/// WinINet InternetSetOption API - 用于设置系统代理
|
||||
final _InternetSetOptionW = _wininet.lookupFunction<
|
||||
Int32 Function(Pointer<Void>, Uint32, Pointer<Void>, Uint32),
|
||||
int Function(Pointer<Void>, int, Pointer<Void>, int)>(
|
||||
'InternetSetOptionW',
|
||||
);
|
||||
|
||||
/// WinINet InternetQueryOption API - 用于查询系统代理
|
||||
final _InternetQueryOptionW = _wininet.lookupFunction<
|
||||
Int32 Function(Pointer<Void>, Uint32, Pointer<Void>, Pointer<Uint32>),
|
||||
int Function(Pointer<Void>, int, Pointer<Void>, Pointer<Uint32>)>(
|
||||
'InternetQueryOptionW',
|
||||
);
|
||||
510
lib/main.dart
510
lib/main.dart
@ -1,6 +1,8 @@
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
// import 'package:flutter_easyloading/flutter_easyloading.dart'; // 已替换为自定义组件
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -17,12 +19,16 @@ import 'package:kaer_with_panels/app/routes/app_pages.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/kr_local_image.dart';
|
||||
|
||||
import 'package:kaer_with_panels/app/utils/kr_window_manager.dart';
|
||||
|
||||
import 'app/services/singbox_imp/kr_sing_box_imp.dart';
|
||||
import 'app/common/app_run_data.dart';
|
||||
import 'app/utils/kr_secure_storage.dart';
|
||||
import 'app/common/app_config.dart';
|
||||
import 'app/services/kr_site_config_service.dart';
|
||||
import 'app/services/global_overlay_service.dart';
|
||||
import 'app/utils/kr_log_util.dart';
|
||||
import 'app/utils/kr_secure_storage.dart';
|
||||
import 'app/utils/kr_init_log_collector.dart';
|
||||
import 'app/utils/kr_file_logger.dart';
|
||||
|
||||
import 'package:kaer_with_panels/app/routes/transitions/slide_transparent_transition.dart';
|
||||
import 'package:kaer_with_panels/app/routes/transitions/transition_config.dart';
|
||||
@ -31,173 +37,357 @@ import 'package:kaer_with_panels/app/routes/transitions/transition_config.dart';
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
// 🔧 全局异常捕获:捕获所有未处理的异常
|
||||
runZonedGuarded(() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// 防止首帧渲染过早(重点)
|
||||
WidgetsBinding.instance.deferFirstFrame();
|
||||
// 🔧 初始化日志收集器(必须最先初始化)
|
||||
final logCollector = KRInitLogCollector();
|
||||
// 🔧 P6修复: 必须await完成初始化,否则日志会丢失
|
||||
await logCollector.initialize();
|
||||
logCollector.log('🚀 应用启动', tag: 'MAIN');
|
||||
|
||||
// ✅ 等待 Flutter 确保窗口尺寸准备完毕(关键修复)
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
// 🔧 诊断信息: 记录平台和设备信息
|
||||
logCollector.log('平台: ${Platform.operatingSystem}', tag: 'DEVICE');
|
||||
logCollector.log('系统版本: ${Platform.operatingSystemVersion}', tag: 'DEVICE');
|
||||
logCollector.log('Dart版本: ${Platform.version}', tag: 'DEVICE');
|
||||
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
await SystemChrome.setPreferredOrientations(
|
||||
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown],
|
||||
// 🔧 Flutter框架异常捕获
|
||||
FlutterError.onError = (FlutterErrorDetails details) {
|
||||
logCollector.logError(
|
||||
'Flutter框架异常: ${details.exception}',
|
||||
tag: 'FLUTTER_ERROR',
|
||||
error: details.exception,
|
||||
stackTrace: details.stack,
|
||||
);
|
||||
FlutterError.presentError(details);
|
||||
};
|
||||
|
||||
// 🔧 异步异常捕获(PlatformDispatcher)
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
logCollector.logError(
|
||||
'平台异步异常: $error',
|
||||
tag: 'PLATFORM_ERROR',
|
||||
error: error,
|
||||
stackTrace: stack,
|
||||
);
|
||||
return true;
|
||||
};
|
||||
|
||||
await _initializeApp(logCollector);
|
||||
}, (error, stack) {
|
||||
// 🔧 Zone捕获的未处理异常
|
||||
final logCollector = KRInitLogCollector();
|
||||
logCollector.logError(
|
||||
'Zone未处理异常: $error',
|
||||
tag: 'ZONE_ERROR',
|
||||
error: error,
|
||||
stackTrace: stack,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _initializeApp(KRInitLogCollector logCollector) async {
|
||||
try {
|
||||
logCollector.logPhaseStart('初始化核心服务');
|
||||
|
||||
// 🔧 初始化文件日志系统(用于诊断 UI 卡死问题)
|
||||
logCollector.log('初始化文件日志系统', tag: 'FILE_LOGGER');
|
||||
await KRFileLogger.initialize();
|
||||
await KRFileLogger.log('📝 文件日志系统已启动');
|
||||
logCollector.logSuccess('文件日志系统初始化完成', tag: 'FILE_LOGGER');
|
||||
|
||||
// 为所有 HttpClient 请求统一注入代理策略
|
||||
logCollector.log('配置 HTTP 代理策略', tag: 'HTTP');
|
||||
HttpOverrides.global = KRProxyHttpOverrides();
|
||||
|
||||
// 初始化 Hive
|
||||
logCollector.log('初始化 Hive 数据库', tag: 'HIVE');
|
||||
await KRSecureStorage().kr_initHive();
|
||||
logCollector.logSuccess('Hive 初始化完成', tag: 'HIVE');
|
||||
|
||||
// 初始化主题
|
||||
logCollector.log('初始化主题服务', tag: 'THEME');
|
||||
await KRThemeService().init();
|
||||
logCollector.logSuccess('主题初始化完成', tag: 'THEME');
|
||||
|
||||
// 初始化翻译
|
||||
logCollector.log('加载多语言翻译', tag: 'I18N');
|
||||
final translations = GetxTranslations();
|
||||
await translations.loadAllTranslations();
|
||||
logCollector.logSuccess('翻译加载完成', tag: 'I18N');
|
||||
|
||||
// 获取最后保存的语言
|
||||
final initialLocale = await KRLanguageUtils.getLastSavedLocale();
|
||||
logCollector.log('使用语言: ${initialLocale.toString()}', tag: 'I18N');
|
||||
|
||||
logCollector.logPhaseEnd('初始化核心服务', success: true);
|
||||
|
||||
// 启动域名预检测(异步,不阻塞应用启动)
|
||||
logCollector.log('启动域名预检测', tag: 'DOMAIN');
|
||||
KRDomain.kr_preCheckDomains();
|
||||
|
||||
logCollector.log('✅ 核心服务初始化完成,启动应用', tag: 'MAIN');
|
||||
// 🔧 P9修复: 不要立即finalize,让日志文件保持打开以收集后续信息
|
||||
// finalize会在应用关闭或_AppLifecycleWrapper的dispose时调用
|
||||
|
||||
// 初始化全局 Overlay 服务
|
||||
Get.put(GlobalOverlayService());
|
||||
|
||||
// 🔧 关键修复:必须先 runApp(),让 Flutter 引擎启动
|
||||
// 窗口管理器初始化移到 runApp 之后,通过 WidgetsBinding.addPostFrameCallback 延迟执行
|
||||
// 这样可以确保窗口显示时 Flutter UI 已经渲染完成
|
||||
runApp(_myApp(translations, initialLocale));
|
||||
|
||||
// 🔧 在 Flutter 引擎启动后再初始化窗口管理器
|
||||
if (Platform.isMacOS || Platform.isWindows) {
|
||||
// 使用 addPostFrameCallback 确保在首帧渲染后再显示窗口
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
logCollector.logPhaseStart('初始化窗口管理器');
|
||||
await KRWindowManager().kr_initWindowManager();
|
||||
logCollector.logPhaseEnd('初始化窗口管理器', success: true);
|
||||
});
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
logCollector.logError(
|
||||
'应用初始化失败',
|
||||
tag: 'INIT_FATAL',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
await logCollector.finalize();
|
||||
rethrow;
|
||||
}
|
||||
|
||||
// 初始化日志系统
|
||||
KRLogUtil.kr_init();
|
||||
|
||||
// Stripe.publishableKey = 'pk_live_51SbuYxAawOMH8rEEkz6f4mnxAUjGC72eQ6qdm5tT6whC4hULkxxdbiPsB4gSCIMnNIGCsIgeASTXBakUcbOuUwQO00jSWjuufx';
|
||||
// Stripe.merchantIdentifier = 'merchant.com.taw.hifastvpn';
|
||||
// if (Platform.isIOS) {
|
||||
// await Stripe.instance.applySettings();
|
||||
// }
|
||||
// 初始化 Hive
|
||||
await KRSecureStorage().kr_initHive();
|
||||
|
||||
|
||||
// 初始化主题
|
||||
await KRThemeService().init();
|
||||
|
||||
// 初始化翻译
|
||||
final translations = GetxTranslations();
|
||||
await translations.loadAllTranslations();
|
||||
|
||||
// 获取最后保存的语言
|
||||
final initialLocale = await KRLanguageUtils.getLastSavedLocale();
|
||||
|
||||
|
||||
|
||||
|
||||
// 初始化窗口管理器
|
||||
if (Platform.isMacOS || Platform.isWindows) {
|
||||
|
||||
await KRWindowManager().kr_initWindowManager();
|
||||
}
|
||||
|
||||
// 启动域名预检测(异步,不阻塞应用启动)
|
||||
// 立即启动域名检测,不延迟
|
||||
KRDomain.kr_preCheckDomains();
|
||||
|
||||
// 初始化 FMTC
|
||||
// try {
|
||||
// if (Platform.isMacOS) {
|
||||
// final baseDir = await getApplicationSupportDirectory();
|
||||
// await FMTCObjectBoxBackend().initialise(rootDirectory: baseDir.path);
|
||||
// } else {
|
||||
// await FMTCObjectBoxBackend().initialise();
|
||||
// }
|
||||
// // 创建地图存储
|
||||
// await FMTCStore(KRFMTC.kr_storeName).manage.create();
|
||||
// // 初始化地图缓存
|
||||
// await KRFMTC.kr_initMapCache();
|
||||
// } catch (error, stackTrace) {
|
||||
|
||||
// }
|
||||
|
||||
// 初始化全局 Overlay 服务
|
||||
Get.put(GlobalOverlayService());
|
||||
|
||||
runApp(_myApp(translations, initialLocale));
|
||||
WidgetsBinding.instance.allowFirstFrame();
|
||||
}
|
||||
|
||||
Widget _myApp(GetxTranslations translations, Locale initialLocale) {
|
||||
return GetMaterialApp(
|
||||
navigatorKey: navigatorKey, // 使用全局导航键
|
||||
title: "Hi快VPN",
|
||||
initialRoute: Routes.KR_SPLASH,
|
||||
getPages: AppPages.routes,
|
||||
builder: (context, child) {
|
||||
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
|
||||
/// 屏幕适配
|
||||
ScreenUtil.init(context,
|
||||
designSize: const Size(868, 668), minTextAdapt: true);
|
||||
} else {
|
||||
/// 屏幕适配
|
||||
ScreenUtil.init(context,
|
||||
designSize: const Size(375, 667), minTextAdapt: true);
|
||||
}
|
||||
|
||||
// child = FlutterEasyLoading(child: child); // 已替换为自定义组件
|
||||
|
||||
// 添加生命周期监听
|
||||
Widget wrappedChild = Listener(
|
||||
onPointerDown: (_) async {
|
||||
// 确保地图缓存已初始化
|
||||
// try {
|
||||
// final store = FMTCStore(KRFMTC.kr_storeName);
|
||||
// if (!await store.manage.ready) {
|
||||
// await store.manage.create();
|
||||
// await KRFMTC.kr_initMapCache();
|
||||
// }
|
||||
// } catch (e) {
|
||||
// print('地图缓存初始化失败: $e');
|
||||
// }
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
|
||||
// 如果是 Mac 平台,添加顶部安全区域
|
||||
if (Platform.isMacOS) {
|
||||
wrappedChild = MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
padding: MediaQuery.of(context).padding.copyWith(
|
||||
top: 10.w, // Mac 平台顶部安全区域
|
||||
),
|
||||
),
|
||||
child: wrappedChild,
|
||||
return _AppLifecycleWrapper(
|
||||
child: GetMaterialApp(
|
||||
navigatorKey: navigatorKey, // 使用全局导航键
|
||||
title: "Hi快VPN",
|
||||
initialRoute: Routes.KR_SPLASH,
|
||||
getPages: AppPages.routes,
|
||||
builder: (context, child) {
|
||||
// 🔧 P2修复: 将ScreenUtil初始化移到StatefulWidget中,避免重复初始化
|
||||
return _ScreenUtilInitializer(
|
||||
child: child ?? Container(),
|
||||
);
|
||||
}
|
||||
|
||||
// 使用 Stack 布局来设置背景
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// 背景层:使用您的自定义组件
|
||||
const KrLocalImage(
|
||||
imageName: 'global-bg',
|
||||
imageType: ImageType.jpg,
|
||||
fit: BoxFit.cover, // 确保背景覆盖整个屏幕
|
||||
),
|
||||
// 内容层
|
||||
wrappedChild,
|
||||
Overlay(
|
||||
initialEntries: [
|
||||
OverlayEntry(builder: (_) => const SizedBox.shrink()), // 初始化空 Overlay
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
theme: KRThemeService().kr_lightTheme(),
|
||||
darkTheme: KRThemeService().kr_darkTheme(),
|
||||
themeMode: KRThemeService().kr_Theme,
|
||||
translations: translations,
|
||||
locale: initialLocale,
|
||||
fallbackLocale: const Locale('zh', 'CN'),
|
||||
debugShowCheckedModeBanner: false,
|
||||
transitionDuration: TransitionConfig.defaultDuration, // 设置动画持续时间
|
||||
customTransition: TransitionConfig.createDefaultTransition(),
|
||||
routingCallback: (routing) {
|
||||
if (routing == null) return;
|
||||
if(Routes.KR_PURCHASE_MEMBERSHIP.contains(routing.current)) return;
|
||||
// 需要显示订阅按钮的路由列表
|
||||
const showButtonRoutes = [
|
||||
Routes.MR_LOGIN,
|
||||
Routes.HI_MENU,
|
||||
Routes.KR_HOME,
|
||||
Routes.HI_USER_INFO,
|
||||
Routes.KR_ORDER_STATUS,
|
||||
];
|
||||
print('routing.current${routing.current}');
|
||||
GlobalOverlayService.instance.updateSubscriptionButtonColor(null);
|
||||
if (showButtonRoutes.contains(routing.current)) {
|
||||
GlobalOverlayService.instance.safeShowSubscriptionButton();
|
||||
} else {
|
||||
GlobalOverlayService.instance.hideSubscriptionButton();
|
||||
}
|
||||
},
|
||||
},
|
||||
theme: KRThemeService().kr_lightTheme(),
|
||||
darkTheme: KRThemeService().kr_darkTheme(),
|
||||
themeMode: KRThemeService().kr_Theme,
|
||||
translations: translations,
|
||||
locale: initialLocale,
|
||||
fallbackLocale: const Locale('zh', 'CN'),
|
||||
debugShowCheckedModeBanner: false,
|
||||
transitionDuration: TransitionConfig.defaultDuration, // 设置动画持续时间
|
||||
customTransition: TransitionConfig.createDefaultTransition(),
|
||||
routingCallback: (routing) {
|
||||
if (routing == null) return;
|
||||
if(Routes.KR_PURCHASE_MEMBERSHIP.contains(routing.current)) return;
|
||||
// 需要显示订阅按钮的路由列表
|
||||
const showButtonRoutes = [
|
||||
Routes.MR_LOGIN,
|
||||
Routes.HI_MENU,
|
||||
Routes.KR_HOME,
|
||||
Routes.HI_USER_INFO,
|
||||
Routes.KR_ORDER_STATUS,
|
||||
];
|
||||
print('routing.current${routing.current}');
|
||||
GlobalOverlayService.instance.updateSubscriptionButtonColor(null);
|
||||
if (showButtonRoutes.contains(routing.current)) {
|
||||
GlobalOverlayService.instance.safeShowSubscriptionButton();
|
||||
} else {
|
||||
GlobalOverlayService.instance.hideSubscriptionButton();
|
||||
}
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// 🔧 P2修复: ScreenUtil初始化包装器,避免重复初始化
|
||||
/// 🔧 P0修复: 使用静态变量确保全局只初始化一次,防止Widget重建导致的状态丢失
|
||||
class _ScreenUtilInitializer extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const _ScreenUtilInitializer({required this.child});
|
||||
|
||||
@override
|
||||
State<_ScreenUtilInitializer> createState() => _ScreenUtilInitializerState();
|
||||
}
|
||||
|
||||
class _ScreenUtilInitializerState extends State<_ScreenUtilInitializer> {
|
||||
// 🔧 P0修复: 使用静态变量,确保即使Widget重建也不会重复初始化
|
||||
static bool _isGlobalInitialized = false;
|
||||
// 🔧 P0修复: 记录初始化时的屏幕尺寸,用于检测屏幕变化
|
||||
static Size? _lastScreenSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
final currentSize = mediaQuery.size;
|
||||
|
||||
// 🔧 P0修复: 只在以下情况初始化:
|
||||
// 1. 从未初始化过
|
||||
// 2. 屏幕尺寸发生显著变化(如旋转屏幕)
|
||||
final needsInit = !_isGlobalInitialized ||
|
||||
(_lastScreenSize != null &&
|
||||
((_lastScreenSize!.width - currentSize.width).abs() > 50 ||
|
||||
(_lastScreenSize!.height - currentSize.height).abs() > 50));
|
||||
|
||||
if (needsInit) {
|
||||
// 🔧 P9修复: 记录屏幕信息(延迟记录,确保能写入文件)
|
||||
if (kDebugMode || AppConfig.enableInitLogCollection) {
|
||||
// 延迟记录,确保在UI线程空闲时执行
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final logCollector = KRInitLogCollector();
|
||||
logCollector.log('━━━ 屏幕信息诊断 ━━━', tag: 'SCREEN');
|
||||
logCollector.log('屏幕尺寸: ${mediaQuery.size.width} x ${mediaQuery.size.height}', tag: 'SCREEN');
|
||||
logCollector.log('设备像素比: ${mediaQuery.devicePixelRatio}', tag: 'SCREEN');
|
||||
logCollector.log('文本缩放: ${mediaQuery.textScaler.scale(1.0)}', tag: 'SCREEN');
|
||||
logCollector.log('安全区域: top=${mediaQuery.padding.top}, bottom=${mediaQuery.padding.bottom}', tag: 'SCREEN');
|
||||
});
|
||||
}
|
||||
|
||||
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
|
||||
// 🔧 修复:设计尺寸改为实际窗口尺寸 800x668,确保字体和UI元素正确缩放
|
||||
ScreenUtil.init(context,
|
||||
designSize: const Size(800, 668), minTextAdapt: true);
|
||||
if (kDebugMode || AppConfig.enableInitLogCollection) {
|
||||
KRInitLogCollector().log('ScreenUtil初始化: 桌面模式 800x668', tag: 'SCREEN');
|
||||
}
|
||||
} else {
|
||||
ScreenUtil.init(context,
|
||||
designSize: const Size(375, 667), minTextAdapt: true);
|
||||
if (kDebugMode || AppConfig.enableInitLogCollection) {
|
||||
KRInitLogCollector().log('ScreenUtil初始化: 移动模式 375x667', tag: 'SCREEN');
|
||||
}
|
||||
}
|
||||
_isGlobalInitialized = true;
|
||||
_lastScreenSize = currentSize;
|
||||
}
|
||||
|
||||
// 如果是 Mac 平台,添加顶部安全区域
|
||||
Widget wrappedChild = widget.child;
|
||||
if (Platform.isMacOS) {
|
||||
wrappedChild = MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
padding: MediaQuery.of(context).padding.copyWith(
|
||||
top: 10.w, // Mac 平台顶部安全区域
|
||||
),
|
||||
),
|
||||
child: wrappedChild,
|
||||
);
|
||||
}
|
||||
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// 背景层:使用您的自定义组件
|
||||
const KrLocalImage(
|
||||
imageName: 'global-bg',
|
||||
imageType: ImageType.jpg,
|
||||
fit: BoxFit.cover, // 确保背景覆盖整个屏幕
|
||||
),
|
||||
// 内容层
|
||||
wrappedChild,
|
||||
Overlay(
|
||||
initialEntries: [
|
||||
OverlayEntry(builder: (_) => const SizedBox.shrink()), // 初始化空 Overlay
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔧 P3修复: 应用生命周期监听包装器
|
||||
class _AppLifecycleWrapper extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const _AppLifecycleWrapper({required this.child});
|
||||
|
||||
@override
|
||||
State<_AppLifecycleWrapper> createState() => _AppLifecycleWrapperState();
|
||||
}
|
||||
|
||||
class _AppLifecycleWrapperState extends State<_AppLifecycleWrapper>
|
||||
with WidgetsBindingObserver {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// 🔧 P9修复: 应用关闭时finalize日志文件
|
||||
KRInitLogCollector().finalize();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
|
||||
if (kDebugMode) {
|
||||
print('🔄 应用生命周期变化: $state');
|
||||
}
|
||||
|
||||
// 🔧 P3修复: 当应用从后台恢复时,重置关键服务
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
if (kDebugMode) {
|
||||
print('♻️ 应用恢复,开始重置关键服务...');
|
||||
}
|
||||
_resetCriticalServices();
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔧 P3修复: 重置关键服务,防止状态污染
|
||||
Future<void> _resetCriticalServices() async {
|
||||
try {
|
||||
// 🔧 P1修复: 重置主题服务
|
||||
await KRThemeService().reset();
|
||||
if (kDebugMode) {
|
||||
print('✅ 主题服务已重置');
|
||||
}
|
||||
|
||||
// 🔧 P1修复: 重置应用运行时数据
|
||||
await KRAppRunData.getInstance().kr_resetRuntimeState();
|
||||
if (kDebugMode) {
|
||||
print('✅ 应用运行时数据已重置');
|
||||
}
|
||||
|
||||
// 🔧 P1修复: 清理域名检测状态
|
||||
KRDomain.kr_resetDomainState();
|
||||
if (kDebugMode) {
|
||||
print('✅ 域名状态已重置');
|
||||
}
|
||||
|
||||
// 刷新UI
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('⚠️ 服务重置失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.child;
|
||||
}
|
||||
}
|
||||
|
||||
/// 全局 HttpOverrides,确保所有 dart:io 网络请求遵循 sing-box 代理策略
|
||||
class KRProxyHttpOverrides extends HttpOverrides {
|
||||
@override
|
||||
HttpClient createHttpClient(SecurityContext? context) {
|
||||
final client = super.createHttpClient(context);
|
||||
client.findProxy = (uri) => KRSingBoxImp.instance.kr_buildProxyRule();
|
||||
return client;
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,7 @@ import 'dart:convert';
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'package:combine/combine.dart';
|
||||
import 'package:kaer_with_panels/utils/isolate_worker.dart';
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:kaer_with_panels/core/model/directories.dart';
|
||||
@ -50,6 +50,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
@override
|
||||
Future<void> init() async {
|
||||
loggy.debug("initializing");
|
||||
_box.setupOnce(NativeApi.initializeApiDLData);
|
||||
_statusReceiver = ReceivePort('service status receiver');
|
||||
final source = _statusReceiver.asBroadcastStream().map((event) => jsonDecode(event as String)).map(SingboxStatus.fromEvent);
|
||||
_status = ValueConnectableStream.seeded(
|
||||
@ -60,162 +61,197 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
|
||||
@override
|
||||
TaskEither<String, Unit> setup(
|
||||
Directories directories,
|
||||
bool debug,
|
||||
) {
|
||||
Directories directories,
|
||||
bool debug,
|
||||
) {
|
||||
final port = _statusReceiver.sendPort.nativePort;
|
||||
return TaskEither(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
_box.setupOnce(NativeApi.initializeApiDLData);
|
||||
final err = _box
|
||||
.setup(
|
||||
directories.baseDir.path.toNativeUtf8().cast(),
|
||||
directories.workingDir.path.toNativeUtf8().cast(),
|
||||
directories.tempDir.path.toNativeUtf8().cast(),
|
||||
port,
|
||||
debug ? 1 : 0,
|
||||
)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
return left(err);
|
||||
}
|
||||
return right(unit);
|
||||
},
|
||||
),
|
||||
);
|
||||
final baseDir = directories.baseDir.path;
|
||||
final workingDir = directories.workingDir.path;
|
||||
final tempDir = directories.tempDir.path;
|
||||
final debugFlag = debug ? 1 : 0;
|
||||
|
||||
return TaskEither(() async {
|
||||
try {
|
||||
final startTime = DateTime.now();
|
||||
_logger.debug('[黑屏调试] setup() 开始调用 libcore.dll - $startTime');
|
||||
|
||||
final err = await IsolateWorker().execute(
|
||||
() => _ffiSetup(baseDir, workingDir, tempDir, port, debugFlag),
|
||||
allowSyncFallback: false,
|
||||
);
|
||||
|
||||
final endTime = DateTime.now();
|
||||
final durationMs = endTime.difference(startTime).inMilliseconds;
|
||||
_logger.debug('[黑屏调试] setup() 完成(耗时: ${durationMs}ms)');
|
||||
|
||||
if (err != null && err.isNotEmpty) {
|
||||
_logger.error('[黑屏调试] setup() 错误: $err');
|
||||
return left(err);
|
||||
}
|
||||
return right(unit);
|
||||
} catch (e) {
|
||||
_logger.error('[黑屏调试] setup() 异常: $e');
|
||||
return left(e.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<String, Unit> validateConfigByPath(
|
||||
String path,
|
||||
String tempPath,
|
||||
bool debug,
|
||||
) {
|
||||
return TaskEither(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final err = _box
|
||||
.parse(
|
||||
path.toNativeUtf8().cast(),
|
||||
tempPath.toNativeUtf8().cast(),
|
||||
debug ? 1 : 0,
|
||||
)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
return left(err);
|
||||
}
|
||||
return right(unit);
|
||||
},
|
||||
),
|
||||
);
|
||||
String path,
|
||||
String tempPath,
|
||||
bool debug,
|
||||
) {
|
||||
final debugFlag = debug ? 1 : 0;
|
||||
return TaskEither(() async {
|
||||
try {
|
||||
final err = await IsolateWorker().execute(
|
||||
() => _ffiValidateConfig(path, tempPath, debugFlag),
|
||||
allowSyncFallback: false,
|
||||
);
|
||||
if (err != null && err.isNotEmpty) {
|
||||
return left(err);
|
||||
}
|
||||
return right(unit);
|
||||
} catch (e) {
|
||||
return left(e.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<String, Unit> changeOptions(SingboxConfigOption options) {
|
||||
return TaskEither(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final json = jsonEncode(options.toJson());
|
||||
final err = _box.changeHiddifyOptions(json.toNativeUtf8().cast()).cast<Utf8>().toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
return left(err);
|
||||
}
|
||||
return right(unit);
|
||||
},
|
||||
),
|
||||
);
|
||||
final json = jsonEncode(options.toJson());
|
||||
return TaskEither(() async {
|
||||
try {
|
||||
final startTime = DateTime.now();
|
||||
_logger.debug('[黑屏调试] changeOptions 开始调用 libcore.dll - $startTime');
|
||||
|
||||
final err = await IsolateWorker().execute(
|
||||
() => _ffiChangeOptions(json),
|
||||
allowSyncFallback: false,
|
||||
);
|
||||
|
||||
final endTime = DateTime.now();
|
||||
final durationMs = endTime.difference(startTime).inMilliseconds;
|
||||
_logger.debug('[黑屏调试] changeOptions 完成(耗时: ${durationMs}ms)');
|
||||
|
||||
if (err != null && err.isNotEmpty) {
|
||||
_logger.error('[黑屏调试] changeOptions 错误: $err');
|
||||
return left(err);
|
||||
}
|
||||
return right(unit);
|
||||
} catch (e) {
|
||||
_logger.error('[黑屏调试] changeOptions 异常: $e');
|
||||
return left(e.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<String, String> generateFullConfigByPath(
|
||||
String path,
|
||||
) {
|
||||
return TaskEither(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final response = _box
|
||||
.generateConfig(
|
||||
path.toNativeUtf8().cast(),
|
||||
)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
if (response.startsWith("error")) {
|
||||
return left(response.replaceFirst("error", ""));
|
||||
}
|
||||
return right(response);
|
||||
},
|
||||
),
|
||||
);
|
||||
String path,
|
||||
) {
|
||||
return TaskEither(() async {
|
||||
try {
|
||||
final result = await IsolateWorker().execute(
|
||||
() => _ffiGenerateFullConfig(path),
|
||||
allowSyncFallback: false,
|
||||
);
|
||||
final ok = result.isNotEmpty && result[0] == true;
|
||||
final payload = result.length > 1 ? result[1] as String : '';
|
||||
if (!ok) {
|
||||
return left(payload);
|
||||
}
|
||||
return right(payload);
|
||||
} catch (e) {
|
||||
return left(e.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<String, Unit> start(
|
||||
String configPath,
|
||||
String name,
|
||||
bool disableMemoryLimit,
|
||||
) {
|
||||
String configPath,
|
||||
String name,
|
||||
bool disableMemoryLimit,
|
||||
) {
|
||||
loggy.debug("starting, memory limit: [${!disableMemoryLimit}]");
|
||||
return TaskEither(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final err = _box
|
||||
.start(
|
||||
configPath.toNativeUtf8().cast(),
|
||||
disableMemoryLimit ? 1 : 0,
|
||||
)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
return left(err);
|
||||
}
|
||||
return right(unit);
|
||||
},
|
||||
),
|
||||
);
|
||||
return TaskEither(() async {
|
||||
try {
|
||||
final startTime = DateTime.now();
|
||||
_logger.debug('[黑屏调试] start() 开始调用 libcore.dll - $startTime');
|
||||
|
||||
final err = await IsolateWorker().execute(
|
||||
() => _ffiStart(configPath, disableMemoryLimit),
|
||||
allowSyncFallback: false,
|
||||
);
|
||||
|
||||
final endTime = DateTime.now();
|
||||
final durationMs = endTime.difference(startTime).inMilliseconds;
|
||||
_logger.debug('[黑屏调试] start() 完成(耗时: ${durationMs}ms)');
|
||||
|
||||
if (err != null && err.isNotEmpty) {
|
||||
_logger.error('[黑屏调试] start() 错误: $err');
|
||||
return left(err);
|
||||
}
|
||||
return right(unit);
|
||||
} catch (e) {
|
||||
_logger.error('[黑屏调试] start() 异常: $e');
|
||||
return left(e.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<String, Unit> stop() {
|
||||
return TaskEither(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final err = _box.stop().cast<Utf8>().toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
return left(err);
|
||||
}
|
||||
return right(unit);
|
||||
},
|
||||
),
|
||||
);
|
||||
return TaskEither(() async {
|
||||
try {
|
||||
final startTime = DateTime.now();
|
||||
_logger.debug('[黑屏调试] stop() 开始调用 libcore.dll - $startTime');
|
||||
|
||||
final err = await IsolateWorker().execute(
|
||||
_ffiStop,
|
||||
allowSyncFallback: false,
|
||||
);
|
||||
|
||||
final endTime = DateTime.now();
|
||||
final durationMs = endTime.difference(startTime).inMilliseconds;
|
||||
_logger.debug('[黑屏调试] stop() 完成(耗时: ${durationMs}ms)');
|
||||
|
||||
if (err != null && err.isNotEmpty) {
|
||||
_logger.error('[黑屏调试] stop() 错误: $err');
|
||||
return left(err);
|
||||
}
|
||||
return right(unit);
|
||||
} catch (e) {
|
||||
_logger.error('[黑屏调试] stop() 异常: $e');
|
||||
return left(e.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<String, Unit> restart(
|
||||
String configPath,
|
||||
String name,
|
||||
bool disableMemoryLimit,
|
||||
) {
|
||||
String configPath,
|
||||
String name,
|
||||
bool disableMemoryLimit,
|
||||
) {
|
||||
loggy.debug("restarting, memory limit: [${!disableMemoryLimit}]");
|
||||
return TaskEither(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final err = _box
|
||||
.restart(
|
||||
configPath.toNativeUtf8().cast(),
|
||||
disableMemoryLimit ? 1 : 0,
|
||||
)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
return left(err);
|
||||
}
|
||||
return right(unit);
|
||||
},
|
||||
),
|
||||
);
|
||||
return TaskEither(() async {
|
||||
try {
|
||||
final err = await IsolateWorker().execute(
|
||||
() => _ffiRestart(configPath, disableMemoryLimit),
|
||||
allowSyncFallback: false,
|
||||
);
|
||||
if (err != null && err.isNotEmpty) {
|
||||
return left(err);
|
||||
}
|
||||
return right(unit);
|
||||
} catch (e) {
|
||||
return left(e.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@ -243,7 +279,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
_serviceStatsStream = null;
|
||||
},
|
||||
).map(
|
||||
(event) {
|
||||
(event) {
|
||||
if (event case String _) {
|
||||
if (event.startsWith('error:')) {
|
||||
loggy.error("[service stats client] error received: $event");
|
||||
@ -283,7 +319,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
}
|
||||
},
|
||||
).map(
|
||||
(event) {
|
||||
(event) {
|
||||
if (event case String _) {
|
||||
if (event.startsWith('error:')) {
|
||||
logger.error("error received: $event");
|
||||
@ -327,7 +363,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
}
|
||||
},
|
||||
).map(
|
||||
(event) {
|
||||
(event) {
|
||||
if (event case String _) {
|
||||
if (event.startsWith('error:')) {
|
||||
logger.error(event);
|
||||
@ -359,38 +395,38 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
|
||||
@override
|
||||
TaskEither<String, Unit> selectOutbound(String groupTag, String outboundTag) {
|
||||
return TaskEither(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final err = _box
|
||||
.selectOutbound(
|
||||
groupTag.toNativeUtf8().cast(),
|
||||
outboundTag.toNativeUtf8().cast(),
|
||||
)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
return left(err);
|
||||
}
|
||||
return right(unit);
|
||||
},
|
||||
),
|
||||
);
|
||||
return TaskEither(() async {
|
||||
try {
|
||||
final err = await IsolateWorker().execute(
|
||||
() => _ffiSelectOutbound(groupTag, outboundTag),
|
||||
allowSyncFallback: false,
|
||||
);
|
||||
if (err != null && err.isNotEmpty) {
|
||||
return left(err);
|
||||
}
|
||||
return right(unit);
|
||||
} catch (e) {
|
||||
return left(e.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<String, Unit> urlTest(String groupTag) {
|
||||
return TaskEither(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final err = _box.urlTest(groupTag.toNativeUtf8().cast()).cast<Utf8>().toDartString();
|
||||
if (err.isNotEmpty) {
|
||||
return left(err);
|
||||
}
|
||||
return right(unit);
|
||||
},
|
||||
),
|
||||
);
|
||||
return TaskEither(() async {
|
||||
try {
|
||||
final err = await IsolateWorker().execute(
|
||||
() => _ffiUrlTest(groupTag),
|
||||
allowSyncFallback: false,
|
||||
);
|
||||
if (err != null && err.isNotEmpty) {
|
||||
return left(err);
|
||||
}
|
||||
return right(unit);
|
||||
} catch (e) {
|
||||
return left(e.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final _logBuffer = <String>[];
|
||||
@ -409,14 +445,10 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
|
||||
@override
|
||||
TaskEither<String, Unit> clearLogs() {
|
||||
return TaskEither(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
_logBuffer.clear();
|
||||
return right(unit);
|
||||
},
|
||||
),
|
||||
);
|
||||
return TaskEither(() async {
|
||||
_logBuffer.clear();
|
||||
return right(unit);
|
||||
});
|
||||
}
|
||||
|
||||
Future<List<String>> _readLogFile(File file) async {
|
||||
@ -443,23 +475,165 @@ class FFISingboxService with InfraLogger implements SingboxService {
|
||||
required String previousAccessToken,
|
||||
}) {
|
||||
loggy.debug("generating warp config");
|
||||
return TaskEither(
|
||||
() => CombineWorker().execute(
|
||||
() {
|
||||
final response = _box
|
||||
.generateWarpConfig(
|
||||
licenseKey.toNativeUtf8().cast(),
|
||||
previousAccountId.toNativeUtf8().cast(),
|
||||
previousAccessToken.toNativeUtf8().cast(),
|
||||
)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
if (response.startsWith("error:")) {
|
||||
return left(response.replaceFirst('error:', ""));
|
||||
}
|
||||
return right(warpFromJson(jsonDecode(response)));
|
||||
},
|
||||
),
|
||||
);
|
||||
return TaskEither(() async {
|
||||
try {
|
||||
final result = await IsolateWorker().execute(
|
||||
() => _ffiGenerateWarpConfig(licenseKey, previousAccountId, previousAccessToken),
|
||||
allowSyncFallback: false,
|
||||
);
|
||||
final ok = result.isNotEmpty && result[0] == true;
|
||||
final payload = result.length > 1 ? result[1] as String : '';
|
||||
if (!ok) {
|
||||
return left(payload);
|
||||
}
|
||||
return right(warpFromJson(jsonDecode(payload)));
|
||||
} catch (e) {
|
||||
return left(e.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
SingboxNativeLibrary _ffiLoadLibrary() {
|
||||
String fullPath = "";
|
||||
if (Platform.environment.containsKey('FLUTTER_TEST')) {
|
||||
fullPath = "libcore";
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
fullPath = p.join(fullPath, "libcore.dll");
|
||||
} else if (Platform.isMacOS) {
|
||||
fullPath = p.join(fullPath, "libcore.dylib");
|
||||
} else {
|
||||
fullPath = p.join(fullPath, "libcore.so");
|
||||
}
|
||||
final lib = DynamicLibrary.open(fullPath);
|
||||
final box = SingboxNativeLibrary(lib);
|
||||
box.setupOnce(NativeApi.initializeApiDLData);
|
||||
return box;
|
||||
}
|
||||
|
||||
String? _ffiSetup(
|
||||
String baseDir,
|
||||
String workingDir,
|
||||
String tempDir,
|
||||
int statusPort,
|
||||
int debugFlag,
|
||||
) {
|
||||
final box = _ffiLoadLibrary();
|
||||
final err = box
|
||||
.setup(
|
||||
baseDir.toNativeUtf8().cast(),
|
||||
workingDir.toNativeUtf8().cast(),
|
||||
tempDir.toNativeUtf8().cast(),
|
||||
statusPort,
|
||||
debugFlag,
|
||||
)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
return err.isEmpty ? null : err;
|
||||
}
|
||||
|
||||
String? _ffiValidateConfig(
|
||||
String path,
|
||||
String tempPath,
|
||||
int debugFlag,
|
||||
) {
|
||||
final box = _ffiLoadLibrary();
|
||||
final err = box
|
||||
.parse(
|
||||
path.toNativeUtf8().cast(),
|
||||
tempPath.toNativeUtf8().cast(),
|
||||
debugFlag,
|
||||
)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
return err.isEmpty ? null : err;
|
||||
}
|
||||
|
||||
String? _ffiChangeOptions(String optionsJson) {
|
||||
final box = _ffiLoadLibrary();
|
||||
final err = box.changeHiddifyOptions(optionsJson.toNativeUtf8().cast()).cast<Utf8>().toDartString();
|
||||
return err.isEmpty ? null : err;
|
||||
}
|
||||
|
||||
List<Object?> _ffiGenerateFullConfig(String path) {
|
||||
final box = _ffiLoadLibrary();
|
||||
final response = box
|
||||
.generateConfig(
|
||||
path.toNativeUtf8().cast(),
|
||||
)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
if (response.startsWith("error")) {
|
||||
return [false, response.replaceFirst("error", "")];
|
||||
}
|
||||
return [true, response];
|
||||
}
|
||||
|
||||
String? _ffiStart(String configPath, bool disableMemoryLimit) {
|
||||
final box = _ffiLoadLibrary();
|
||||
final err = box
|
||||
.start(
|
||||
configPath.toNativeUtf8().cast(),
|
||||
disableMemoryLimit ? 1 : 0,
|
||||
)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
return err.isEmpty ? null : err;
|
||||
}
|
||||
|
||||
String? _ffiStop() {
|
||||
final box = _ffiLoadLibrary();
|
||||
final err = box.stop().cast<Utf8>().toDartString();
|
||||
return err.isEmpty ? null : err;
|
||||
}
|
||||
|
||||
String? _ffiRestart(String configPath, bool disableMemoryLimit) {
|
||||
final box = _ffiLoadLibrary();
|
||||
final err = box
|
||||
.restart(
|
||||
configPath.toNativeUtf8().cast(),
|
||||
disableMemoryLimit ? 1 : 0,
|
||||
)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
return err.isEmpty ? null : err;
|
||||
}
|
||||
|
||||
String? _ffiSelectOutbound(String groupTag, String outboundTag) {
|
||||
final box = _ffiLoadLibrary();
|
||||
final err = box
|
||||
.selectOutbound(
|
||||
groupTag.toNativeUtf8().cast(),
|
||||
outboundTag.toNativeUtf8().cast(),
|
||||
)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
return err.isEmpty ? null : err;
|
||||
}
|
||||
|
||||
String? _ffiUrlTest(String groupTag) {
|
||||
final box = _ffiLoadLibrary();
|
||||
final err = box.urlTest(groupTag.toNativeUtf8().cast()).cast<Utf8>().toDartString();
|
||||
return err.isEmpty ? null : err;
|
||||
}
|
||||
|
||||
List<Object?> _ffiGenerateWarpConfig(
|
||||
String licenseKey,
|
||||
String previousAccountId,
|
||||
String previousAccessToken,
|
||||
) {
|
||||
final box = _ffiLoadLibrary();
|
||||
final response = box
|
||||
.generateWarpConfig(
|
||||
licenseKey.toNativeUtf8().cast(),
|
||||
previousAccountId.toNativeUtf8().cast(),
|
||||
previousAccessToken.toNativeUtf8().cast(),
|
||||
)
|
||||
.cast<Utf8>()
|
||||
.toDartString();
|
||||
if (response.startsWith("error:")) {
|
||||
return [false, response.replaceFirst("error:", "")];
|
||||
}
|
||||
return [true, response];
|
||||
}
|
||||
|
||||
21
lib/utils/isolate_worker.dart
Normal file
21
lib/utils/isolate_worker.dart
Normal file
@ -0,0 +1,21 @@
|
||||
import 'dart:async';
|
||||
import 'dart:isolate';
|
||||
|
||||
/// Simple worker that executes functions in a separate isolate.
|
||||
/// Replacement for combine package's CombineWorker.
|
||||
class IsolateWorker {
|
||||
/// Execute a function in a separate isolate and return the result.
|
||||
///
|
||||
/// Note: The function must be a top-level function or a static method,
|
||||
/// and it cannot capture non-sendable objects from the surrounding scope.
|
||||
Future<T> execute<T>(T Function() computation, {bool allowSyncFallback = false}) async {
|
||||
try {
|
||||
return await Isolate.run(computation);
|
||||
} catch (e) {
|
||||
if (!allowSyncFallback) {
|
||||
rethrow;
|
||||
}
|
||||
return computation();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1517,6 +1517,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
synchronized:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: synchronized
|
||||
sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.0+3"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@ -58,6 +58,7 @@ dependencies:
|
||||
fpdart: ^1.1.0
|
||||
dartx: ^1.2.0
|
||||
rxdart: ^0.27.7
|
||||
synchronized: ^3.0.1 # 🔧 P0-4: 用于线程安全的互斥锁保护
|
||||
combine: 0.5.7 # 精确版本,兼容 Flutter 3.24.3(与 hiddify-app 相同)
|
||||
encrypt: ^5.0.0
|
||||
path: ^1.8.3
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user