feat: 保存配置
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user