feat: 保存配置

This commit is contained in:
2026-01-07 17:33:55 -08:00
parent 9563a81a6e
commit 3f429e5546
17 changed files with 4124 additions and 1271 deletions
@@ -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=$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 实现
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;
}
}
}