Files
pangolin/client/lib/state/nodes_provider.dart
T
wangjia f20eddad55
ci-pangolin / Lint — shellcheck (push) Has been cancelled
ci-pangolin / OpenAPI Sync Check (push) Has been cancelled
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Has been cancelled
ci-pangolin / Flutter — analyze + test (push) Has been cancelled
feat(client): P4 节点实测延迟/设备/套餐价/兑换/统计接真(#6 6D)
- 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>
2026-06-19 00:04:25 +08:00

109 lines
3.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
});