Compare commits

...

1 Commits

Author SHA1 Message Date
3f429e5546 feat: 保存配置 2026-01-07 17:33:55 -08:00
17 changed files with 4124 additions and 1271 deletions

View File

@ -31,7 +31,7 @@ class KRDomain {
// static String kr_currentDomain = "apicn.bearvpn.top"; // static String kr_currentDomain = "apicn.bearvpn.top";
static List<String> kr_baseDomains = ["api.hifast.biz", "api.airovpn.tel",]; 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 = [ static List<String> kr_backupDomainUrls = [
@ -427,12 +427,6 @@ class KRDomain {
/// ///
static Future<void> kr_preCheckDomains() async { static Future<void> kr_preCheckDomains() async {
// Debug
// if (kDebugMode) {
// KRLogUtil.kr_i('🐛 Debug 模式,跳过域名预检测', tag: 'KRDomain');
// return;
// }
KRLogUtil.kr_i('🚀 开始预检测域名可用性', tag: 'KRDomain'); KRLogUtil.kr_i('🚀 开始预检测域名可用性', tag: 'KRDomain');
// //

View File

@ -77,6 +77,24 @@ class KRAppRunData {
return kr_account.value != null && kr_account.value!.startsWith('9000'); 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.1Token格式是否有效 /// 🔧 2.1Token格式是否有效
/// Token是否符合JWT格式header.payload.signature /// Token是否符合JWT格式header.payload.signature
/// Token数据 /// Token数据

View File

@ -16,7 +16,6 @@ import '../../../localization/app_translations.dart';
import '../../../localization/kr_language_utils.dart'; import '../../../localization/kr_language_utils.dart';
import '../../../model/business/kr_group_outbound_list.dart'; import '../../../model/business/kr_group_outbound_list.dart';
import '../../../model/business/kr_outbound_item.dart'; import '../../../model/business/kr_outbound_item.dart';
import '../../../services/kr_announcement_service.dart';
import '../../../utils/kr_event_bus.dart'; import '../../../utils/kr_event_bus.dart';
import '../../../utils/kr_update_util.dart'; import '../../../utils/kr_update_util.dart';
import '../../../widgets/dialogs/kr_dialog.dart'; import '../../../widgets/dialogs/kr_dialog.dart';
@ -87,6 +86,15 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
// //
final kr_isConnected = false.obs; 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; final kr_isLatency = false.obs;
@ -116,6 +124,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
// 🔧 // 🔧
DateTime? _lastStatusChangeTime; DateTime? _lastStatusChangeTime;
String? _lastStoppedErrorSignature;
Timer? _statusWatchdogTimer; Timer? _statusWatchdogTimer;
// //
@ -311,12 +320,19 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
// //
_bindConnectionStatus(); _bindConnectionStatus();
// Hiddify
// /
_setupConnectionModeListener();
// //
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
// 🔧 // 🔧
_restoreSelectedNode(); _restoreSelectedNode();
//
//
// //
Future.delayed(const Duration(milliseconds: 500), () { Future.delayed(const Duration(milliseconds: 500), () {
kr_forceSyncConnectionStatus(true); kr_forceSyncConnectionStatus(true);
@ -327,6 +343,10 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
_startStatusWatchdog(); _startStatusWatchdog();
} }
/// Windows kr_updateConnectionType
/// TUN
///
/// 🔧 /// 🔧
Future<void> _restoreSelectedNode() async { Future<void> _restoreSelectedNode() async {
try { try {
@ -593,7 +613,6 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
kr_currentViewStatus.value = KRHomeViewsStatus.kr_loggedIn; kr_currentViewStatus.value = KRHomeViewsStatus.kr_loggedIn;
KRLogUtil.kr_i('登录状态变化:设置为已登录', tag: 'HomeController'); KRLogUtil.kr_i('登录状态变化:设置为已登录', tag: 'HomeController');
KRAnnouncementService().kr_checkAnnouncement();
// splash // splash
KRLogUtil.kr_i('订阅服务已在启动页初始化,跳过重复初始化', tag: 'HomeController'); KRLogUtil.kr_i('订阅服务已在启动页初始化,跳过重复初始化', tag: 'HomeController');
@ -748,7 +767,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
_lastStatusChangeTime = DateTime.now(); _lastStatusChangeTime = DateTime.now();
switch (status) { switch (status) {
case SingboxStopped(): case SingboxStopped(:final alert, :final message):
KRLogUtil.kr_i('🔴 状态: 已停止', tag: 'HomeController'); KRLogUtil.kr_i('🔴 状态: 已停止', tag: 'HomeController');
kr_connectText.value = AppTranslations.kr_home.disconnected; kr_connectText.value = AppTranslations.kr_home.disconnected;
kr_stopConnectionTimer(); kr_stopConnectionTimer();
@ -758,12 +777,17 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
kr_currentSpeed.value = "--"; kr_currentSpeed.value = "--";
kr_isLatency.value = false; kr_isLatency.value = false;
kr_isConnected.value = false; kr_isConnected.value = false;
// kr_isToggleBusy .then()
kr_isToggleBusy.value = false;
kr_currentNodeLatency.value = -2; kr_currentNodeLatency.value = -2;
// isConnected // isConnected
kr_isConnected.refresh(); kr_isConnected.refresh();
_maybeShowStoppedError(alert: alert, message: message);
break; break;
case SingboxStarting(): case SingboxStarting():
KRLogUtil.kr_i('🟡 状态: 正在启动', tag: 'HomeController'); KRLogUtil.kr_i('🟡 状态: 正在启动', tag: 'HomeController');
// stopped
_lastStoppedErrorSignature = null;
kr_connectText.value = AppTranslations.kr_home.connecting; kr_connectText.value = AppTranslations.kr_home.connecting;
kr_currentSpeed.value = AppTranslations.kr_home.connecting; kr_currentSpeed.value = AppTranslations.kr_home.connecting;
kr_currentNodeLatency.value = -1; kr_currentNodeLatency.value = -1;
@ -773,7 +797,6 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
break; break;
case SingboxStarted(): case SingboxStarted():
KRLogUtil.kr_i('🟢 状态: 已启动', tag: 'HomeController'); KRLogUtil.kr_i('🟢 状态: 已启动', tag: 'HomeController');
if (kDebugMode) {}
// //
_cancelConnectionTimeout(); _cancelConnectionTimeout();
@ -782,13 +805,13 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
kr_updateConnectionInfo(); kr_updateConnectionInfo();
kr_isLatency.value = false; kr_isLatency.value = false;
kr_isConnected.value = true; kr_isConnected.value = true;
// kr_isToggleBusy .then()
kr_isToggleBusy.value = false;
// 🔧 -1(),0() // 🔧 -1(),0()
if (kr_currentNodeLatency.value == -1) { if (kr_currentNodeLatency.value == -1) {
if (kDebugMode) {}
kr_currentNodeLatency.value = 0; kr_currentNodeLatency.value = 0;
kr_currentNodeLatency.refresh(); // kr_currentNodeLatency.refresh(); //
if (kDebugMode) {}
} }
// 🔧 // 🔧
@ -798,7 +821,6 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
kr_isConnected.refresh(); kr_isConnected.refresh();
// UI // UI
update(); update();
if (kDebugMode) {}
break; break;
case SingboxStopping(): case SingboxStopping():
KRLogUtil.kr_i('🟠 状态: 正在停止', tag: 'HomeController'); 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=$newTypelastSuccessful=$_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 /// 🔧 : hiddify-app toggleConnection
Future<void> kr_toggleSwitch(bool value) async { Future<void> kr_toggleSwitch(bool value) async {
final currentStatus = KRSingBoxImp.instance.kr_status.value; 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( KRLogUtil.kr_i(
'🔵 toggleSwitch 被调用: value=$value, currentStatus=$currentStatus', '🔵 toggleSwitch 被调用: value=$value, currentStatus=$currentStatus',
tag: 'HomeController'); tag: 'HomeController');
@ -910,15 +1011,13 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
return; return;
} }
} }
kr_isToggleBusy.value = true;
try {
// 🔧 : // 🔧 :
_lastStatusChangeTime = DateTime.now(); _lastStatusChangeTime = DateTime.now();
if (value) { if (value) {
// //
KRLogUtil.kr_i('🔄 开始连接...', tag: 'HomeController'); KRLogUtil.kr_i('🔄 开始连接...', tag: 'HomeController');
if (kDebugMode) {}
await _kr_prepareCountrySelectionBeforeStart(); await _kr_prepareCountrySelectionBeforeStart();
final selectedAfter = final selectedAfter =
await KRSecureStorage().kr_readData(key: 'SELECTED_NODE_TAG'); await KRSecureStorage().kr_readData(key: 'SELECTED_NODE_TAG');
@ -931,79 +1030,72 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
// KRLogUtil.kr_i('准备后 kr_cutSeletedTag: ${kr_cutSeletedTag.value}', // KRLogUtil.kr_i('准备后 kr_cutSeletedTag: ${kr_cutSeletedTag.value}',
// tag: 'HomeController'); // tag: 'HomeController');
await kr_performNodeSwitch(selectedAfter!); await kr_performNodeSwitch(selectedAfter!);
await KRSingBoxImp.instance.kr_start(); // VPN 使 .then().catchError()
KRLogUtil.kr_i('✅ 连接命令已发送', tag: 'HomeController'); // UI 线
if (kDebugMode) {} 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();
// 🔧 : 3 Get.snackbar(
await _waitForStatus(SingboxStatus.started().runtimeType, '加载失败',
maxSeconds: 3); '切换 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 { } else {
// //
KRLogUtil.kr_i('🛑 开始断开连接...', tag: 'HomeController'); KRLogUtil.kr_i('🛑 开始断开连接...', tag: 'HomeController');
if (kDebugMode) {}
await KRSingBoxImp.instance.kr_stop().timeout( // VPN 使 .then().catchError()
const Duration(seconds: 10), KRSingBoxImp.instance.kr_stop().then((_) {
onTimeout: () { KRLogUtil.kr_i('✅ 停止命令已返回DNS/proxy 恢复可能还在进行中)', tag: 'HomeController');
KRLogUtil.kr_e('⚠️ 停止操作超时', tag: 'HomeController'); // UI status stream
throw TimeoutException('Stop operation timeout'); // 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'); });
if (kDebugMode) {}
// 🔧 : KRLogUtil.kr_i('✅ 断开命令已发送(异步执行)', tag: 'HomeController');
final success = await _waitForStatus(
SingboxStatus.stopped().runtimeType, // 使 catchError
maxSeconds: 3, Future.delayed(const Duration(seconds: 15)).then((_) {
); if (kr_isToggleBusy.value) {
if (!success) { KRLogUtil.kr_w('⚠️ 关闭超时 15 秒,强制释放开关锁', tag: 'HomeController');
// kr_isToggleBusy.value = false;
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
kr_forceSyncConnectionStatus();
// 🔧
_lastStatusChangeTime = DateTime.now();
return true; //
}
await Future.delayed(const Duration(milliseconds: 100));
}
// 🔧 :
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 { Future<void> _kr_prepareCountrySelectionBeforeStart() async {
@ -1461,92 +1553,45 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
kr_currentNodeLatency.value = -1; kr_currentNodeLatency.value = -1;
kr_isLatency.value = true; // kr_isLatency.value = true; //
// 🔧 // 🚀 A
KRLogUtil.kr_i('💾 保存新节点选择: $tag', tag: 'HomeController'); // 使 KRSingBoxImp.kr_selectOutbound() selectOutbound API
await KRSecureStorage() //
.kr_saveData(key: 'SELECTED_NODE_TAG', value: tag); // - 3
// -
// - 20 urltest
KRLogUtil.kr_i('🔄 [方案A] 调用 KRSingBoxImp.kr_selectOutbound()...', tag: 'HomeController');
// 🚀 使 selectOutbound hiddify-app // UI响应性
// VPNVPN开关不闪烁
KRLogUtil.kr_i('🔄 [热切换] 调用 selectOutbound 切换节点...',
tag: 'HomeController');
// 🔧 selector tag
// selectOutbound(groupTag, outboundTag) - tagtag
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
kr_cutSeletedTag.value = tag; kr_cutSeletedTag.value = tag;
kr_updateConnectionInfo(); kr_updateConnectionInfo();
// kr_moveToSelectedNode(); kr_moveToSelectedNode();
// 🔧
KRLogUtil.kr_i('⏳ [热切换] 等待内核状态同步200ms...', tag: 'HomeController');
await Future.delayed(const Duration(milliseconds: 200));
// 🔍
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(
'📊 [验证] ${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');
}
} catch (e) {
KRLogUtil.kr_w('⚠️ [验证] 节点验证过程出错: $e', tag: 'HomeController');
}
//
// 使 .then() UI
KRSingBoxImp.instance.kr_selectOutbound(tag).then((_) {
KRLogUtil.kr_i('✅ 节点热切换成功,已启动保护机制: $tag', tag: 'HomeController');
// //
_kr_updateLatencyOnConnected(); _kr_updateLatencyOnConnected();
}).catchError((error) {
KRLogUtil.kr_e('❌ 节点切换失败: $error', tag: 'HomeController');
KRLogUtil.kr_i('✅ 节点热切换成功VPN保持连接: $tag', tag: 'HomeController'); //
KRLogUtil.kr_i('🔄 节点选择失败,恢复到原节点: $originalTag', tag: 'HomeController');
kr_cutTag.value = originalTag;
kr_currentNodeName.value = originalTag;
kr_cutSeletedTag.value = originalTag;
//
try {
KRSecureStorage().kr_saveData(key: 'SELECTED_NODE_TAG', value: originalTag);
} catch (e) {
KRLogUtil.kr_e('❌ 恢复节点选择失败: $e', tag: 'HomeController');
}
KRCommonUtil.kr_showToast('节点切换失败,已恢复为: $originalTag');
});
KRLogUtil.kr_i('✅ 节点热切换请求已发送VPN保持连接: $tag', tag: 'HomeController');
return true; return true;
} catch (switchError) { } catch (switchError) {
// //
@ -2556,11 +2601,55 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
} }
} }
} }
// 🔧
_kr_checkAndRefreshExpiredSubscribe();
}); });
KRLogUtil.kr_i('✅ 状态监控定时器已启动', tag: 'HomeController'); 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; 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() { void _cancelConnectionTimeout() {
_connectionTimeoutTimer?.cancel(); _connectionTimeoutTimer?.cancel();
_connectionTimeoutTimer = null; _connectionTimeoutTimer = null;
@ -2660,7 +2789,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
return delays; return delays;
} }
/// /// reduce loops and logs
bool _kr_tryUpdateDelayFromActiveGroups() { bool _kr_tryUpdateDelayFromActiveGroups() {
try { try {
final activeGroups = KRSingBoxImp.instance.kr_activeGroups; final activeGroups = KRSingBoxImp.instance.kr_activeGroups;
@ -2670,19 +2799,30 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
return false; return false;
} }
// selector // auto
final targetTag = kr_cutTag.value == "auto" ? "auto" : kr_cutTag.value;
// selector 退
for (var group in activeGroups) { for (var group in activeGroups) {
if (group.type == ProxyType.selector) { if (group.type == ProxyType.selector) {
KRLogUtil.kr_d('找到 selector 组: ${group.tag}, 选中: ${group.selected}', try {
tag: 'HomeController'); // 使 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; kr_currentNodeLatency.value = item.urlTestDelay;
KRLogUtil.kr_i('✅ 延迟值: ${item.urlTestDelay}ms',
tag: 'HomeController'); if (kDebugMode) {
return true; KRLogUtil.kr_d(
'✅ 延迟更新: $targetTag = ${item.urlTestDelay}ms',
tag: 'HomeController',
);
} }
return true; //
} catch (_) {
//
continue;
} }
} }
} }

View File

@ -85,8 +85,8 @@ class HttpUtil {
client.findProxy = (url) { client.findProxy = (url) {
try { try {
// SingBox // SingBox
final singBoxStatus = KRSingBoxImp.instance.kr_status; final singBoxStatus = KRSingBoxImp.instance.kr_status.value;
final isProxyAvailable = singBoxStatus == SingboxStatus.started(); final isProxyAvailable = singBoxStatus is SingboxStarted;;
if (!isProxyAvailable) { if (!isProxyAvailable) {
// 使 // 使

View File

@ -225,17 +225,21 @@ class KRDeviceInfoService {
} }
/// Windows设备ID - 使GUID /// Windows设备ID - 使GUID
/// 🔧 使 flutter_udid wmic
Future<String> _getWindowsDeviceId() async { Future<String> _getWindowsDeviceId() async {
try { try {
final windowsInfo = await _deviceInfo.windowsInfo; final windowsInfo = await _deviceInfo.windowsInfo;
// 使 flutter_udid // 🔧 使 FlutterUdid.consistentUdid
String udid = await FlutterUdid.consistentUdid; // Windows "cmd.exe /c wmic csproduct get UUID"
// 使 CREATE_NO_WINDOW
// 使 device_info_plus
// windowsInfo.deviceId
// //
final factors = [ final factors = [
udid, windowsInfo.deviceId, // ID ( MachineGuid)
windowsInfo.deviceId, // ID
windowsInfo.computerName, // windowsInfo.computerName, //
windowsInfo.productName, // windowsInfo.productName, //
windowsInfo.numberOfCores.toString(), // CPU核心数 windowsInfo.numberOfCores.toString(), // CPU核心数

File diff suppressed because it is too large Load Diff

View File

@ -15,10 +15,30 @@ class KRThemeService extends GetxService {
final String _key = 'themeOption'; // final String _key = 'themeOption'; //
late ThemeMode _currentThemeOption = ThemeMode.light; // late ThemeMode _currentThemeOption = ThemeMode.light; //
// 🔧 P0修复: ,使
bool _isInitialized = false;
/// ///
Future<void> init() async { Future<void> init() async {
try {
_currentThemeOption = await kr_loadThemeOptionFromStorage(); _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 { ThemeMode get kr_Theme {
@ -142,18 +162,6 @@ class KRThemeService extends GetxService {
return Colors.transparent; // 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, //
),
// //
); );
} }

View 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);
}
}

View 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) {
//
}
}
}

View File

@ -26,11 +26,11 @@ class KRWindowManager with WindowListener, TrayListener {
const WindowOptions windowOptions = WindowOptions( const WindowOptions windowOptions = WindowOptions(
size: Size(800, 668), size: Size(800, 668),
minimumSize: Size(400, 334), minimumSize: Size(800, 668),
center: true, center: true,
backgroundColor: Colors.white, backgroundColor: Colors.white,
skipTaskbar: false, skipTaskbar: false,
title: 'Hi快VPN', title: 'Kaer VPN',
titleBarStyle: TitleBarStyle.normal, titleBarStyle: TitleBarStyle.normal,
windowButtonVisibility: true, windowButtonVisibility: true,
); );
@ -47,16 +47,17 @@ class KRWindowManager with WindowListener, TrayListener {
await windowManager.setTitleBarStyle(TitleBarStyle.normal); await windowManager.setTitleBarStyle(TitleBarStyle.normal);
await windowManager.setTitle('HiFastVPN'); await windowManager.setTitle('HiFastVPN');
await windowManager.setSize(const Size(800, 668)); 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.center();
await windowManager.show(); await windowManager.show();
// //
await windowManager.setPreventClose(true); await windowManager.setPreventClose(true);
} else { } else {
await windowManager.setTitle('HiFastVPN'); await windowManager.setTitle('Kaer VPN');
await windowManager.setSize(const Size(800, 668)); 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.center();
await windowManager.show(); // macOS
} }
// //
@ -94,7 +95,7 @@ class KRWindowManager with WindowListener, TrayListener {
/// ///
void _initPlatformChannel() { void _initPlatformChannel() {
if (Platform.isMacOS) { if (Platform.isMacOS) {
const platform = MethodChannel('hifast_vpn/terminate'); const platform = MethodChannel('kaer_vpn/terminate');
platform.setMethodCallHandler((call) async { platform.setMethodCallHandler((call) async {
if (call.method == 'onTerminate') { if (call.method == 'onTerminate') {
KRLogUtil.kr_i('收到应用终止通知'); KRLogUtil.kr_i('收到应用终止通知');
@ -149,8 +150,66 @@ class KRWindowManager with WindowListener, TrayListener {
} }
/// 退 /// 退
///
Future<void> _exitApp() async { Future<void> _exitApp() async {
KRLogUtil.kr_i('_exitApp: 退出应用'); 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 _handleTerminate();
await windowManager.destroy(); await windowManager.destroy();
} }
@ -159,9 +218,9 @@ class KRWindowManager with WindowListener, TrayListener {
Future<void> _showWindow() async { Future<void> _showWindow() async {
KRLogUtil.kr_i('_showWindow: 开始显示窗口'); KRLogUtil.kr_i('_showWindow: 开始显示窗口');
try { try {
await windowManager.setSkipTaskbar(false);
await windowManager.show(); await windowManager.show();
await windowManager.focus(); await windowManager.focus();
await windowManager.setSkipTaskbar(false);
await windowManager.setAlwaysOnTop(true); await windowManager.setAlwaysOnTop(true);
await Future.delayed(const Duration(milliseconds: 100)); await Future.delayed(const Duration(milliseconds: 100));
await windowManager.setAlwaysOnTop(false); await windowManager.setAlwaysOnTop(false);
@ -180,6 +239,7 @@ class KRWindowManager with WindowListener, TrayListener {
@override @override
void onWindowClose() async { void onWindowClose() async {
if (Platform.isWindows) { if (Platform.isWindows) {
await windowManager.setSkipTaskbar(true);
await windowManager.hide(); await windowManager.hide();
} else if (Platform.isMacOS) { } else if (Platform.isMacOS) {
await windowManager.hide(); await windowManager.hide();
@ -216,9 +276,29 @@ class KRWindowManager with WindowListener, TrayListener {
/// ///
Future<void> _handleTerminate() async { Future<void> _handleTerminate() async {
KRLogUtil.kr_i('_handleTerminate: 处理应用终止'); KRLogUtil.kr_i('_handleTerminate: 处理应用终止');
if (KRSingBoxImp.instance.kr_status == SingboxStatus.started()) {
// 🔧 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(); 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(); await trayManager.destroy();
KRLogUtil.kr_i('✅ 托盘已销毁', tag: 'WindowManager');
} catch (e) {
KRLogUtil.kr_w('⚠️ 销毁托盘出错: $e', tag: 'WindowManager');
}
} }
} }

View File

@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart'; 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 /// Windows DNS
/// ///
@ -38,6 +39,8 @@ class KRWindowsDnsUtil {
} }
try { try {
// 🔒 5
return await Future.value(() async {
KRLogUtil.kr_i('📦 开始备份 Windows DNS 设置...', tag: 'WindowsDNS'); KRLogUtil.kr_i('📦 开始备份 Windows DNS 设置...', tag: 'WindowsDNS');
// 1. // 1.
@ -51,15 +54,33 @@ class KRWindowsDnsUtil {
// 2. DNS // 2. DNS
final dnsServers = await _kr_getCurrentDnsServers(interfaceName); final dnsServers = await _kr_getCurrentDnsServers(interfaceName);
if (dnsServers.isEmpty) {
KRLogUtil.kr_w('⚠️ 当前 DNS 为空,可能是自动获取', tag: 'WindowsDNS'); // 🔧 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 _originalDnsServers = []; // DHCP
} else { } else {
_originalDnsServers = dnsServers; _originalDnsServers = validDnsServers;
KRLogUtil.kr_i('✅ 已备份 DNS: ${dnsServers.join(", ")}', tag: 'WindowsDNS'); KRLogUtil.kr_i('✅ 已备份有效 DNS: ${validDnsServers.join(", ")}', tag: 'WindowsDNS');
if (dnsServers.length != validDnsServers.length) {
KRLogUtil.kr_i(' (已过滤掉 ${dnsServers.length - validDnsServers.length} 个本地地址)', tag: 'WindowsDNS');
}
} }
return true; return true;
}()).timeout(
const Duration(seconds: 5),
onTimeout: () {
KRLogUtil.kr_w('⏱️ DNS 备份操作超时5秒跳过备份', tag: 'WindowsDNS');
return false;
},
);
} catch (e) { } catch (e) {
KRLogUtil.kr_e('❌ 备份 DNS 设置失败: $e', tag: 'WindowsDNS'); KRLogUtil.kr_e('❌ 备份 DNS 设置失败: $e', tag: 'WindowsDNS');
return false; return false;
@ -80,22 +101,33 @@ class KRWindowsDnsUtil {
try { try {
KRLogUtil.kr_i('🔄 开始恢复 Windows DNS 设置...', tag: 'WindowsDNS'); KRLogUtil.kr_i('🔄 开始恢复 Windows DNS 设置...', tag: 'WindowsDNS');
// 1. // 🔧 P1修复:
if (_primaryInterfaceName == null) { final currentInterface = await _kr_getPrimaryNetworkInterface();
KRLogUtil.kr_w('⚠️ 没有备份的网络接口,尝试自动检测', tag: 'WindowsDNS'); if (currentInterface == null) {
_primaryInterfaceName = await _kr_getPrimaryNetworkInterface(); KRLogUtil.kr_e('❌ 无法检测当前网络接口,执行兜底恢复', tag: 'WindowsDNS');
if (_primaryInterfaceName == null) {
KRLogUtil.kr_e('❌ 无法检测网络接口,执行兜底恢复', tag: 'WindowsDNS');
return await _kr_fallbackRestoreDns(); return await _kr_fallbackRestoreDns();
} }
//
if (_primaryInterfaceName != null && _primaryInterfaceName != currentInterface) {
KRLogUtil.kr_w('⚠️ 网络接口已变化: $_primaryInterfaceName$currentInterface', tag: 'WindowsDNS');
KRLogUtil.kr_w(' 执行兜底恢复以确保当前接口DNS正常', tag: 'WindowsDNS');
_primaryInterfaceName = currentInterface; //
return await _kr_fallbackRestoreDns();
} }
// 2. DNS // 使
_primaryInterfaceName = currentInterface;
KRLogUtil.kr_i('🔍 当前网络接口: $_primaryInterfaceName', tag: 'WindowsDNS');
// 1. DNS
if (_originalDnsServers == null) { if (_originalDnsServers == null) {
KRLogUtil.kr_w('⚠️ 没有备份的 DNS执行兜底恢复', tag: 'WindowsDNS'); KRLogUtil.kr_w('⚠️ 没有备份的 DNS执行兜底恢复', tag: 'WindowsDNS');
return await _kr_fallbackRestoreDns(); return await _kr_fallbackRestoreDns();
} }
// 2. DNS
if (_originalDnsServers!.isEmpty) { if (_originalDnsServers!.isEmpty) {
// DHCP // DHCP
KRLogUtil.kr_i('🔄 恢复为 DHCP 自动获取 DNS', tag: 'WindowsDNS'); KRLogUtil.kr_i('🔄 恢复为 DHCP 自动获取 DNS', tag: 'WindowsDNS');
@ -130,6 +162,15 @@ class KRWindowsDnsUtil {
return await _kr_fallbackRestoreDns(); 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; return true;
} catch (e) { } catch (e) {
KRLogUtil.kr_e('❌ 恢复 DNS 设置失败: $e', tag: 'WindowsDNS'); KRLogUtil.kr_e('❌ 恢复 DNS 设置失败: $e', tag: 'WindowsDNS');
@ -187,7 +228,7 @@ class KRWindowsDnsUtil {
Future<String?> _kr_getPrimaryNetworkInterface() async { Future<String?> _kr_getPrimaryNetworkInterface() async {
try { try {
// 使 netsh // 使 netsh
final result = await Process.run('netsh', ['interface', 'show', 'interface']); final result = await KRWindowsProcessUtil.runHidden('netsh', ['interface', 'show', 'interface']);
if (result.exitCode != 0) { if (result.exitCode != 0) {
KRLogUtil.kr_e('❌ 获取网络接口失败: ${result.stderr}', tag: 'WindowsDNS'); KRLogUtil.kr_e('❌ 获取网络接口失败: ${result.stderr}', tag: 'WindowsDNS');
@ -271,7 +312,7 @@ class KRWindowsDnsUtil {
/// DNS /// DNS
Future<List<String>> _kr_getCurrentDnsServers(String interfaceName) async { Future<List<String>> _kr_getCurrentDnsServers(String interfaceName) async {
try { try {
final result = await Process.run('netsh', [ final result = await KRWindowsProcessUtil.runHidden('netsh', [
'interface', 'interface',
'ipv4', 'ipv4',
'show', 'show',
@ -294,12 +335,11 @@ class KRWindowsDnsUtil {
final ipMatch = RegExp(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b').firstMatch(line); final ipMatch = RegExp(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b').firstMatch(line);
if (ipMatch != null) { if (ipMatch != null) {
final ip = ipMatch.group(0)!; final ip = ipMatch.group(0)!;
// // 🔧 127.0.0.1便DNS是否还在使用sing-box的本地DNS
if (!ip.startsWith('127.')) { // DNS时126127.0.0.1
dnsServers.add(ip); dnsServers.add(ip);
} }
} }
}
KRLogUtil.kr_d('🔍 当前 DNS: ${dnsServers.join(", ")}', tag: 'WindowsDNS'); KRLogUtil.kr_d('🔍 当前 DNS: ${dnsServers.join(", ")}', tag: 'WindowsDNS');
return dnsServers; return dnsServers;
@ -325,7 +365,7 @@ class KRWindowsDnsUtil {
// 1. DNS // 1. DNS
KRLogUtil.kr_i('🔧 设置主 DNS: ${dnsServers[0]}', tag: 'WindowsDNS'); KRLogUtil.kr_i('🔧 设置主 DNS: ${dnsServers[0]}', tag: 'WindowsDNS');
var result = await Process.run('netsh', [ var result = await KRWindowsProcessUtil.runHidden('netsh', [
'interface', 'interface',
'ipv4', 'ipv4',
'set', 'set',
@ -345,7 +385,7 @@ class KRWindowsDnsUtil {
if (dnsServers.length > 1) { if (dnsServers.length > 1) {
for (int i = 1; i < dnsServers.length; i++) { for (int i = 1; i < dnsServers.length; i++) {
KRLogUtil.kr_i('🔧 设置备用 DNS ${i}: ${dnsServers[i]}', tag: 'WindowsDNS'); KRLogUtil.kr_i('🔧 设置备用 DNS ${i}: ${dnsServers[i]}', tag: 'WindowsDNS');
result = await Process.run('netsh', [ result = await KRWindowsProcessUtil.runHidden('netsh', [
'interface', 'interface',
'ipv4', 'ipv4',
'add', 'add',
@ -384,7 +424,7 @@ class KRWindowsDnsUtil {
try { try {
KRLogUtil.kr_i('🔧 设置 DNS 为自动获取 (DHCP)', tag: 'WindowsDNS'); KRLogUtil.kr_i('🔧 设置 DNS 为自动获取 (DHCP)', tag: 'WindowsDNS');
final result = await Process.run('netsh', [ final result = await KRWindowsProcessUtil.runHidden('netsh', [
'interface', 'interface',
'ipv4', 'ipv4',
'set', 'set',
@ -416,7 +456,7 @@ class KRWindowsDnsUtil {
try { try {
KRLogUtil.kr_i('🔄 刷新 DNS 缓存...', tag: 'WindowsDNS'); KRLogUtil.kr_i('🔄 刷新 DNS 缓存...', tag: 'WindowsDNS');
final result = await Process.run('ipconfig', ['/flushdns']); final result = await KRWindowsProcessUtil.runHidden('ipconfig', ['/flushdns']);
if (result.exitCode == 0) { if (result.exitCode == 0) {
KRLogUtil.kr_i('✅ DNS 缓存已刷新', tag: 'WindowsDNS'); 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;
}
}
/// ///
/// ///
/// 退 /// 退

View 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',
);

View File

@ -1,6 +1,8 @@
import 'dart:io'; import 'dart:io';
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
// import 'package:flutter_easyloading/flutter_easyloading.dart'; // // import 'package:flutter_easyloading/flutter_easyloading.dart'; //
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter/services.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/widgets/kr_local_image.dart';
import 'package:kaer_with_panels/app/utils/kr_window_manager.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/utils/kr_secure_storage.dart';
import 'app/common/app_config.dart'; import 'app/common/app_config.dart';
import 'app/services/kr_site_config_service.dart'; import 'app/services/kr_site_config_service.dart';
import 'app/services/global_overlay_service.dart'; import 'app/services/global_overlay_service.dart';
import 'app/utils/kr_log_util.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/slide_transparent_transition.dart';
import 'package:kaer_with_panels/app/routes/transitions/transition_config.dart'; import 'package:kaer_with_panels/app/routes/transitions/transition_config.dart';
@ -31,144 +37,140 @@ import 'package:kaer_with_panels/app/routes/transitions/transition_config.dart';
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
void main() async { void main() async {
// 🔧
runZonedGuarded(() async {
WidgetsFlutterBinding.ensureInitialized(); 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) { // 🔧 Flutter框架异常捕获
await SystemChrome.setPreferredOrientations( FlutterError.onError = (FlutterErrorDetails details) {
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown], 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 {
KRLogUtil.kr_init(); 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();
// Stripe.publishableKey = 'pk_live_51SbuYxAawOMH8rEEkz6f4mnxAUjGC72eQ6qdm5tT6whC4hULkxxdbiPsB4gSCIMnNIGCsIgeASTXBakUcbOuUwQO00jSWjuufx';
// Stripe.merchantIdentifier = 'merchant.com.taw.hifastvpn';
// if (Platform.isIOS) {
// await Stripe.instance.applySettings();
// }
// Hive // Hive
logCollector.log('初始化 Hive 数据库', tag: 'HIVE');
await KRSecureStorage().kr_initHive(); await KRSecureStorage().kr_initHive();
logCollector.logSuccess('Hive 初始化完成', tag: 'HIVE');
// //
logCollector.log('初始化主题服务', tag: 'THEME');
await KRThemeService().init(); await KRThemeService().init();
logCollector.logSuccess('主题初始化完成', tag: 'THEME');
// //
logCollector.log('加载多语言翻译', tag: 'I18N');
final translations = GetxTranslations(); final translations = GetxTranslations();
await translations.loadAllTranslations(); await translations.loadAllTranslations();
logCollector.logSuccess('翻译加载完成', tag: 'I18N');
// //
final initialLocale = await KRLanguageUtils.getLastSavedLocale(); final initialLocale = await KRLanguageUtils.getLastSavedLocale();
logCollector.log('使用语言: ${initialLocale.toString()}', tag: 'I18N');
logCollector.logPhaseEnd('初始化核心服务', success: true);
//
if (Platform.isMacOS || Platform.isWindows) {
await KRWindowManager().kr_initWindowManager();
}
// //
// logCollector.log('启动域名预检测', tag: 'DOMAIN');
KRDomain.kr_preCheckDomains(); KRDomain.kr_preCheckDomains();
// FMTC logCollector.log('✅ 核心服务初始化完成,启动应用', tag: 'MAIN');
// try { // 🔧 P9修复: finalize,
// if (Platform.isMacOS) { // finalize会在应用关闭或_AppLifecycleWrapper的dispose时调用
// 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 // Overlay
Get.put(GlobalOverlayService()); Get.put(GlobalOverlayService());
// 🔧 runApp() Flutter
// runApp WidgetsBinding.addPostFrameCallback
// Flutter UI
runApp(_myApp(translations, initialLocale)); runApp(_myApp(translations, initialLocale));
WidgetsBinding.instance.allowFirstFrame();
// 🔧 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;
}
} }
Widget _myApp(GetxTranslations translations, Locale initialLocale) { Widget _myApp(GetxTranslations translations, Locale initialLocale) {
return GetMaterialApp( return _AppLifecycleWrapper(
child: GetMaterialApp(
navigatorKey: navigatorKey, // 使 navigatorKey: navigatorKey, // 使
title: "Hi快VPN", title: "Hi快VPN",
initialRoute: Routes.KR_SPLASH, initialRoute: Routes.KR_SPLASH,
getPages: AppPages.routes, getPages: AppPages.routes,
builder: (context, child) { builder: (context, child) {
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { // 🔧 P2修复: ScreenUtil初始化移到StatefulWidget中,
/// return _ScreenUtilInitializer(
ScreenUtil.init(context, child: child ?? Container(),
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,
);
}
// 使 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(), theme: KRThemeService().kr_lightTheme(),
@ -199,5 +201,193 @@ Widget _myApp(GetxTranslations translations, Locale initialLocale) {
GlobalOverlayService.instance.hideSubscriptionButton(); 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) {
// 🔧 800x668UI元素正确缩放
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;
}
}

View File

@ -3,7 +3,7 @@ import 'dart:convert';
import 'dart:ffi'; import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'package:combine/combine.dart'; import 'package:kaer_with_panels/utils/isolate_worker.dart';
import 'package:ffi/ffi.dart'; import 'package:ffi/ffi.dart';
import 'package:fpdart/fpdart.dart'; import 'package:fpdart/fpdart.dart';
import 'package:kaer_with_panels/core/model/directories.dart'; import 'package:kaer_with_panels/core/model/directories.dart';
@ -50,6 +50,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
@override @override
Future<void> init() async { Future<void> init() async {
loggy.debug("initializing"); loggy.debug("initializing");
_box.setupOnce(NativeApi.initializeApiDLData);
_statusReceiver = ReceivePort('service status receiver'); _statusReceiver = ReceivePort('service status receiver');
final source = _statusReceiver.asBroadcastStream().map((event) => jsonDecode(event as String)).map(SingboxStatus.fromEvent); final source = _statusReceiver.asBroadcastStream().map((event) => jsonDecode(event as String)).map(SingboxStatus.fromEvent);
_status = ValueConnectableStream.seeded( _status = ValueConnectableStream.seeded(
@ -64,27 +65,35 @@ class FFISingboxService with InfraLogger implements SingboxService {
bool debug, bool debug,
) { ) {
final port = _statusReceiver.sendPort.nativePort; final port = _statusReceiver.sendPort.nativePort;
return TaskEither( final baseDir = directories.baseDir.path;
() => CombineWorker().execute( final workingDir = directories.workingDir.path;
() { final tempDir = directories.tempDir.path;
_box.setupOnce(NativeApi.initializeApiDLData); final debugFlag = debug ? 1 : 0;
final err = _box
.setup( return TaskEither(() async {
directories.baseDir.path.toNativeUtf8().cast(), try {
directories.workingDir.path.toNativeUtf8().cast(), final startTime = DateTime.now();
directories.tempDir.path.toNativeUtf8().cast(), _logger.debug('[黑屏调试] setup() 开始调用 libcore.dll - $startTime');
port,
debug ? 1 : 0, final err = await IsolateWorker().execute(
) () => _ffiSetup(baseDir, workingDir, tempDir, port, debugFlag),
.cast<Utf8>() allowSyncFallback: false,
.toDartString(); );
if (err.isNotEmpty) {
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 left(err);
} }
return right(unit); return right(unit);
}, } catch (e) {
), _logger.error('[黑屏调试] setup() 异常: $e');
); return left(e.toString());
}
});
} }
@override @override
@ -93,62 +102,72 @@ class FFISingboxService with InfraLogger implements SingboxService {
String tempPath, String tempPath,
bool debug, bool debug,
) { ) {
return TaskEither( final debugFlag = debug ? 1 : 0;
() => CombineWorker().execute( return TaskEither(() async {
() { try {
final err = _box final err = await IsolateWorker().execute(
.parse( () => _ffiValidateConfig(path, tempPath, debugFlag),
path.toNativeUtf8().cast(), allowSyncFallback: false,
tempPath.toNativeUtf8().cast(), );
debug ? 1 : 0, if (err != null && err.isNotEmpty) {
)
.cast<Utf8>()
.toDartString();
if (err.isNotEmpty) {
return left(err); return left(err);
} }
return right(unit); return right(unit);
}, } catch (e) {
), return left(e.toString());
); }
});
} }
@override @override
TaskEither<String, Unit> changeOptions(SingboxConfigOption options) { TaskEither<String, Unit> changeOptions(SingboxConfigOption options) {
return TaskEither(
() => CombineWorker().execute(
() {
final json = jsonEncode(options.toJson()); final json = jsonEncode(options.toJson());
final err = _box.changeHiddifyOptions(json.toNativeUtf8().cast()).cast<Utf8>().toDartString(); return TaskEither(() async {
if (err.isNotEmpty) { 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 left(err);
} }
return right(unit); return right(unit);
}, } catch (e) {
), _logger.error('[黑屏调试] changeOptions 异常: $e');
); return left(e.toString());
}
});
} }
@override @override
TaskEither<String, String> generateFullConfigByPath( TaskEither<String, String> generateFullConfigByPath(
String path, String path,
) { ) {
return TaskEither( return TaskEither(() async {
() => CombineWorker().execute( try {
() { final result = await IsolateWorker().execute(
final response = _box () => _ffiGenerateFullConfig(path),
.generateConfig( allowSyncFallback: false,
path.toNativeUtf8().cast(),
)
.cast<Utf8>()
.toDartString();
if (response.startsWith("error")) {
return left(response.replaceFirst("error", ""));
}
return right(response);
},
),
); );
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 @override
@ -158,38 +177,58 @@ class FFISingboxService with InfraLogger implements SingboxService {
bool disableMemoryLimit, bool disableMemoryLimit,
) { ) {
loggy.debug("starting, memory limit: [${!disableMemoryLimit}]"); loggy.debug("starting, memory limit: [${!disableMemoryLimit}]");
return TaskEither( return TaskEither(() async {
() => CombineWorker().execute( try {
() { final startTime = DateTime.now();
final err = _box _logger.debug('[黑屏调试] start() 开始调用 libcore.dll - $startTime');
.start(
configPath.toNativeUtf8().cast(), final err = await IsolateWorker().execute(
disableMemoryLimit ? 1 : 0, () => _ffiStart(configPath, disableMemoryLimit),
) allowSyncFallback: false,
.cast<Utf8>() );
.toDartString();
if (err.isNotEmpty) { 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 left(err);
} }
return right(unit); return right(unit);
}, } catch (e) {
), _logger.error('[黑屏调试] start() 异常: $e');
); return left(e.toString());
}
});
} }
@override @override
TaskEither<String, Unit> stop() { TaskEither<String, Unit> stop() {
return TaskEither( return TaskEither(() async {
() => CombineWorker().execute( try {
() { final startTime = DateTime.now();
final err = _box.stop().cast<Utf8>().toDartString(); _logger.debug('[黑屏调试] stop() 开始调用 libcore.dll - $startTime');
if (err.isNotEmpty) {
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 left(err);
} }
return right(unit); return right(unit);
}, } catch (e) {
), _logger.error('[黑屏调试] stop() 异常: $e');
); return left(e.toString());
}
});
} }
@override @override
@ -199,23 +238,20 @@ class FFISingboxService with InfraLogger implements SingboxService {
bool disableMemoryLimit, bool disableMemoryLimit,
) { ) {
loggy.debug("restarting, memory limit: [${!disableMemoryLimit}]"); loggy.debug("restarting, memory limit: [${!disableMemoryLimit}]");
return TaskEither( return TaskEither(() async {
() => CombineWorker().execute( try {
() { final err = await IsolateWorker().execute(
final err = _box () => _ffiRestart(configPath, disableMemoryLimit),
.restart( allowSyncFallback: false,
configPath.toNativeUtf8().cast(), );
disableMemoryLimit ? 1 : 0, if (err != null && err.isNotEmpty) {
)
.cast<Utf8>()
.toDartString();
if (err.isNotEmpty) {
return left(err); return left(err);
} }
return right(unit); return right(unit);
}, } catch (e) {
), return left(e.toString());
); }
});
} }
@override @override
@ -359,38 +395,38 @@ class FFISingboxService with InfraLogger implements SingboxService {
@override @override
TaskEither<String, Unit> selectOutbound(String groupTag, String outboundTag) { TaskEither<String, Unit> selectOutbound(String groupTag, String outboundTag) {
return TaskEither( return TaskEither(() async {
() => CombineWorker().execute( try {
() { final err = await IsolateWorker().execute(
final err = _box () => _ffiSelectOutbound(groupTag, outboundTag),
.selectOutbound( allowSyncFallback: false,
groupTag.toNativeUtf8().cast(), );
outboundTag.toNativeUtf8().cast(), if (err != null && err.isNotEmpty) {
)
.cast<Utf8>()
.toDartString();
if (err.isNotEmpty) {
return left(err); return left(err);
} }
return right(unit); return right(unit);
}, } catch (e) {
), return left(e.toString());
); }
});
} }
@override @override
TaskEither<String, Unit> urlTest(String groupTag) { TaskEither<String, Unit> urlTest(String groupTag) {
return TaskEither( return TaskEither(() async {
() => CombineWorker().execute( try {
() { final err = await IsolateWorker().execute(
final err = _box.urlTest(groupTag.toNativeUtf8().cast()).cast<Utf8>().toDartString(); () => _ffiUrlTest(groupTag),
if (err.isNotEmpty) { allowSyncFallback: false,
);
if (err != null && err.isNotEmpty) {
return left(err); return left(err);
} }
return right(unit); return right(unit);
}, } catch (e) {
), return left(e.toString());
); }
});
} }
final _logBuffer = <String>[]; final _logBuffer = <String>[];
@ -409,14 +445,10 @@ class FFISingboxService with InfraLogger implements SingboxService {
@override @override
TaskEither<String, Unit> clearLogs() { TaskEither<String, Unit> clearLogs() {
return TaskEither( return TaskEither(() async {
() => CombineWorker().execute(
() {
_logBuffer.clear(); _logBuffer.clear();
return right(unit); return right(unit);
}, });
),
);
} }
Future<List<String>> _readLogFile(File file) async { Future<List<String>> _readLogFile(File file) async {
@ -443,10 +475,156 @@ class FFISingboxService with InfraLogger implements SingboxService {
required String previousAccessToken, required String previousAccessToken,
}) { }) {
loggy.debug("generating warp config"); loggy.debug("generating warp config");
return TaskEither( return TaskEither(() async {
() => CombineWorker().execute( try {
() { final result = await IsolateWorker().execute(
final response = _box () => _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( .generateWarpConfig(
licenseKey.toNativeUtf8().cast(), licenseKey.toNativeUtf8().cast(),
previousAccountId.toNativeUtf8().cast(), previousAccountId.toNativeUtf8().cast(),
@ -455,11 +633,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
.cast<Utf8>() .cast<Utf8>()
.toDartString(); .toDartString();
if (response.startsWith("error:")) { if (response.startsWith("error:")) {
return left(response.replaceFirst('error:', "")); return [false, response.replaceFirst("error:", "")];
}
return right(warpFromJson(jsonDecode(response)));
},
),
);
} }
return [true, response];
} }

View 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();
}
}
}

View File

@ -1517,6 +1517,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" 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: term_glyph:
dependency: transitive dependency: transitive
description: description:

View File

@ -58,6 +58,7 @@ dependencies:
fpdart: ^1.1.0 fpdart: ^1.1.0
dartx: ^1.2.0 dartx: ^1.2.0
rxdart: ^0.27.7 rxdart: ^0.27.7
synchronized: ^3.0.1 # 🔧 P0-4: 用于线程安全的互斥锁保护
combine: 0.5.7 # 精确版本,兼容 Flutter 3.24.3(与 hiddify-app 相同) combine: 0.5.7 # 精确版本,兼容 Flutter 3.24.3(与 hiddify-app 相同)
encrypt: ^5.0.0 encrypt: ^5.0.0
path: ^1.8.3 path: ^1.8.3