f20eddad55
- latency_probe.dart:并行 TCP 握手实测各节点 per-client RTT(服务端无法代知)。 - nodes_provider:解析 /v1/nodes 的 host/port,后台测延迟回填 Node.ping;智能选择 按实测最小;去掉 8 个伪造演示节点(空列表用中性占位,不再伪造服务器)。 - Node:+host/port/copyWith/pingLabel(未测显示 —);各端 ping 显示改 pingLabel。 - account_screens:PlansScreen 接 /v1/plans(价格 priceLabel + me 标当前档); DevicesScreen 接 /v1/me/devices + 移除 DELETE + last_seen;RedeemScreen 接 /v1/redeem(成功刷新 me,失败显示后端文案)。 - stats_page:周柱接 me.weekly_gb,本月流量/时长接 usage(30),延迟用生效节点实测。 - 顺带补登记早前 log_time.dart 的删除(log.dart 重构遗漏 stage)。 flutter analyze 0 error;115 tests passed;受影响 4 golden 重生成。 本机设备高亮 + 真实 device id 随 P6(device_info_plus)落地。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
109 lines
3.8 KiB
Dart
109 lines
3.8 KiB
Dart
// nodes_provider.dart — 节点清单 + 当前选择 + 实测延迟
|
||
//
|
||
// 从 GET /v1/nodes 拉取真实节点(含 host/port);拉取后后台对各节点做 TCP 握手
|
||
// 实测 per-client 延迟并回填。未登录 / 失败返回空列表(不再伪造演示节点)。
|
||
import 'dart:async';
|
||
import 'dart:convert';
|
||
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:http/http.dart' as http;
|
||
|
||
import '../models/node.dart';
|
||
import '../services/api_config.dart';
|
||
import '../services/latency_probe.dart';
|
||
import 'auth_provider.dart';
|
||
|
||
// ── 节点列表 AsyncNotifier ────────────────────────────────────────
|
||
|
||
class NodesNotifier extends AsyncNotifier<List<Node>> {
|
||
bool _disposed = false;
|
||
|
||
@override
|
||
Future<List<Node>> build() async {
|
||
ref.onDispose(() => _disposed = true);
|
||
final auth = ref.watch(authProvider);
|
||
if (!auth.isLoggedIn) return const [];
|
||
final list = await _fetchNodes(auth.accessToken!);
|
||
unawaited(_measure(list));
|
||
return list;
|
||
}
|
||
|
||
Future<void> refresh() async {
|
||
final auth = ref.read(authProvider);
|
||
if (!auth.isLoggedIn) {
|
||
state = const AsyncData([]);
|
||
return;
|
||
}
|
||
state = const AsyncLoading();
|
||
state = await AsyncValue.guard(() async {
|
||
final list = await _fetchNodes(auth.accessToken!);
|
||
unawaited(_measure(list));
|
||
return list;
|
||
});
|
||
}
|
||
|
||
/// 后台实测各节点延迟,完成后回填(notifier 未销毁才更新)。
|
||
Future<void> _measure(List<Node> list) async {
|
||
if (list.isEmpty) return;
|
||
final pings = await probeAll(
|
||
[for (final n in list) (uuid: n.uuid, host: n.host, port: n.port)],
|
||
);
|
||
if (_disposed) return;
|
||
final updated = [for (final n in list) n.copyWith(ping: pings[n.uuid] ?? n.ping)];
|
||
state = AsyncData(updated);
|
||
}
|
||
|
||
static Future<List<Node>> _fetchNodes(String accessToken) async {
|
||
final uri = Uri.parse('$kApiBaseUrl/v1/nodes');
|
||
final resp = await http.get(uri, headers: {
|
||
'Authorization': 'Bearer $accessToken',
|
||
}).timeout(const Duration(seconds: 10));
|
||
|
||
if (resp.statusCode != 200) return const [];
|
||
|
||
final body = jsonDecode(resp.body) as Map<String, dynamic>;
|
||
final rawList = body['nodes'] as List<dynamic>? ?? [];
|
||
return rawList.map((e) {
|
||
final m = e as Map<String, dynamic>;
|
||
return Node(
|
||
uuid: m['id'] as String? ?? '',
|
||
code: m['region'] as String? ?? '??',
|
||
nameZh: m['name_zh'] as String? ?? '',
|
||
nameEn: m['name_en'] as String? ?? '',
|
||
tier: m['tier'] as String? ?? 'free',
|
||
host: m['host'] as String? ?? '',
|
||
port: (m['port'] as num?)?.toInt() ?? 0,
|
||
ping: 0, // 由 _measure 实测回填
|
||
);
|
||
}).toList();
|
||
}
|
||
}
|
||
|
||
final nodesProvider =
|
||
AsyncNotifierProvider<NodesNotifier, List<Node>>(NodesNotifier.new);
|
||
|
||
// ── 当前选中的节点 code;'AUTO' 表示智能选择(默认)─────────────────
|
||
|
||
final selectedNodeCodeProvider = StateProvider<String>((ref) => kSmartNodeCode);
|
||
|
||
/// 是否处于智能选择。
|
||
final isSmartSelectProvider = Provider<bool>(
|
||
(ref) => ref.watch(selectedNodeCodeProvider) == kSmartNodeCode,
|
||
);
|
||
|
||
/// 实际生效的节点:列表为空时返回占位;智能选择取实测延迟最小者(未测得排后)。
|
||
final effectiveNodeProvider = Provider<Node>((ref) {
|
||
final nodes = ref.watch(nodesProvider).valueOrNull ?? const <Node>[];
|
||
if (nodes.isEmpty) return kPlaceholderNode;
|
||
|
||
final code = ref.watch(selectedNodeCodeProvider);
|
||
if (code == kSmartNodeCode) {
|
||
return nodes.reduce((a, b) {
|
||
final pa = a.ping > 0 ? a.ping : 1 << 30;
|
||
final pb = b.ping > 0 ? b.ping : 1 << 30;
|
||
return pa <= pb ? a : b;
|
||
});
|
||
}
|
||
return nodes.firstWhere((n) => n.code == code, orElse: () => nodes.first);
|
||
});
|