fix(client): 连接态把内核 urltest 回写连接节点 ping,修延迟显示 — + 保持同源一致
1.0.32 纯 direct node.ping 的代价:auto-connect 太快连上、没有断开窗口让 direct TCP 量到 延迟 → node.ping 一直 0 → 显示 —。修:ConnectionController 订阅 stats,连接态把内核 urltest 最小正延迟回写连接节点的 node.ping(setLivePing)。连接页与节点列表都读 node.ping → 同一个数; 连接节点有实时 urltest 值、其余节点保留 direct 实测。断开态仍 direct TCP。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -76,11 +76,14 @@ class ConnectionController extends StateNotifier<ConnectionState> {
|
|||||||
: super(const ConnectionState(phase: VpnPhase.off)) {
|
: super(const ConnectionState(phase: VpnPhase.off)) {
|
||||||
// 订阅桥状态流:状态由内核事件驱动,严禁 UI 乐观翻转。
|
// 订阅桥状态流:状态由内核事件驱动,严禁 UI 乐观翻转。
|
||||||
_statusSub = _bridge.statusStream.listen(_onKernelStatus);
|
_statusSub = _bridge.statusStream.listen(_onKernelStatus);
|
||||||
|
// 订阅内核 stats:连接态把 urltest 实测回写连接节点的 ping,让连接页与节点列表同源一致。
|
||||||
|
_statsSub = _bridge.statsStream.listen(_onStats);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Ref _ref;
|
final Ref _ref;
|
||||||
final VpnBridge _bridge;
|
final VpnBridge _bridge;
|
||||||
StreamSubscription<VpnStatus>? _statusSub;
|
StreamSubscription<VpnStatus>? _statusSub;
|
||||||
|
StreamSubscription<VpnStatsEvent>? _statsSub;
|
||||||
Timer? _elapsed;
|
Timer? _elapsed;
|
||||||
Timer? _watchdog;
|
Timer? _watchdog;
|
||||||
bool _probing = false;
|
bool _probing = false;
|
||||||
@@ -222,6 +225,18 @@ class ConnectionController extends StateNotifier<ConnectionState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 连接态:取内核 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 轮询)───────────────────────
|
// ── 连通看门狗(路径 B:服务端权威 down 轮询)───────────────────────
|
||||||
void _startWatchdog() {
|
void _startWatchdog() {
|
||||||
_watchdog?.cancel();
|
_watchdog?.cancel();
|
||||||
@@ -312,6 +327,7 @@ class ConnectionController extends StateNotifier<ConnectionState> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_statusSub?.cancel();
|
_statusSub?.cancel();
|
||||||
|
_statsSub?.cancel();
|
||||||
_stopElapsed();
|
_stopElapsed();
|
||||||
_stopWatchdog();
|
_stopWatchdog();
|
||||||
_api?.dispose();
|
_api?.dispose();
|
||||||
|
|||||||
@@ -55,6 +55,20 @@ class NodesNotifier extends AsyncNotifier<List<Node>> {
|
|||||||
if (result.hasValue) state = result; // 成功才换;失败保留旧列表
|
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 未销毁才更新)。
|
/// 后台实测各节点延迟,完成后回填(notifier 未销毁才更新)。
|
||||||
/// 探测失败(返回 0,如连接态全局 TUN 把到节点的 TCP 握手接住)时**保留旧 ping**,
|
/// 探测失败(返回 0,如连接态全局 TUN 把到节点的 TCP 握手接住)时**保留旧 ping**,
|
||||||
/// 不把已知延迟覆盖成空——否则连上后节点列表延迟会整列消失。
|
/// 不把已知延迟覆盖成空——否则连上后节点列表延迟会整列消失。
|
||||||
|
|||||||
Reference in New Issue
Block a user