import 'dart:async'; import 'dart:developer' as developer; import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../core/config/app_config.dart'; import '../data/models/node/service_node.dart'; import 'app_provider.dart'; // ── State ───────────────────────────────────────────────── class NodeState { final List nodes; final ServiceNode? currentNode; final bool isLoading; final bool isSpeedTesting; /// nodeId → latency(ms)。null 表示测速中或超时 final Map latencyMap; const NodeState({ this.nodes = const [], this.currentNode, this.isLoading = false, this.isSpeedTesting = false, this.latencyMap = const {}, }); NodeState copyWith({ List? nodes, ServiceNode? currentNode, bool? isLoading, bool? isSpeedTesting, Map? latencyMap, bool clearCurrentNode = false, }) { return NodeState( nodes: nodes ?? this.nodes, currentNode: clearCurrentNode ? null : (currentNode ?? this.currentNode), isLoading: isLoading ?? this.isLoading, isSpeedTesting: isSpeedTesting ?? this.isSpeedTesting, latencyMap: latencyMap ?? this.latencyMap, ); } } // ── Notifier ────────────────────────────────────────────── class NodeNotifier extends Notifier { // 熔断内部状态(不暴露给 UI) final Map _failCounts = {}; final Map _circuitOpenTimes = {}; @override NodeState build() { Future.microtask(_init); return const NodeState(isLoading: true); } SharedPreferences get _prefs => ref.read(sharedPreferencesProvider); /// Debug / Release 分 key 缓存,避免 debug 构建复用 release 写入的生产节点缓存 String get _nodeListCacheKey => AppConfig.isDebug ? 'node_list_cache_debug' : 'node_list_cache'; String get _selectedNodeIdKey => AppConfig.isDebug ? 'node_selected_id_debug' : 'node_selected_id'; // ── 初始化 ──────────────────────────────────────────── Future _init() async { // 1. 加载本地缓存;若缓存为空,用编译期 fallbackHosts 构造虚拟节点垫底, // 避免首屏在拉到节点列表前出现"无节点可切"的窗口 final cachedNodes = _loadCachedNodes(); final savedId = _prefs.getInt(_selectedNodeIdKey); final initialNodes = cachedNodes.isNotEmpty ? cachedNodes : _buildFallbackNodes(); ServiceNode? selected; if (savedId != null) { selected = initialNodes.where((n) => n.id == savedId).firstOrNull; } selected ??= initialNodes.firstOrNull; state = state.copyWith( nodes: initialNodes, currentNode: selected, isLoading: false, ); // 1.5 主动探活:缓存的 currentNode 可能已下线,先 ping 一次。 // 失败立即 failover,避免首屏业务请求承担探活成本(用户看到红 toast)。 // 不 await——与 fetchNodeList/测速 并行,最终都会收敛到正确节点。 unawaited(_probeCurrentNode()); // 2. 从远程 CDN 拉取最新兜底列表,合并到 fallbackHosts 头部 final remoteFallbacks = await _fetchRemoteFallbackHosts(); // 3. 后台拉取最新节点列表(优先使用远程兜底 + 本地硬编码兜底) await fetchNodeList(extraHosts: remoteFallbacks); // 4. 测速 if (state.nodes.isNotEmpty) { await speedTestAll(); } } /// 主动探活当前节点:用一次轻量 GET 请求验证 currentNode 是否可达, /// 失败立即触发 immediate failover 切下一个候选。 /// 与 _init 中的 fetchNodeList / speedTestAll 并发执行,竞争状态下最后写入者胜。 Future _probeCurrentNode() async { final node = state.currentNode; if (node == null) return; final dio = Dio(BaseOptions( connectTimeout: const Duration(milliseconds: AppConfig.speedTestTimeoutMs), receiveTimeout: const Duration(milliseconds: AppConfig.speedTestTimeoutMs), )); try { await dio.get('${node.normalizedApiHost}contract/watchlist/test'); developer.log('Node probe ok: ${node.name}', name: 'Node'); } catch (e) { developer.log( 'Node probe failed: ${node.name} → $e, switching node', name: 'Node', ); reportFailure(immediate: true); } } /// 用 AppConfig.fallbackHosts 构造一组兜底虚拟节点,id 用负数与真节点隔离。 /// 仅在没有缓存节点 / 节点列表 API 不可达时作为应急节点池使用。 List _buildFallbackNodes() { final hosts = AppConfig.fallbackHosts; return List.generate(hosts.length, (i) { final api = hosts[i]; var ws = api.replaceFirst(RegExp(r'^https?://'), 'wss://'); if (ws.endsWith('/')) ws = ws.substring(0, ws.length - 1); ws = '$ws/market-new'; return ServiceNode( id: -(i + 1), name: 'Fallback ${i + 1}', apiHost: api, wsHost: ws, status: 1, ); }); } /// 从远程 CDN 拉取兜底 Host 列表,失败时静默返回空列表 Future> _fetchRemoteFallbackHosts() async { final dio = Dio(BaseOptions( connectTimeout: const Duration(milliseconds: AppConfig.speedTestTimeoutMs), receiveTimeout: const Duration(milliseconds: AppConfig.speedTestTimeoutMs), )); for (final url in AppConfig.remoteConfigUrls) { try { final response = await dio.get>(url); final hosts = (response.data?['fallbackHosts'] as List?) ?.whereType() .where((h) => h.startsWith('http')) .toList(); if (hosts != null && hosts.isNotEmpty) { developer.log('Remote config loaded from $url: ${hosts.length} hosts', name: 'Node'); return hosts; } } catch (e) { developer.log('Remote config fetch failed from $url: $e', name: 'Node'); } } return []; } List _loadCachedNodes() { final json = _prefs.getString(_nodeListCacheKey); if (json == null || json.isEmpty) return []; try { return ServiceNode.listFromJson(json); } catch (_) { return []; } } void _saveCachedNodes(List nodes) { _prefs.setString(_nodeListCacheKey, ServiceNode.listToJson(nodes)); } // ── 拉取节点列表 ───────────────────────────────────── Future fetchNodeList({List extraHosts = const []}) async { final dio = Dio(BaseOptions( connectTimeout: const Duration(milliseconds: AppConfig.speedTestTimeoutMs), receiveTimeout: const Duration(milliseconds: AppConfig.speedTestTimeoutMs), headers: {'Content-Type': 'application/json'}, )); // 远程兜底列表优先,再用硬编码列表;去重保留顺序 final seen = {}; final hostsToTry = [...extraHosts, ...AppConfig.fallbackHosts] .where((h) => seen.add(h)) .toList(); for (final host in hostsToTry) { try { final response = await dio.get>('${host}contract/node/list'); final body = response.data; if (body == null) continue; final code = body['code'] as int? ?? -1; if (code != 0) continue; final list = (body['data'] as List) .map((e) => ServiceNode.fromJson(e as Map)) .where((n) => n.isValid) .toList(); if (list.isEmpty) continue; // 更新缓存 _saveCachedNodes(list); // 保持当前选择(如果仍在新列表中) final currentId = state.currentNode?.id; ServiceNode? selected; if (currentId != null) { selected = list.where((n) => n.id == currentId).firstOrNull; } selected ??= list.first; state = state.copyWith( nodes: list, currentNode: selected, isLoading: false, ); return; } catch (e) { developer.log('fetchNodeList from $host failed: $e', name: 'Node'); continue; } } // 全部失败,保持当前状态 state = state.copyWith(isLoading: false); } // ── 测速 ────────────────────────────────────────────── Future speedTestAll() async { if (state.nodes.isEmpty) return; state = state.copyWith(isSpeedTesting: true, latencyMap: {}); final results = {}; final futures = >[]; for (final node in state.nodes) { futures.add( _speedTest(node).then((ms) => results[node.id] = ms), ); } await Future.wait(futures); state = state.copyWith( isSpeedTesting: false, latencyMap: results, ); // 如果没有用户手动选择,自动切到最快节点 final savedId = _prefs.getInt(_selectedNodeIdKey); if (savedId == null) { _autoSelectBestNode(results); } } Future _speedTest(ServiceNode node) async { final dio = Dio(BaseOptions( connectTimeout: const Duration(milliseconds: AppConfig.speedTestTimeoutMs), receiveTimeout: const Duration(milliseconds: AppConfig.speedTestTimeoutMs), )); final sw = Stopwatch()..start(); try { await dio.get('${node.normalizedApiHost}contract/watchlist/test'); sw.stop(); return sw.elapsedMilliseconds; } catch (_) { sw.stop(); return null; // 超时或不可达 } } void _autoSelectBestNode(Map latencyMap) { final reachable = state.nodes.where((n) { final ms = latencyMap[n.id]; return ms != null; }).toList(); if (reachable.isEmpty) return; reachable.sort((a, b) { final la = latencyMap[a.id]!; final lb = latencyMap[b.id]!; return la.compareTo(lb); }); final best = reachable.first; if (best.id != state.currentNode?.id) { state = state.copyWith(currentNode: best); } } // ── 用户手动选择 ───────────────────────────────────── void selectNode(ServiceNode node) { _prefs.setInt(_selectedNodeIdKey, node.id); _failCounts.remove(node.id); state = state.copyWith(currentNode: node); } // ── 故障转移(由 Dio 拦截器调用)──────────────────── /// 报告请求失败,返回 true 表示已切换节点。 /// /// [immediate] 为 true 时第一次失败就熔断当前节点切下一个; /// 用于 DNS / connect / timeout 等"硬"网络错误,避免连续等 N 次超时才切。 /// 5xx 等服务端错误仍走累积阈值,避免单点抖动导致频繁切换。 bool reportFailure({bool immediate = false}) { final node = state.currentNode; if (node == null || state.nodes.length <= 1) return false; final count = (_failCounts[node.id] ?? 0) + 1; _failCounts[node.id] = count; final shouldTrip = immediate || count >= AppConfig.failoverThreshold; if (shouldTrip) { // 熔断当前节点 _circuitOpenTimes[node.id] = DateTime.now(); final next = _getNextAvailableNode(); if (next != null) { developer.log( 'Failover: ${node.name} → ${next.name} (immediate=$immediate)', name: 'Node', ); _failCounts.remove(next.id); state = state.copyWith(currentNode: next); return true; } // 所有节点都熔断了,重置全部 _circuitOpenTimes.clear(); _failCounts.clear(); } return false; } /// 报告请求成功,重置失败计数 void reportSuccess() { final node = state.currentNode; if (node == null) return; if (_failCounts.containsKey(node.id)) { _failCounts.remove(node.id); } } ServiceNode? _getNextAvailableNode() { final currentId = state.currentNode?.id; final available = state.nodes.where((n) { if (n.id == currentId) return false; return !_isCircuitOpen(n.id); }).toList(); if (available.isEmpty) return null; // 优先选延迟最低的 available.sort((a, b) { final la = state.latencyMap[a.id]; final lb = state.latencyMap[b.id]; if (la == null && lb == null) return 0; if (la == null) return 1; if (lb == null) return -1; return la.compareTo(lb); }); return available.first; } bool _isCircuitOpen(int nodeId) { final openTime = _circuitOpenTimes[nodeId]; if (openTime == null) return false; if (DateTime.now().difference(openTime).inSeconds > AppConfig.circuitBreakDurationSec) { // 半开:超过冷却期,允许重试 _circuitOpenTimes.remove(nodeId); _failCounts.remove(nodeId); return false; } return true; } } // ── Provider ────────────────────────────────────────────── final nodeProvider = NotifierProvider( NodeNotifier.new, );