284 lines
11 KiB
Dart
Executable File
284 lines
11 KiB
Dart
Executable File
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 [];
|
||
}
|
||
}
|