diff --git a/client/lib/state/connection_provider.dart b/client/lib/state/connection_provider.dart index 7f144bb..da31aa8 100644 --- a/client/lib/state/connection_provider.dart +++ b/client/lib/state/connection_provider.dart @@ -76,11 +76,14 @@ class ConnectionController extends StateNotifier { : super(const ConnectionState(phase: VpnPhase.off)) { // 订阅桥状态流:状态由内核事件驱动,严禁 UI 乐观翻转。 _statusSub = _bridge.statusStream.listen(_onKernelStatus); + // 订阅内核 stats:连接态把 urltest 实测回写连接节点的 ping,让连接页与节点列表同源一致。 + _statsSub = _bridge.statsStream.listen(_onStats); } final Ref _ref; final VpnBridge _bridge; StreamSubscription? _statusSub; + StreamSubscription? _statsSub; Timer? _elapsed; Timer? _watchdog; bool _probing = false; @@ -222,6 +225,18 @@ class ConnectionController extends StateNotifier { } } + // 连接态:取内核 urltest 最小正延迟,回写连接节点 ping。全局 TUN 下 App 直接探测节点不可行, + // 内核(经 REALITY 真实直连数据面)是唯一能实测的;回写到 node.ping → 连接页/节点列表同源。 + void _onStats(VpnStatsEvent e) { + if (state.phase != VpnPhase.on) return; + final node = _connectedNode; + if (node == null || node.uuid.isEmpty) return; + final ds = e.urltestResults.where((r) => r.delayMs > 0).map((r) => r.delayMs); + if (ds.isEmpty) return; + final best = ds.reduce((a, b) => a < b ? a : b); + _ref.read(nodesProvider.notifier).setLivePing(node.uuid, best); + } + // ── 连通看门狗(路径 B:服务端权威 down 轮询)─────────────────────── void _startWatchdog() { _watchdog?.cancel(); @@ -312,6 +327,7 @@ class ConnectionController extends StateNotifier { @override void dispose() { _statusSub?.cancel(); + _statsSub?.cancel(); _stopElapsed(); _stopWatchdog(); _api?.dispose(); diff --git a/client/lib/state/nodes_provider.dart b/client/lib/state/nodes_provider.dart index 305a8ec..92f29c6 100644 --- a/client/lib/state/nodes_provider.dart +++ b/client/lib/state/nodes_provider.dart @@ -55,6 +55,20 @@ class NodesNotifier extends AsyncNotifier> { if (result.hasValue) state = result; // 成功才换;失败保留旧列表 } + /// 回写某节点的实测延迟(ms)。连接态由 ConnectionController 用内核 urltest 回填连接节点, + /// 让「连接页延迟」与「节点列表延迟」同源(都读 node.ping)、显示一致。ms<=0 忽略。 + void setLivePing(String uuid, int ms) { + if (ms <= 0 || _disposed) return; + final list = state.valueOrNull; + if (list == null) return; + var changed = false; + final updated = [ + for (final n in list) + if (n.uuid == uuid && n.ping != ms) (() { changed = true; return n.copyWith(ping: ms); })() else n + ]; + if (changed) state = AsyncData(updated); + } + /// 后台实测各节点延迟,完成后回填(notifier 未销毁才更新)。 /// 探测失败(返回 0,如连接态全局 TUN 把到节点的 TCP 握手接住)时**保留旧 ping**, /// 不把已知延迟覆盖成空——否则连上后节点列表延迟会整列消失。