220 lines
7.2 KiB
Dart
Executable File
220 lines
7.2 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: []);
|
||
}
|
||
|
||
// 获取第一个订阅对象
|
||
final 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('节点列表解析: is_try_out=$isTryOut, 节点数=${nodesData?.length ?? 0}', 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
|
||
|
||
// 🔧 如果有 protocols 字段,从中解析 port 和 cipher
|
||
if (protocols.isNotEmpty) {
|
||
try {
|
||
final protocolsList = jsonDecode(protocols) as List;
|
||
if (protocolsList.isNotEmpty) {
|
||
final firstProtocol = protocolsList[0] as Map<String, dynamic>;
|
||
// 优先使用 protocols 中的配置
|
||
if (firstProtocol['port'] != null) {
|
||
port = _parseIntSafely(firstProtocol['port']);
|
||
}
|
||
if (firstProtocol['cipher'] != null && firstProtocol['cipher'].toString().isNotEmpty) {
|
||
method = firstProtocol['cipher'].toString();
|
||
}
|
||
KRLogUtil.kr_i('从 protocols 解析: port=$port, cipher=$method', tag: 'NodeList');
|
||
}
|
||
} catch (e) {
|
||
KRLogUtil.kr_w('解析 protocols 字段失败: $e', 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 [];
|
||
}
|
||
}
|