hi-client/lib/app/model/response/kr_node_list.dart
2026-01-22 16:58:54 -08:00

284 lines
11 KiB
Dart
Executable File
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.

import 'dart:convert';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
class KRNodeList {
final List<KrNodeListItem> list;
final String subscribeId;
final String startTime;
final String expireTime;
final bool isTryOut; // 是否是试用订阅
const KRNodeList({
required this.list,
this.subscribeId = "0",
this.startTime = "",
this.expireTime = "",
this.isTryOut = false,
});
factory KRNodeList.fromJson(Map<String, dynamic> json) {
try {
// 新的 API 返回格式: {"list": [{"id": 24, "is_try_out": true, "nodes": [...]}]}
final List<dynamic>? listData = json['list'] as List<dynamic>?;
if (listData == null || listData.isEmpty) {
KRLogUtil.kr_w('节点列表为空', tag: 'NodeList');
return const KRNodeList(list: []);
}
// 尝试找到与请求参数 id 匹配的订阅项
// 如果 json 中有 subscribeId则查找匹配项否则使用第一个
Map<String, dynamic>? subscribeData;
final String? requestSubscribeId = json['subscribeId']?.toString();
if (requestSubscribeId != null && requestSubscribeId.isNotEmpty) {
// 查找 id 匹配的订阅项
try {
subscribeData = listData.firstWhere(
(item) => (item as Map<String, dynamic>)['id']?.toString() == requestSubscribeId,
orElse: () => listData[0] as Map<String, dynamic>
) as Map<String, dynamic>;
KRLogUtil.kr_i('✅ 找到匹配的订阅项: id=$requestSubscribeId', tag: 'NodeList');
} catch (e) {
KRLogUtil.kr_w('⚠️ 未找到匹配的订阅项,使用第一个', tag: 'NodeList');
subscribeData = listData[0] as Map<String, dynamic>;
}
} else {
// 如果没有提供 subscribeId使用第一个
subscribeData = listData[0] as Map<String, dynamic>;
}
final bool isTryOut = subscribeData['is_try_out'] as bool? ?? false;
final List<dynamic>? nodesData = subscribeData['nodes'] as List<dynamic>?;
KRLogUtil.kr_i('🔍 节点列表解析: subscribe_id=${subscribeData['id']}, is_try_out=$isTryOut, 节点数=${nodesData?.length ?? 0}', tag: 'NodeList');
KRLogUtil.kr_i('📊 所有订阅项数量: ${listData.length}', tag: 'NodeList');
return KRNodeList(
list: nodesData?.map((e) => KrNodeListItem.fromJson(e as Map<String, dynamic>)).toList() ?? [],
subscribeId: subscribeData['id']?.toString() ?? "0",
startTime: subscribeData['start_time']?.toString() ?? "",
expireTime: subscribeData['expire_time']?.toString() ?? "",
isTryOut: isTryOut,
);
} catch (err) {
KRLogUtil.kr_e('KRNodeList解析错误: $err', tag: 'NodeList');
return const KRNodeList(list: []);
}
}
}
class KrNodeListItem {
final int id;
String name;
final String uuid;
final String protocol;
final String relayMode;
final String relayNode;
final String serverAddr;
final int port; // 端口字段
final String method; // 加密方法用于Shadowsocks等
final String protocols; // 协议配置JSON字符串新API格式
final int speedLimit;
final List<String> tags;
final int traffic;
final double trafficRatio;
final int upload;
final String city;
final String config;
final String country;
final int createdAt;
final int download;
final String startTime;
final String expireTime;
final double latitude;
final double latitudeCountry;
final double longitude;
final double longitudeCountry;
KrNodeListItem({
required this.id,
required this.name,
required this.uuid,
required this.protocol,
this.relayMode = '',
this.relayNode = '',
required this.serverAddr,
this.port = 0, // 默认值
this.method = '', // 默认空字符串
this.protocols = '', // 默认空字符串
required this.speedLimit,
required this.tags,
required this.traffic,
required this.trafficRatio,
required this.upload,
required this.city,
required this.config,
required this.country,
this.createdAt = 0,
required this.download,
required this.startTime,
required this.expireTime,
required this.latitude,
required this.latitudeCountry,
required this.longitude,
required this.longitudeCountry,
});
factory KrNodeListItem.fromJson(Map<String, dynamic> json) {
try {
// 支持新旧两种API格式
// 最新格式: protocols 字段包含协议配置数组
// 新格式: address, port, method直接字段
// 旧格式: server_addr, config 中包含 port
final serverAddr = json['address']?.toString() ?? json['server_addr']?.toString() ?? '';
int port = _parseIntSafely(json['port']);
String method = json['method']?.toString() ?? ''; // 加密方法Shadowsocks等
final protocols = json['protocols']?.toString() ?? ''; // 协议配置JSON
// 🔧 打印原始节点 JSON用于调试
// KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'NodeList');
// KRLogUtil.kr_i('📥 收到节点 API 原始数据:', tag: 'NodeList');
// KRLogUtil.kr_i('节点名称: ${json['name']}', tag: 'NodeList');
// KRLogUtil.kr_i('协议类型: ${json['protocol']}', tag: 'NodeList');
// KRLogUtil.kr_i('完整 JSON:', tag: 'NodeList');
// KRLogUtil.kr_i(jsonEncode(json), tag: 'NodeList');
// 🔧 如果有 protocols 字段,从中解析 cipher但不覆盖顶层的 port
if (protocols.isNotEmpty) {
try {
final protocolsList = jsonDecode(protocols) as List;
final currentProtocol = json['protocol']?.toString().toLowerCase() ?? '';
// KRLogUtil.kr_i('📋 protocols 字段内容 (${protocolsList.length} 个协议):', tag: 'NodeList');
for (var i = 0; i < protocolsList.length; i++) {
// KRLogUtil.kr_i(' 协议 $i: ${jsonEncode(protocolsList[i])}', tag: 'NodeList');
}
if (protocolsList.isNotEmpty) {
// 🔧 修复:查找与当前协议类型匹配的配置,而不是直接使用第一个
Map<String, dynamic>? matchedProtocolConfig;
// 尝试找到协议类型匹配的配置
for (var protocolConfig in protocolsList) {
final configMap = protocolConfig as Map<String, dynamic>;
// 🔧 修复API 使用的字段名是 'type',不是 'protocol'
final protocolType = (configMap['type'] ?? configMap['protocol'])?.toString().toLowerCase() ?? '';
// 检查是否匹配(支持 shadowsocks/ss, vmess, vless, trojan 等)
if (protocolType == currentProtocol ||
(currentProtocol == 'shadowsocks' && protocolType == 'ss') ||
(currentProtocol == 'ss' && protocolType == 'shadowsocks')) {
matchedProtocolConfig = configMap;
// KRLogUtil.kr_i('🎯 找到匹配的协议配置: $protocolType', tag: 'NodeList');
break;
}
}
// 如果没找到匹配的使用第一个配置兼容旧API
final targetProtocol = matchedProtocolConfig ?? (protocolsList[0] as Map<String, dynamic>);
// 🔧 关键修复:只在顶层没有 port 字段时,才使用 protocols 中的端口
// 这样可以保留顶层的正确端口(如 53441不被 protocols 数组中的端口(如 287覆盖
if (port == 0 && targetProtocol['port'] != null) {
port = _parseIntSafely(targetProtocol['port']);
// KRLogUtil.kr_i('✅ 从 protocols 解析端口: $port', tag: 'NodeList');
} else {
// KRLogUtil.kr_i('✅ 保留顶层端口: $port (protocols中的端口: ${targetProtocol['port']})', tag: 'NodeList');
}
// 提取 cipher加密方法
if (targetProtocol['cipher'] != null && targetProtocol['cipher'].toString().isNotEmpty) {
method = targetProtocol['cipher'].toString();
// KRLogUtil.kr_i('✅ 从 protocols 解析 cipher: $method', tag: 'NodeList');
}
}
} catch (e) {
KRLogUtil.kr_w('⚠️ 解析 protocols 字段失败: $e', tag: 'NodeList');
}
}
// KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'NodeList');
return KrNodeListItem(
id: _parseIntSafely(json['id']),
name: json['name']?.toString() ?? '',
uuid: json['uuid']?.toString() ?? '',
protocol: json['protocol']?.toString() ?? '',
relayMode: json['relay_mode']?.toString() ?? '',
relayNode: json['relay_node']?.toString() ?? '',
serverAddr: serverAddr,
port: port,
method: method,
protocols: protocols,
speedLimit: _parseIntSafely(json['speed_limit']),
tags: _parseStringList(json['tags']),
traffic: _parseIntSafely(json['traffic']),
trafficRatio: _parseDoubleSafely(json['traffic_ratio']),
upload: _parseIntSafely(json['upload']),
city: json['city']?.toString() ?? '',
config: json['config']?.toString() ?? '',
country: json['country']?.toString() ?? '',
createdAt: _parseIntSafely(json['created_at']),
download: _parseIntSafely(json['download']),
startTime: json['start_time']?.toString() ?? '',
expireTime: json['expire_time']?.toString() ?? '',
latitude: _parseDoubleSafely(json['latitude']),
latitudeCountry: _parseDoubleSafely(json['latitude_country']),
longitude: _parseDoubleSafely(json['longitude']),
longitudeCountry: _parseDoubleSafely(json['longitude_country']),
);
} catch (err) {
KRLogUtil.kr_e('KrNodeListItem解析错误: $err', tag: 'NodeList');
return KrNodeListItem(
id: 0,
name: '',
uuid: '',
protocol: '',
serverAddr: '',
port: 0,
method: '',
protocols: '',
speedLimit: 0,
tags: [],
traffic: 0,
trafficRatio: 0,
upload: 0,
city: '',
config: '',
country: '',
download: 0,
startTime: '',
expireTime: '',
latitude: 0.0,
latitudeCountry: 0.0,
longitude: 0.0,
longitudeCountry: 0.0,
);
}
}
// 添加安全解析工具方法
static int _parseIntSafely(dynamic value) {
if (value == null) return 0;
if (value is int) return value;
if (value is String) return int.tryParse(value) ?? 0;
return 0;
}
static double _parseDoubleSafely(dynamic value) {
if (value == null) return 0.0;
if (value is double) return value;
if (value is int) return value.toDouble();
if (value is String) return double.tryParse(value) ?? 0.0;
return 0.0;
}
static List<String> _parseStringList(dynamic value) {
if (value == null) return [];
if (value is List) {
return value.map((e) => e?.toString() ?? '').toList();
}
return [];
}
}