feat: 提交国家分组功能和debug功能
This commit is contained in:
parent
76e83e0135
commit
2a1ee3018d
@ -57,7 +57,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "app.hifastvpn.com"
|
applicationId "app.hifastvpn.com"
|
||||||
minSdkVersion flutter.minSdkVersion
|
minSdkVersion flutter.minSdkVersion
|
||||||
targetSdkVersion 36
|
targetSdkVersion 34
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|||||||
@ -34,6 +34,11 @@
|
|||||||
|
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<!-- 如果 targetSdkVersion >= 33 -->
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<application
|
<application
|
||||||
android:name=".Application"
|
android:name=".Application"
|
||||||
android:banner="@mipmap/ic_banner"
|
android:banner="@mipmap/ic_banner"
|
||||||
|
|||||||
@ -34,6 +34,60 @@ class KROutboundItem {
|
|||||||
|
|
||||||
/// 构造函数,接受 KrItem 对象并初始化 KROutboundItem
|
/// 构造函数,接受 KrItem 对象并初始化 KROutboundItem
|
||||||
KROutboundItem(this.nodeListItem) {
|
KROutboundItem(this.nodeListItem) {
|
||||||
|
_initFromNodeListItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 静态工厂:构造虚拟 urltest 节点(用于 ${country}-auto)
|
||||||
|
factory KROutboundItem.fromVirtual(String tag, String country, Map<String, dynamic> config) {
|
||||||
|
// 构造一个虚拟 KrNodeListItem,仅填充必要字段
|
||||||
|
final virtualNode = KrNodeListItem(
|
||||||
|
id: 0,
|
||||||
|
name: tag,
|
||||||
|
protocol: 'urltest',
|
||||||
|
serverAddr: '',
|
||||||
|
port: 0,
|
||||||
|
uuid: '',
|
||||||
|
config: jsonEncode(config),
|
||||||
|
city: '',
|
||||||
|
country: country,
|
||||||
|
tags: [],
|
||||||
|
latitude: 0,
|
||||||
|
longitude: 0,
|
||||||
|
latitudeCountry: 0,
|
||||||
|
longitudeCountry: 0,
|
||||||
|
relayNode: '',
|
||||||
|
relayMode: 'none',
|
||||||
|
protocols: '',
|
||||||
|
method: '',
|
||||||
|
speedLimit: 0,
|
||||||
|
traffic: 0,
|
||||||
|
trafficRatio: 0,
|
||||||
|
upload: 0,
|
||||||
|
download: 0,
|
||||||
|
startTime: '',
|
||||||
|
expireTime: '',
|
||||||
|
);
|
||||||
|
return KROutboundItem._virtual(virtualNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 私有构造:用于虚拟节点,避免重复解析
|
||||||
|
KROutboundItem._virtual(this.nodeListItem) {
|
||||||
|
// 直接填充虚拟节点所需字段
|
||||||
|
id = nodeListItem.id.toString();
|
||||||
|
protocol = nodeListItem.protocol;
|
||||||
|
tag = nodeListItem.name;
|
||||||
|
serverAddr = nodeListItem.serverAddr;
|
||||||
|
city = nodeListItem.city;
|
||||||
|
country = nodeListItem.country;
|
||||||
|
latitude = nodeListItem.latitude;
|
||||||
|
latitudeCountry = nodeListItem.latitudeCountry;
|
||||||
|
longitude = nodeListItem.longitude;
|
||||||
|
longitudeCountry = nodeListItem.longitudeCountry;
|
||||||
|
config = jsonDecode(nodeListItem.config); // 已知 config 有效
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 初始化逻辑提取,供主构造调用
|
||||||
|
void _initFromNodeListItem() {
|
||||||
id = nodeListItem.id.toString();
|
id = nodeListItem.id.toString();
|
||||||
protocol = nodeListItem.protocol;
|
protocol = nodeListItem.protocol;
|
||||||
latitude = nodeListItem.latitude;
|
latitude = nodeListItem.latitude;
|
||||||
@ -56,167 +110,165 @@ class KROutboundItem {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兜底:尝试解析 config 字段(旧API格式)
|
// 兜底:尝试从 config 字段解析(旧API格式)
|
||||||
if (nodeListItem.config.isEmpty) {
|
if (nodeListItem.config.isNotEmpty) {
|
||||||
if (kDebugMode) {
|
try {
|
||||||
print('❌ 节点 ${nodeListItem.name} 缺少配置信息(无port或config)');
|
final json = jsonDecode(nodeListItem.config) as Map<String, dynamic>;
|
||||||
}
|
if (kDebugMode) {
|
||||||
config = {};
|
print('📄 解析到 config JSON: $json');
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
late Map<String, dynamic> json;
|
|
||||||
try {
|
|
||||||
json = jsonDecode(nodeListItem.config) as Map<String, dynamic>;
|
|
||||||
} catch (e) {
|
|
||||||
if (kDebugMode) {
|
|
||||||
print('❌ 节点 ${nodeListItem.name} 的 config 解析失败: $e,尝试使用直接字段');
|
|
||||||
}
|
|
||||||
if (kDebugMode) {
|
|
||||||
print('📄 Config 内容: ${nodeListItem.config}');
|
|
||||||
}
|
|
||||||
_buildConfigFromFields(nodeListItem);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (nodeListItem.protocol) {
|
|
||||||
case "vless":
|
|
||||||
final securityConfig =
|
|
||||||
json["security_config"] as Map<String, dynamic>? ?? {};
|
|
||||||
|
|
||||||
// 智能设置 server_name
|
|
||||||
String serverName = securityConfig["sni"] ?? "";
|
|
||||||
if (serverName.isEmpty) {
|
|
||||||
serverName = nodeListItem.serverAddr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
config = {
|
// 提取 transport 配置
|
||||||
"type": "vless",
|
Map<String, dynamic>? transportConfig;
|
||||||
"tag": nodeListItem.name,
|
if (json['transport'] != null && json['transport'] != 'tcp') {
|
||||||
"server": nodeListItem.serverAddr,
|
transportConfig = _buildTransport(json);
|
||||||
"server_port": json["port"],
|
if (kDebugMode) {
|
||||||
"uuid": nodeListItem.uuid,
|
print('✅ 找到 transport 配置: $transportConfig');
|
||||||
if (json["flow"] != null && json["flow"] != "none")
|
}
|
||||||
"flow": json["flow"],
|
}
|
||||||
if (json["transport"] != null && json["transport"] != "tcp")
|
|
||||||
"transport": _buildTransport(json),
|
// 提取 security_config
|
||||||
"tls": {
|
Map<String, dynamic>? securityConfig;
|
||||||
"enabled": json["security"] == "tls",
|
if (json['security_config'] != null) {
|
||||||
"server_name": serverName,
|
securityConfig = json['security_config'] as Map<String, dynamic>;
|
||||||
"insecure": securityConfig["allow_insecure"] ?? true,
|
if (kDebugMode) {
|
||||||
"utls": {
|
print('✅ 找到 security_config: $securityConfig');
|
||||||
"enabled": true,
|
}
|
||||||
"fingerprint": securityConfig["fingerprint"] ?? "chrome"
|
}
|
||||||
|
|
||||||
|
// 根据协议类型构建配置
|
||||||
|
switch (nodeListItem.protocol) {
|
||||||
|
case "shadowsocks":
|
||||||
|
config = {
|
||||||
|
"type": "shadowsocks",
|
||||||
|
"tag": nodeListItem.name,
|
||||||
|
"server": nodeListItem.serverAddr,
|
||||||
|
"server_port": json["port"],
|
||||||
|
"method": json["method"],
|
||||||
|
"password": nodeListItem.uuid
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case "vless":
|
||||||
|
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||||
|
.hasMatch(nodeListItem.serverAddr);
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"type": "vless",
|
||||||
|
"tag": nodeListItem.name,
|
||||||
|
"server": nodeListItem.serverAddr,
|
||||||
|
"server_port": json["port"],
|
||||||
|
"uuid": nodeListItem.uuid,
|
||||||
|
if (transportConfig != null) "transport": transportConfig,
|
||||||
|
if (json["security"] == "tls") "tls": {
|
||||||
|
"enabled": true,
|
||||||
|
if (isDomain) "server_name": securityConfig?["sni"] ?? nodeListItem.serverAddr,
|
||||||
|
"insecure": securityConfig?["allow_insecure"] ?? true,
|
||||||
|
"utls": {
|
||||||
|
"enabled": true,
|
||||||
|
"fingerprint": securityConfig?["fingerprint"] ?? "chrome"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case "vmess":
|
||||||
|
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||||
|
.hasMatch(nodeListItem.serverAddr);
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"type": "vmess",
|
||||||
|
"tag": nodeListItem.name,
|
||||||
|
"server": nodeListItem.serverAddr,
|
||||||
|
"server_port": json["port"],
|
||||||
|
"uuid": nodeListItem.uuid,
|
||||||
|
"alter_id": 0,
|
||||||
|
"security": "auto",
|
||||||
|
if (transportConfig != null) "transport": transportConfig,
|
||||||
|
if (json["security"] == "tls") "tls": {
|
||||||
|
"enabled": true,
|
||||||
|
if (isDomain) "server_name": securityConfig?["sni"] ?? nodeListItem.serverAddr,
|
||||||
|
"insecure": securityConfig?["allow_insecure"] ?? true,
|
||||||
|
"utls": {
|
||||||
|
"enabled": true,
|
||||||
|
"fingerprint": securityConfig?["fingerprint"] ?? "chrome"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case "trojan":
|
||||||
|
final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
||||||
|
.hasMatch(nodeListItem.serverAddr);
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"type": "trojan",
|
||||||
|
"tag": nodeListItem.name,
|
||||||
|
"server": nodeListItem.serverAddr,
|
||||||
|
"server_port": json["port"],
|
||||||
|
"password": nodeListItem.uuid,
|
||||||
|
if (transportConfig != null) "transport": transportConfig,
|
||||||
|
"tls": {
|
||||||
|
"enabled": json["security"] == "tls",
|
||||||
|
if (isDomain) "server_name": securityConfig?["sni"] ?? nodeListItem.serverAddr,
|
||||||
|
"insecure": securityConfig?["allow_insecure"] ?? true,
|
||||||
|
"utls": {
|
||||||
|
"enabled": true,
|
||||||
|
"fingerprint": securityConfig?["fingerprint"] ?? "chrome"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case "hysteria":
|
||||||
|
case "hysteria2":
|
||||||
|
final securityConfig = json["security_config"] as Map<String, dynamic>? ?? {};
|
||||||
|
config = {
|
||||||
|
"type": "hysteria2",
|
||||||
|
"tag": nodeListItem.name,
|
||||||
|
"server": nodeListItem.serverAddr,
|
||||||
|
"server_port": json["port"],
|
||||||
|
"password": nodeListItem.uuid,
|
||||||
|
"up_mbps": 100,
|
||||||
|
"down_mbps": 100,
|
||||||
|
"obfs": {
|
||||||
|
"type": "salamander",
|
||||||
|
"password": json["obfs_password"] ?? nodeListItem.uuid
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"enabled": true,
|
||||||
|
"server_name": securityConfig["sni"] ?? "",
|
||||||
|
"insecure": securityConfig["allow_insecure"] ?? true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (kDebugMode) {
|
||||||
|
print('⚠️ 不支持的协议类型: ${nodeListItem.protocol}');
|
||||||
}
|
}
|
||||||
}
|
config = {};
|
||||||
};
|
|
||||||
break;
|
|
||||||
case "vmess":
|
|
||||||
final securityConfig =
|
|
||||||
json["security_config"] as Map<String, dynamic>? ?? {};
|
|
||||||
|
|
||||||
// 智能设置 server_name
|
|
||||||
String serverName = securityConfig["sni"] ?? "";
|
|
||||||
if (serverName.isEmpty) {
|
|
||||||
serverName = nodeListItem.serverAddr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
config = {
|
// 检查 relayNode 是否为 JSON 字符串并解析
|
||||||
"type": "vmess",
|
if (nodeListItem.relayNode.isNotEmpty && nodeListItem.relayMode != "none") {
|
||||||
"tag": nodeListItem.name,
|
final relayNodeJson = jsonDecode(nodeListItem.relayNode);
|
||||||
"server": nodeListItem.serverAddr,
|
if (relayNodeJson is List && nodeListItem.relayMode != "none") {
|
||||||
"server_port": json["port"],
|
// 随机选择一个元素
|
||||||
"uuid": nodeListItem.uuid,
|
final randomNode = (relayNodeJson..shuffle()).first;
|
||||||
"alter_id": 0,
|
config["server"] = randomNode["host"]; // 提取 host
|
||||||
"security": "auto",
|
config["server_port"] = randomNode["port"]; // 提取 port
|
||||||
if (json["transport"] != null && json["transport"] != "tcp")
|
|
||||||
"transport": _buildTransport(json),
|
|
||||||
"tls": {
|
|
||||||
"enabled": json["security"] == "tls",
|
|
||||||
"server_name": serverName,
|
|
||||||
"insecure": securityConfig["allow_insecure"] ?? true,
|
|
||||||
"utls": {"enabled": true, "fingerprint": "chrome"}
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
break;
|
|
||||||
case "shadowsocks":
|
|
||||||
config = {
|
|
||||||
"type": "shadowsocks",
|
|
||||||
"tag": nodeListItem.name,
|
|
||||||
"server": nodeListItem.serverAddr,
|
|
||||||
"server_port": json["port"],
|
|
||||||
"method": json["method"],
|
|
||||||
"password": nodeListItem.uuid
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case "hysteria":
|
|
||||||
case "hysteria2":
|
|
||||||
// 后端的 "hysteria" 实际上是 Hysteria2 协议
|
|
||||||
final securityConfig =
|
|
||||||
json["security_config"] as Map<String, dynamic>? ?? {};
|
|
||||||
config = {
|
|
||||||
"type": "hysteria2",
|
|
||||||
"tag": nodeListItem.name,
|
|
||||||
"server": nodeListItem.serverAddr,
|
|
||||||
"server_port": json["port"],
|
|
||||||
"password": nodeListItem.uuid,
|
|
||||||
"up_mbps": 100,
|
|
||||||
"down_mbps": 100,
|
|
||||||
"obfs": {
|
|
||||||
"type": "salamander",
|
|
||||||
"password": json["obfs_password"] ?? nodeListItem.uuid
|
|
||||||
},
|
|
||||||
"tls": {
|
|
||||||
"enabled": true,
|
|
||||||
"server_name": securityConfig["sni"] ?? "",
|
|
||||||
"insecure": securityConfig["allow_insecure"] ?? true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case "trojan":
|
|
||||||
final securityConfig =
|
|
||||||
json["security_config"] as Map<String, dynamic>? ?? {};
|
|
||||||
|
|
||||||
// 智能设置 server_name
|
|
||||||
String serverName = securityConfig["sni"] ?? "";
|
|
||||||
if (serverName.isEmpty) {
|
|
||||||
// 如果没有配置 SNI,使用服务器地址
|
|
||||||
serverName = nodeListItem.serverAddr;
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
config = {
|
if (kDebugMode) {
|
||||||
"type": "trojan",
|
print('⚠️ 解析 config 字段失败: $e');
|
||||||
"tag": nodeListItem.name,
|
}
|
||||||
"server": nodeListItem.serverAddr,
|
config = {};
|
||||||
"server_port": json["port"],
|
|
||||||
"password": nodeListItem.uuid,
|
|
||||||
"tls": {
|
|
||||||
"enabled": json["security"] == "tls",
|
|
||||||
"server_name": serverName,
|
|
||||||
"insecure": securityConfig["allow_insecure"] ?? true,
|
|
||||||
"utls": {"enabled": true, "fingerprint": "chrome"}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查 relayNode 是否为 JSON 字符串并解析
|
|
||||||
if (nodeListItem.relayNode.isNotEmpty && nodeListItem.relayMode != "none") {
|
|
||||||
final relayNodeJson = jsonDecode(nodeListItem.relayNode);
|
|
||||||
if (relayNodeJson is List && nodeListItem.relayMode != "none") {
|
|
||||||
// 随机选择一个元素
|
|
||||||
final randomNode = (relayNodeJson..shuffle()).first;
|
|
||||||
config["server"] = randomNode["host"]; // 提取 host
|
|
||||||
config["server_port"] = randomNode["port"]; // 提取 port
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 解析配置
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
// 现在它可以正确地访问已保存的 nodeListItem 成员
|
|
||||||
return 'KROutboundItem(name: ${nodeListItem.name}, protocol: ${nodeListItem.protocol}, server: ${nodeListItem.serverAddr}, port: ${nodeListItem.port})';
|
return 'KROutboundItem(name: ${nodeListItem.name}, protocol: ${nodeListItem.protocol}, server: ${nodeListItem.serverAddr}, port: ${nodeListItem.port})';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建传输配置
|
/// 构建传输配置
|
||||||
Map<String, dynamic> _buildTransport(Map<String, dynamic> json) {
|
Map<String, dynamic> _buildTransport(Map<String, dynamic> json) {
|
||||||
final transportType = json["transport"] as String?;
|
final transportType = json["transport"] as String?;
|
||||||
@ -252,27 +304,6 @@ class KROutboundItem {
|
|||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print('🔧 开始构建节点配置 - 协议: ${nodeListItem.protocol}, 名称: ${nodeListItem.name}');
|
print('🔧 开始构建节点配置 - 协议: ${nodeListItem.protocol}, 名称: ${nodeListItem.name}');
|
||||||
}
|
}
|
||||||
if (kDebugMode) {
|
|
||||||
print('📋 节点详细信息:');
|
|
||||||
}
|
|
||||||
if (kDebugMode) {
|
|
||||||
print(' - serverAddr: ${nodeListItem.serverAddr}');
|
|
||||||
}
|
|
||||||
if (kDebugMode) {
|
|
||||||
print(' - port: ${nodeListItem.port}');
|
|
||||||
}
|
|
||||||
if (kDebugMode) {
|
|
||||||
print(' - uuid: ${nodeListItem.uuid}');
|
|
||||||
}
|
|
||||||
if (kDebugMode) {
|
|
||||||
print(' - method: ${nodeListItem.method}');
|
|
||||||
}
|
|
||||||
if (kDebugMode) {
|
|
||||||
print(' - config: ${nodeListItem.config}');
|
|
||||||
}
|
|
||||||
if (kDebugMode) {
|
|
||||||
print(' - protocols: ${nodeListItem.protocols}');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🔧 尝试从 config 字段解析 transport 配置
|
// 🔧 尝试从 config 字段解析 transport 配置
|
||||||
Map<String, dynamic>? transportConfig;
|
Map<String, dynamic>? transportConfig;
|
||||||
|
|||||||
@ -88,6 +88,9 @@ class KrOutboundsList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 生成国家自动选择虚拟节点
|
||||||
|
_generateCountryAutoNodes(countryGroups);
|
||||||
|
|
||||||
// 将标签分组转换为 KRGroupOutboundList 并添加到 groupOutboundList
|
// 将标签分组转换为 KRGroupOutboundList 并添加到 groupOutboundList
|
||||||
for (var tag in tagGroups.keys) {
|
for (var tag in tagGroups.keys) {
|
||||||
final item = KRGroupOutboundList(
|
final item = KRGroupOutboundList(
|
||||||
@ -108,7 +111,40 @@ class KrOutboundsList {
|
|||||||
country: country,
|
country: country,
|
||||||
outboundList: countryGroups[country]!)); // 添加国家分组到列表
|
outboundList: countryGroups[country]!)); // 添加国家分组到列表
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成国家自动选择虚拟节点
|
||||||
|
void _generateCountryAutoNodes(Map<String, List<KROutboundItem>> countryGroups) {
|
||||||
|
for (var entry in countryGroups.entries) {
|
||||||
|
final country = entry.key;
|
||||||
|
final nodes = entry.value;
|
||||||
|
final autoTag = '${country}-auto';
|
||||||
|
|
||||||
|
if (kDebugMode) {
|
||||||
|
print('🤖 生成国家自动选择节点: $autoTag, 包含 ${nodes.length} 个节点');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建 urltest 配置
|
||||||
|
final urltestConfig = {
|
||||||
|
'type': 'urltest',
|
||||||
|
'tag': autoTag,
|
||||||
|
'outbounds': nodes.map((node) => node.tag).toList(),
|
||||||
|
'url': 'https://www.google.com/generate_204',
|
||||||
|
'interval': '10m',
|
||||||
|
'tolerance': 50,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建虚拟节点
|
||||||
|
final virtualNode = KROutboundItem.fromVirtual(autoTag, country, urltestConfig);
|
||||||
|
|
||||||
|
// 添加到各个列表
|
||||||
|
allList.add(virtualNode);
|
||||||
|
keyList[autoTag] = virtualNode;
|
||||||
|
configJsonList.add(urltestConfig);
|
||||||
|
|
||||||
|
if (kDebugMode) {
|
||||||
|
print('✅ 生成虚拟节点: $autoTag, 配置: ${urltestConfig.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,15 @@ class HINodeListController extends GetxController {
|
|||||||
/// 首页服务
|
/// 首页服务
|
||||||
final KRHomeController homeController = Get.find<KRHomeController>();
|
final KRHomeController homeController = Get.find<KRHomeController>();
|
||||||
|
|
||||||
|
/// 调试模式状态
|
||||||
|
final RxBool isDebugMode = false.obs;
|
||||||
|
|
||||||
|
/// 模式按钮点击计数器
|
||||||
|
int modeButtonClickCount = 0;
|
||||||
|
|
||||||
|
/// 最后一次点击时间
|
||||||
|
DateTime? lastModeButtonClickTime;
|
||||||
|
|
||||||
/// 获取连接类型字符串
|
/// 获取连接类型字符串
|
||||||
String kr_getConnectionTypeString() {
|
String kr_getConnectionTypeString() {
|
||||||
final connectionType = KRSingBoxImp.instance.kr_connectionType.value;
|
final connectionType = KRSingBoxImp.instance.kr_connectionType.value;
|
||||||
@ -41,4 +50,48 @@ class HINodeListController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 处理模式按钮点击(用于激活调试模式)
|
||||||
|
void kr_handleModeButtonClick() {
|
||||||
|
final now = DateTime.now();
|
||||||
|
|
||||||
|
// 检查是否在2秒内连续点击
|
||||||
|
if (lastModeButtonClickTime != null &&
|
||||||
|
now.difference(lastModeButtonClickTime!).inSeconds > 2) {
|
||||||
|
// 超过2秒,重置计数器
|
||||||
|
modeButtonClickCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
modeButtonClickCount++;
|
||||||
|
lastModeButtonClickTime = now;
|
||||||
|
|
||||||
|
KRLogUtil.kr_i('模式按钮点击次数: $modeButtonClickCount', tag: 'HINodeListController');
|
||||||
|
|
||||||
|
if (modeButtonClickCount >= 5) {
|
||||||
|
// 激活调试模式
|
||||||
|
isDebugMode.value = true;
|
||||||
|
modeButtonClickCount = 0; // 重置计数器
|
||||||
|
KRLogUtil.kr_i('🐛 调试模式已激活!', tag: 'HINodeListController');
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取要显示的节点列表(根据调试模式过滤)
|
||||||
|
List<dynamic> kr_getFilteredNodeList() {
|
||||||
|
if (isDebugMode.value) {
|
||||||
|
// 调试模式:显示所有节点
|
||||||
|
return kr_subscribeService.allList;
|
||||||
|
} else {
|
||||||
|
// 正常模式:只显示 country-auto 节点
|
||||||
|
return kr_subscribeService.allList.where((node) => node.tag.endsWith('-auto')).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 重置调试模式
|
||||||
|
void kr_resetDebugMode() {
|
||||||
|
isDebugMode.value = false;
|
||||||
|
modeButtonClickCount = 0;
|
||||||
|
lastModeButtonClickTime = null;
|
||||||
|
KRLogUtil.kr_i('调试模式已重置', tag: 'HINodeListController');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -231,8 +231,9 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
padding: EdgeInsets.symmetric(vertical: 8.w),
|
padding: EdgeInsets.symmetric(vertical: 8.w),
|
||||||
// 2. 使用 children 属性,并一次性构建所有列表项
|
// 2. 使用 children 属性,并一次性构建所有列表项
|
||||||
children: [
|
children: [
|
||||||
if (controller.kr_subscribeService.allList.isEmpty)
|
if (controller.kr_getFilteredNodeList().isEmpty)
|
||||||
_buildEmptyListPlaceholder(context, AppTranslations.kr_home.noNodes)
|
_buildEmptyListPlaceholder(context,
|
||||||
|
controller.isDebugMode.value ? '调试模式:无节点数据' : AppTranslations.kr_home.noNodes)
|
||||||
else ...[
|
else ...[
|
||||||
InkWell(
|
InkWell(
|
||||||
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
|
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
|
||||||
@ -292,13 +293,28 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
// 2. 第二个 Text: "根据网络IP自动匹配最快线路"
|
// 2. 第二个 Text: "根据网络IP自动匹配最快线路"
|
||||||
Text(
|
Obx(() {
|
||||||
'根据网络IP自动匹配最快线路', // 您指定的文本
|
// 当选择全局 auto 时,显示当前选中的节点信息
|
||||||
style: KrAppTextStyle(
|
if (controller.homeController.kr_cutTag.value == 'auto') {
|
||||||
fontSize: 10,
|
final autoNodeInfo = controller.homeController.kr_getGlobalAutoSelectedNode();
|
||||||
color: Colors.white,
|
if (autoNodeInfo != null) {
|
||||||
),
|
return Text(
|
||||||
),
|
'当前: ${autoNodeInfo['tag']} (${autoNodeInfo['delay']}ms)',
|
||||||
|
style: KrAppTextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: Colors.white.withOpacity(0.8),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Text(
|
||||||
|
'根据网络IP自动匹配最快线路', // 默认文本
|
||||||
|
style: KrAppTextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -319,7 +335,7 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
...controller.kr_subscribeService.allList.map((item) {
|
...controller.kr_getFilteredNodeList().map((item) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
|
// 🔧 修复:改为 async,等待节点切换完成后再关闭列表
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
@ -390,78 +406,75 @@ class HINodeListView extends GetView<HINodeListController> {
|
|||||||
KRCountryFlag(countryCode: item.country, width: 30.w, height: 20.w, isCircle: false, maintainSize: false),
|
KRCountryFlag(countryCode: item.country, width: 30.w, height: 20.w, isCircle: false, maintainSize: false),
|
||||||
SizedBox(width: 12.w),
|
SizedBox(width: 12.w),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Obx(() {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
final isDebug = controller.isDebugMode.value;
|
||||||
children: [
|
|
||||||
Row(
|
final text = isDebug
|
||||||
children: [
|
? '${controller.homeController.kr_getCountryFullName(item.country)} - ${item.tag}'
|
||||||
Flexible(
|
: item.country;
|
||||||
child: Text(
|
|
||||||
'${controller.homeController.kr_getCountryFullName(item.country)}-${item.tag}',
|
return Text(
|
||||||
style: KrAppTextStyle(fontSize: 14, color: Colors.white),
|
text,
|
||||||
overflow: TextOverflow.ellipsis,
|
style: KrAppTextStyle(
|
||||||
maxLines: 1,
|
fontSize: 16,
|
||||||
),
|
fontWeight: FontWeight.w500,
|
||||||
),
|
color: Colors.white,
|
||||||
Obx(() => controller.homeController.kr_cutTag.value == item.tag
|
|
||||||
? Container(
|
|
||||||
margin: EdgeInsets.only(left: 4.w),
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.w),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: krModernGreenLight.withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(4.w),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
AppTranslations.kr_home.selected,
|
|
||||||
style: KrAppTextStyle(fontSize: 10, color: krModernGreen, fontWeight: FontWeight.w500),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink()),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
SizedBox(height: 2.w),
|
);
|
||||||
Obx(() {
|
}),
|
||||||
// 1. 获取延迟值和测速状态
|
|
||||||
int displayDelay = item.urlTestDelay.value;
|
|
||||||
bool isTesting = controller.homeController.kr_isLatency.value;
|
|
||||||
|
|
||||||
// 2. 声明文本和颜色变量
|
|
||||||
String delayText;
|
|
||||||
Color delayColor;
|
|
||||||
|
|
||||||
// 3. 根据状态设置文本和颜色
|
|
||||||
if (isTesting && displayDelay == 0) {
|
|
||||||
delayText = '测速中...';
|
|
||||||
delayColor = Colors.grey; // 测速时使用灰色
|
|
||||||
} else if (displayDelay == 0) {
|
|
||||||
delayText = '- ms'; // 未测速或初始状态
|
|
||||||
delayColor = Colors.grey;
|
|
||||||
} else if (displayDelay >= 3000) {
|
|
||||||
delayText = AppTranslations.kr_home.timeout; // "超时"
|
|
||||||
delayColor = Colors.red; // 超时状态使用红色
|
|
||||||
} else {
|
|
||||||
delayText = '${displayDelay}ms';
|
|
||||||
// 正常延迟,根据快慢设置不同颜色
|
|
||||||
delayColor = (displayDelay < 500) ? krModernGreen : Colors.orange;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 返回最终的 Text 组件
|
|
||||||
return Text(
|
|
||||||
delayText,
|
|
||||||
style: KrAppTextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
color: delayColor, // 使用动态计算出的颜色
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
Text(
|
|
||||||
item.city,
|
|
||||||
style: KrAppTextStyle(fontSize: 12, color: Theme.of(context).textTheme.bodySmall?.color),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
Obx(() {
|
||||||
|
// 1. 获取延迟值和测速状态
|
||||||
|
int displayDelay;
|
||||||
|
bool isTesting = controller.homeController.kr_isLatency.value;
|
||||||
|
|
||||||
|
// 极简逻辑:只根据节点类型决定显示方式
|
||||||
|
print('🔄 _kr_buildNodeListItem节点: item.tag=${item.tag}, country=${item.country}');
|
||||||
|
|
||||||
|
// 如果节点本身是auto组节点,显示组内最快节点的速度
|
||||||
|
if (item.tag.endsWith('-auto')) {
|
||||||
|
print('🎯 检测到auto组节点,获取最快节点信息');
|
||||||
|
final autoNodeInfo = controller.homeController.kr_getCountryAutoSelectedNode(item.country);
|
||||||
|
print('📊 _kr_buildNodeListItem获取auto最快节点: $autoNodeInfo');
|
||||||
|
displayDelay = autoNodeInfo?['delay'] ?? 0;
|
||||||
|
print('✅ _kr_buildNodeListItem使用auto最快节点延迟: $displayDelay');
|
||||||
|
} else {
|
||||||
|
// 普通节点,直接显示节点自身的速度
|
||||||
|
displayDelay = item.urlTestDelay.value;
|
||||||
|
print('🔄 _kr_buildNodeListItem使用普通节点延迟: $displayDelay');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 声明文本和颜色变量
|
||||||
|
String delayText;
|
||||||
|
Color delayColor;
|
||||||
|
|
||||||
|
// 3. 根据状态设置文本和颜色
|
||||||
|
if (isTesting && displayDelay == 0) {
|
||||||
|
delayText = '测速中...';
|
||||||
|
delayColor = Colors.grey; // 测速时使用灰色
|
||||||
|
} else if (displayDelay == 0) {
|
||||||
|
delayText = '- ms'; // 未测速或初始状态
|
||||||
|
delayColor = Colors.grey;
|
||||||
|
} else if (displayDelay >= 3000) {
|
||||||
|
delayText = AppTranslations.kr_home.timeout; // "超时"
|
||||||
|
delayColor = Colors.red; // 超时状态使用红色
|
||||||
|
} else {
|
||||||
|
delayText = '${displayDelay}ms';
|
||||||
|
// 正常延迟,根据快慢设置不同颜色
|
||||||
|
delayColor = (displayDelay < 500) ? krModernGreen : Colors.orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 返回最终的 Text 组件
|
||||||
|
return Text(
|
||||||
|
delayText,
|
||||||
|
style: KrAppTextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: delayColor, // 使用动态计算出的颜色
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
SizedBox(width: 12.w),
|
||||||
Obx(() => controller.homeController.kr_cutTag.value == item.tag
|
Obx(() => controller.homeController.kr_cutTag.value == item.tag
|
||||||
? KrLocalImage(
|
? KrLocalImage(
|
||||||
imageName: 'radio-active-icon',
|
imageName: 'radio-active-icon',
|
||||||
|
|||||||
@ -23,7 +23,49 @@ class HINodePageView extends GetView<HINodeListController> {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// 模式切换器
|
Positioned(
|
||||||
|
left: 0, // ⭐ 所有内容整体贴到左边
|
||||||
|
child: Obx(() => controller.isDebugMode.value
|
||||||
|
? GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
controller.kr_resetDebugMode();
|
||||||
|
Get.snackbar(
|
||||||
|
'调试模式',
|
||||||
|
'已关闭调试模式',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red.withOpacity(0.8),
|
||||||
|
borderRadius: BorderRadius.circular(12.w),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.bug_report,
|
||||||
|
size: 12.w,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
SizedBox(width: 3.w),
|
||||||
|
Text(
|
||||||
|
'关闭调试',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10.sp,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink()),
|
||||||
|
),
|
||||||
|
// 模式切换器
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(left: 100.w, right: 60.w),
|
padding: EdgeInsets.only(left: 100.w, right: 60.w),
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -159,7 +201,12 @@ class HINodePageView extends GetView<HINodeListController> {
|
|||||||
}) {
|
}) {
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: onTap,
|
onTap: () {
|
||||||
|
// 处理模式按钮点击(用于调试模式激活)
|
||||||
|
controller.kr_handleModeButtonClick();
|
||||||
|
// 执行原有的点击逻辑
|
||||||
|
onTap();
|
||||||
|
},
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
padding: EdgeInsets.symmetric(vertical: 4.w),
|
padding: EdgeInsets.symmetric(vertical: 4.w),
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import '../../../common/app_config.dart';
|
|||||||
import '../../../localization/app_translations.dart';
|
import '../../../localization/app_translations.dart';
|
||||||
import '../../../localization/kr_language_utils.dart';
|
import '../../../localization/kr_language_utils.dart';
|
||||||
import '../../../model/business/kr_group_outbound_list.dart';
|
import '../../../model/business/kr_group_outbound_list.dart';
|
||||||
|
import '../../../model/business/kr_outbound_item.dart';
|
||||||
import '../../../services/kr_announcement_service.dart';
|
import '../../../services/kr_announcement_service.dart';
|
||||||
import '../../../utils/kr_event_bus.dart';
|
import '../../../utils/kr_event_bus.dart';
|
||||||
import '../../../utils/kr_update_util.dart';
|
import '../../../utils/kr_update_util.dart';
|
||||||
@ -1421,8 +1422,8 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
|
|
||||||
/// 获取真实连接的节点信息(auto 模式下获取实际连接的节点)
|
/// 获取真实连接的节点信息(auto 模式下获取实际连接的节点)
|
||||||
Map<String, dynamic> kr_getRealConnectedNodeInfo() {
|
Map<String, dynamic> kr_getRealConnectedNodeInfo() {
|
||||||
// 如果不是 auto 模式,直接返回当前选中的节点信息
|
// 如果不是 auto 模式,也不是 country-auto 模式,直接返回当前选中的节点信息
|
||||||
if (kr_cutTag.value != 'auto') {
|
if (kr_cutTag.value != 'auto' && !kr_cutTag.value.endsWith('-auto')) {
|
||||||
final node = kr_subscribeService.keyList[kr_cutSeletedTag.value];
|
final node = kr_subscribeService.keyList[kr_cutSeletedTag.value];
|
||||||
return {
|
return {
|
||||||
'nodeName': kr_cutSeletedTag.value,
|
'nodeName': kr_cutSeletedTag.value,
|
||||||
@ -1431,11 +1432,13 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// auto 模式下,获取 urltest 组的实际连接节点
|
// 处理 auto 模式(包括全局 auto 和 country-auto)
|
||||||
print('当前活动组----${KRSingBoxImp.instance.kr_activeGroups.length}');
|
print('当前活动组----${KRSingBoxImp.instance.kr_activeGroups.length}');
|
||||||
for (var group in KRSingBoxImp.instance.kr_activeGroups) {
|
for (var group in KRSingBoxImp.instance.kr_activeGroups) {
|
||||||
print('当前活动组----$group}');
|
print('当前活动组----$group}');
|
||||||
if (group.type == ProxyType.urltest) {
|
|
||||||
|
// 处理全局 auto 模式
|
||||||
|
if (kr_cutTag.value == 'auto' && group.type == ProxyType.urltest && group.tag == 'auto') {
|
||||||
final selectedNode = group.selected;
|
final selectedNode = group.selected;
|
||||||
final node = kr_subscribeService.keyList[selectedNode];
|
final node = kr_subscribeService.keyList[selectedNode];
|
||||||
return {
|
return {
|
||||||
@ -1444,12 +1447,39 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
'country': node?.country ?? '',
|
'country': node?.country ?? '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理 country-auto 模式
|
||||||
|
if (kr_cutTag.value.endsWith('-auto') && group.type == ProxyType.urltest) {
|
||||||
|
if (group.tag == kr_cutTag.value) {
|
||||||
|
final selectedNode = group.selected;
|
||||||
|
final node = kr_subscribeService.keyList[selectedNode];
|
||||||
|
return {
|
||||||
|
'nodeName': selectedNode,
|
||||||
|
'delay': node?.urlTestDelay.value ?? -2,
|
||||||
|
'country': node?.country ?? '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
print('hhhhhh${kr_subscribeService.keyList}', );
|
print('hhhhhh${kr_subscribeService.keyList}', );
|
||||||
|
|
||||||
|
// 处理 country-auto 模式的备用方案(当 SingBox 组数据不可用时)
|
||||||
|
if (kr_cutTag.value.endsWith('-auto')) {
|
||||||
|
final countryCode = kr_cutTag.value.replaceAll('-auto', '');
|
||||||
|
KRLogUtil.kr_i('🔄 kr_getRealConnectedNodeInfo 使用备用方案获取 country-auto 信息: $countryCode', tag: 'HomeController');
|
||||||
|
final autoNodeInfo = kr_getCountryAutoSelectedNode(countryCode);
|
||||||
|
if (autoNodeInfo != null) {
|
||||||
|
return {
|
||||||
|
'nodeName': autoNodeInfo['tag'],
|
||||||
|
'delay': autoNodeInfo['delay'] ?? -2,
|
||||||
|
'country': autoNodeInfo['country'] ?? countryCode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 如果没有找到 urltest 组,返回默认值
|
// 如果没有找到 urltest 组,返回默认值
|
||||||
return {
|
return {
|
||||||
'nodeName': 'auto',
|
'nodeName': kr_cutTag.value,
|
||||||
'delay': -2,
|
'delay': -2,
|
||||||
'country': '',
|
'country': '',
|
||||||
};
|
};
|
||||||
@ -1470,9 +1500,10 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
/// 获取真实连接的节点国家
|
/// 获取真实连接的节点国家
|
||||||
String kr_getRealConnectedNodeCountry() {
|
String kr_getRealConnectedNodeCountry() {
|
||||||
final info = kr_getRealConnectedNodeInfo();
|
final info = kr_getRealConnectedNodeInfo();
|
||||||
final delay = kr_currentNodeLatency.value;
|
final delay = info['delay'] as int; // 使用真实连接的节点延迟,而不是 kr_currentNodeLatency.value
|
||||||
final country1 = kr_getCurrentNodeCountry();
|
final country1 = kr_getCurrentNodeCountry();
|
||||||
print('country----$country1');
|
print('country----$country1');
|
||||||
|
print('kr_getRealConnectedNodeCountry - delay from info: $delay, country from info: ${info['country']}');
|
||||||
final country = kr_getCountryFullName(info['country']);
|
final country = kr_getCountryFullName(info['country']);
|
||||||
if (delay == -2) {
|
if (delay == -2) {
|
||||||
return '--';
|
return '--';
|
||||||
@ -2181,6 +2212,225 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取国家-auto组的当前选中子节点信息
|
||||||
|
Map<String, dynamic>? kr_getCountryAutoSelectedNode(String countryCode) {
|
||||||
|
try {
|
||||||
|
final activeGroups = KRSingBoxImp.instance.kr_activeGroups;
|
||||||
|
final allGroups = KRSingBoxImp.instance.kr_allGroups;
|
||||||
|
final autoGroupTag = '${countryCode}-auto';
|
||||||
|
|
||||||
|
KRLogUtil.kr_i('🔍 开始获取国家-auto选中节点: countryCode=$countryCode, autoGroupTag=$autoGroupTag', tag: 'HomeController');
|
||||||
|
KRLogUtil.kr_i('📊 活跃组数量: ${activeGroups.length}, 所有组数量: ${allGroups.length}', tag: 'HomeController');
|
||||||
|
|
||||||
|
// 优先检查活跃组
|
||||||
|
if (activeGroups.isNotEmpty) {
|
||||||
|
KRLogUtil.kr_i('✅ 活跃组不为空,优先检查活跃组', tag: 'HomeController');
|
||||||
|
for (var group in activeGroups) {
|
||||||
|
KRLogUtil.kr_i('🔄 检查活跃组: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'HomeController');
|
||||||
|
|
||||||
|
if (group.tag == autoGroupTag && group.type == ProxyType.urltest) {
|
||||||
|
KRLogUtil.kr_i('✅ 在活跃组中找到匹配的urltest组: $autoGroupTag', tag: 'HomeController');
|
||||||
|
return _kr_extractSelectedNodeInfo(group, countryCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果活跃组中没有,检查所有组
|
||||||
|
if (allGroups.isNotEmpty) {
|
||||||
|
KRLogUtil.kr_i('🔍 活跃组中未找到,检查所有组', tag: 'HomeController');
|
||||||
|
for (var group in allGroups) {
|
||||||
|
KRLogUtil.kr_i('🔄 检查所有组: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'HomeController');
|
||||||
|
|
||||||
|
if (group.tag == autoGroupTag && group.type == ProxyType.urltest) {
|
||||||
|
KRLogUtil.kr_i('✅ 在所有组中找到匹配的urltest组: $autoGroupTag', tag: 'HomeController');
|
||||||
|
return _kr_extractSelectedNodeInfo(group, countryCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果组数据都为空,使用备用方案:从订阅服务中找该国家最快的节点
|
||||||
|
KRLogUtil.kr_i('🔄 组数据为空,使用备用方案从订阅服务获取最快节点', tag: 'HomeController');
|
||||||
|
return _kr_getFastestNodeFromSubscribeService(countryCode);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_e('💥 获取国家-auto选中节点异常: $e', tag: 'HomeController');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从订阅服务获取该国家最快节点的备用方案
|
||||||
|
Map<String, dynamic>? _kr_getFastestNodeFromSubscribeService(String countryCode) {
|
||||||
|
try {
|
||||||
|
KRLogUtil.kr_i('🔄 使用订阅服务查找国家最快节点: $countryCode', tag: 'HomeController');
|
||||||
|
|
||||||
|
// 从订阅服务中获取该国家的所有节点
|
||||||
|
final allNodes = kr_subscribeService.allList.where((node) =>
|
||||||
|
node.country == countryCode && !node.tag.endsWith('-auto')
|
||||||
|
).toList();
|
||||||
|
|
||||||
|
KRLogUtil.kr_i('📊 找到 ${allNodes.length} 个该国家的普通节点', tag: 'HomeController');
|
||||||
|
|
||||||
|
if (allNodes.isEmpty) {
|
||||||
|
KRLogUtil.kr_w('⚠️ 未找到该国家的任何普通节点: $countryCode', tag: 'HomeController');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找出延迟最小的节点(排除0和超时)
|
||||||
|
KROutboundItem? fastestNode;
|
||||||
|
int fastestDelay = 999999;
|
||||||
|
|
||||||
|
for (var node in allNodes) {
|
||||||
|
final delay = node.urlTestDelay.value;
|
||||||
|
if (delay > 0 && delay < 3000 && delay < fastestDelay) {
|
||||||
|
fastestDelay = delay;
|
||||||
|
fastestNode = node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fastestNode != null) {
|
||||||
|
KRLogUtil.kr_i('✅ 找到最快节点: ${fastestNode.tag}, 延迟: ${fastestDelay}ms', tag: 'HomeController');
|
||||||
|
return {
|
||||||
|
'tag': fastestNode.tag,
|
||||||
|
'delay': fastestDelay,
|
||||||
|
'country': countryCode,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
KRLogUtil.kr_w('⚠️ 该国家的节点都没有有效延迟数据', tag: 'HomeController');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_e('💥 获取订阅服务最快节点异常: $e', tag: 'HomeController');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 提取选中节点信息的辅助方法
|
||||||
|
Map<String, dynamic>? _kr_extractSelectedNodeInfo(dynamic group, String countryCode) {
|
||||||
|
try {
|
||||||
|
// 获取当前选中的节点
|
||||||
|
final selectedNode = group.selected;
|
||||||
|
KRLogUtil.kr_i('🎯 当前选中节点: $selectedNode', tag: 'HomeController');
|
||||||
|
KRLogUtil.kr_i('📋 组内项目数量: ${group.items.length}', tag: 'HomeController');
|
||||||
|
|
||||||
|
if (selectedNode != null && selectedNode.isNotEmpty) {
|
||||||
|
KRLogUtil.kr_i('✅ 选中节点有效,开始查找节点详情', tag: 'HomeController');
|
||||||
|
|
||||||
|
// 打印所有节点信息用于调试
|
||||||
|
for (var item in group.items) {
|
||||||
|
KRLogUtil.kr_i('📄 组内节点: tag=${item.tag}, delay=${item.urlTestDelay}', tag: 'HomeController');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在组内查找选中的节点
|
||||||
|
try {
|
||||||
|
final selectedItem = group.items.firstWhere(
|
||||||
|
(item) => item.tag == selectedNode,
|
||||||
|
);
|
||||||
|
|
||||||
|
KRLogUtil.kr_i('🎉 成功找到选中节点: tag=${selectedItem.tag}, delay=${selectedItem.urlTestDelay}', tag: 'HomeController');
|
||||||
|
|
||||||
|
return {
|
||||||
|
'tag': selectedNode,
|
||||||
|
'delay': selectedItem.urlTestDelay,
|
||||||
|
'country': countryCode,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_e('❌ 在组内未找到选中节点: $selectedNode', tag: 'HomeController');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
KRLogUtil.kr_w('⚠️ 选中节点为空或无效', tag: 'HomeController');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_e('💥 提取选中节点信息异常: $e', tag: 'HomeController');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取全局 auto 组的当前选中子节点信息
|
||||||
|
Map<String, dynamic>? kr_getGlobalAutoSelectedNode() {
|
||||||
|
try {
|
||||||
|
final activeGroups = KRSingBoxImp.instance.kr_activeGroups;
|
||||||
|
|
||||||
|
KRLogUtil.kr_i('🌍 开始获取全局auto选中节点信息', tag: 'HomeController');
|
||||||
|
KRLogUtil.kr_i('📊 活跃组数量: ${activeGroups.length}', tag: 'HomeController');
|
||||||
|
|
||||||
|
for (var group in activeGroups) {
|
||||||
|
KRLogUtil.kr_i('🔄 检查全局组: tag=${group.tag}, type=${group.type}, selected=${group.selected}', tag: 'HomeController');
|
||||||
|
KRLogUtil.kr_i('📋 全局组内项目数量: ${group.items.length}', tag: 'HomeController');
|
||||||
|
|
||||||
|
if (group.tag == 'auto' && group.type == ProxyType.urltest) {
|
||||||
|
KRLogUtil.kr_i('✅ 找到全局auto urltest组', tag: 'HomeController');
|
||||||
|
|
||||||
|
// 获取当前选中的节点
|
||||||
|
final selectedNode = group.selected;
|
||||||
|
KRLogUtil.kr_i('🎯 全局auto当前选中节点: $selectedNode', tag: 'HomeController');
|
||||||
|
|
||||||
|
if (selectedNode != null && selectedNode.isNotEmpty) {
|
||||||
|
KRLogUtil.kr_i('✅ 全局auto选中节点有效,开始查找详情', tag: 'HomeController');
|
||||||
|
|
||||||
|
// 打印所有节点信息用于调试
|
||||||
|
for (var item in group.items) {
|
||||||
|
KRLogUtil.kr_i('📄 全局auto组内节点: tag=${item.tag}, delay=${item.urlTestDelay}', tag: 'HomeController');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在组内查找选中的节点
|
||||||
|
try {
|
||||||
|
final selectedItem = group.items.firstWhere(
|
||||||
|
(item) => item.tag == selectedNode,
|
||||||
|
);
|
||||||
|
|
||||||
|
KRLogUtil.kr_i('🎉 成功找到全局auto选中节点: tag=${selectedItem.tag}, delay=${selectedItem.urlTestDelay}', tag: 'HomeController');
|
||||||
|
|
||||||
|
return {
|
||||||
|
'tag': selectedNode,
|
||||||
|
'delay': selectedItem.urlTestDelay,
|
||||||
|
'country': '',
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_e('❌ 在全局auto组内未找到选中节点: $selectedNode', tag: 'HomeController');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
KRLogUtil.kr_w('⚠️ 全局auto选中节点为空或无效', tag: 'HomeController');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KRLogUtil.kr_w('❌ 未找到全局auto组', tag: 'HomeController');
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_e('💥 获取全局auto选中节点异常: $e', tag: 'HomeController');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取指定国家的所有真实节点延迟列表
|
||||||
|
List<Map<String, dynamic>> kr_getCountryRealNodeDelays(String countryCode) {
|
||||||
|
final delays = <Map<String, dynamic>>[];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 从订阅服务中获取该国家的节点列表
|
||||||
|
final countryNodes = kr_subscribeService.keyList.values
|
||||||
|
.where((item) => item.country == countryCode && !item.tag.endsWith('-auto'))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
for (final node in countryNodes) {
|
||||||
|
delays.add({
|
||||||
|
'tag': node.tag,
|
||||||
|
'delay': node.urlTestDelay.value,
|
||||||
|
'city': node.city,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按延迟排序
|
||||||
|
delays.sort((a, b) => a['delay'].compareTo(b['delay']));
|
||||||
|
} catch (e) {
|
||||||
|
KRLogUtil.kr_e('获取国家真实节点延迟列表失败: $e', tag: 'HomeController');
|
||||||
|
}
|
||||||
|
|
||||||
|
return delays;
|
||||||
|
}
|
||||||
|
|
||||||
/// 尝试从活动组更新延迟值
|
/// 尝试从活动组更新延迟值
|
||||||
bool _kr_tryUpdateDelayFromActiveGroups() {
|
bool _kr_tryUpdateDelayFromActiveGroups() {
|
||||||
try {
|
try {
|
||||||
@ -2206,6 +2456,17 @@ class KRHomeController extends GetxController with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 如果是国家-auto模式,从对应的国家urltest组获取延迟
|
||||||
|
else if (kr_cutTag.value.endsWith('-auto')) {
|
||||||
|
final countryCode = kr_cutTag.value.replaceAll('-auto', '');
|
||||||
|
for (var item in group.items) {
|
||||||
|
if (item.tag == kr_cutTag.value && item.urlTestDelay != 0) {
|
||||||
|
kr_currentNodeLatency.value = item.urlTestDelay;
|
||||||
|
KRLogUtil.kr_i('✅ ${countryCode}-auto模式延迟值: ${item.urlTestDelay}ms', tag: 'HomeController');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// 手动选择模式
|
// 手动选择模式
|
||||||
else {
|
else {
|
||||||
for (var item in group.items) {
|
for (var item in group.items) {
|
||||||
|
|||||||
@ -1025,10 +1025,25 @@ class KRSingBoxImp {
|
|||||||
KRLogUtil.kr_i('💾 开始保存配置文件...', tag: 'SingBox');
|
KRLogUtil.kr_i('💾 开始保存配置文件...', tag: 'SingBox');
|
||||||
KRLogUtil.kr_i('📊 出站节点数量: ${outbounds.length}', tag: 'SingBox');
|
KRLogUtil.kr_i('📊 出站节点数量: ${outbounds.length}', tag: 'SingBox');
|
||||||
|
|
||||||
|
// 分离真实节点和自动选择节点
|
||||||
|
final realNodes = <Map<String, dynamic>>[];
|
||||||
|
final autoNodes = <Map<String, dynamic>>[];
|
||||||
|
|
||||||
|
for (final outbound in outbounds) {
|
||||||
|
if (outbound['tag']?.toString().endsWith('-auto') == true) {
|
||||||
|
autoNodes.add(outbound);
|
||||||
|
} else {
|
||||||
|
realNodes.add(outbound);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KRLogUtil.kr_i('📊 真实节点数量: ${realNodes.length}', tag: 'SingBox');
|
||||||
|
KRLogUtil.kr_i('📊 自动选择节点数量: ${autoNodes.length}', tag: 'SingBox');
|
||||||
|
|
||||||
// 打印每个节点的详细配置
|
// 打印每个节点的详细配置
|
||||||
for (int i = 0; i < outbounds.length; i++) {
|
for (int i = 0; i < realNodes.length; i++) {
|
||||||
final outbound = outbounds[i];
|
final outbound = realNodes[i];
|
||||||
KRLogUtil.kr_i('📋 节点[$i] 配置:', tag: 'SingBox');
|
KRLogUtil.kr_i('📋 真实节点[$i] 配置:', tag: 'SingBox');
|
||||||
KRLogUtil.kr_i(' - type: ${outbound['type']}', tag: 'SingBox');
|
KRLogUtil.kr_i(' - type: ${outbound['type']}', tag: 'SingBox');
|
||||||
KRLogUtil.kr_i(' - tag: ${outbound['tag']}', tag: 'SingBox');
|
KRLogUtil.kr_i(' - tag: ${outbound['tag']}', tag: 'SingBox');
|
||||||
KRLogUtil.kr_i(' - server: ${outbound['server']}', tag: 'SingBox');
|
KRLogUtil.kr_i(' - server: ${outbound['server']}', tag: 'SingBox');
|
||||||
@ -1042,11 +1057,20 @@ class KRSingBoxImp {
|
|||||||
if (outbound['uuid'] != null) {
|
if (outbound['uuid'] != null) {
|
||||||
KRLogUtil.kr_i(' - uuid: ${outbound['uuid']?.toString().substring(0, 8)}...', tag: 'SingBox');
|
KRLogUtil.kr_i(' - uuid: ${outbound['uuid']?.toString().substring(0, 8)}...', tag: 'SingBox');
|
||||||
}
|
}
|
||||||
KRLogUtil.kr_i(' - 完整配置: ${jsonEncode(outbound)}', tag: 'SingBox');
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < autoNodes.length; i++) {
|
||||||
|
final outbound = autoNodes[i];
|
||||||
|
KRLogUtil.kr_i('🤖 自动选择节点[$i] 配置:', tag: 'SingBox');
|
||||||
|
KRLogUtil.kr_i(' - type: ${outbound['type']}', tag: 'SingBox');
|
||||||
|
KRLogUtil.kr_i(' - tag: ${outbound['tag']}', tag: 'SingBox');
|
||||||
|
if (outbound['outbounds'] != null) {
|
||||||
|
KRLogUtil.kr_i(' - outbounds: ${outbound['outbounds']}', tag: 'SingBox');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⚠️ 临时过滤 Hysteria2 节点以避免 libcore 崩溃
|
// ⚠️ 临时过滤 Hysteria2 节点以避免 libcore 崩溃
|
||||||
kr_outbounds = outbounds.where((outbound) {
|
final filteredRealNodes = realNodes.where((outbound) {
|
||||||
final type = outbound['type'];
|
final type = outbound['type'];
|
||||||
if (type == 'hysteria2' || type == 'hysteria') {
|
if (type == 'hysteria2' || type == 'hysteria') {
|
||||||
KRLogUtil.kr_w('⚠️ 跳过 Hysteria2 节点: ${outbound['tag']} (libcore bug)', tag: 'SingBox');
|
KRLogUtil.kr_w('⚠️ 跳过 Hysteria2 节点: ${outbound['tag']} (libcore bug)', tag: 'SingBox');
|
||||||
@ -1055,11 +1079,30 @@ class KRSingBoxImp {
|
|||||||
return true;
|
return true;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
KRLogUtil.kr_i('✅ 过滤后节点数量: ${kr_outbounds.length}/${outbounds.length}', tag: 'SingBox');
|
// 过滤自动选择节点中的 Hysteria2
|
||||||
|
final filteredAutoNodes = autoNodes.where((outbound) {
|
||||||
|
if (outbound['outbounds'] != null) {
|
||||||
|
final outbounds = outbound['outbounds'] as List<dynamic>;
|
||||||
|
// 检查是否包含 Hysteria2 节点
|
||||||
|
final hasHysteria2 = outbounds.any((tag) => tag.toString().toLowerCase().contains('hysteria'));
|
||||||
|
if (hasHysteria2) {
|
||||||
|
KRLogUtil.kr_w('⚠️ 跳过包含 Hysteria2 的自动选择节点: ${outbound['tag']} (libcore bug)', tag: 'SingBox');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
KRLogUtil.kr_i('✅ 过滤后真实节点数量: ${filteredRealNodes.length}/${realNodes.length}', tag: 'SingBox');
|
||||||
|
KRLogUtil.kr_i('✅ 过滤后自动选择节点数量: ${filteredAutoNodes.length}/${autoNodes.length}', tag: 'SingBox');
|
||||||
|
|
||||||
|
// 构建所有可用标签列表(selector 包含真实和自动节点)
|
||||||
|
final allAvailableTags = <String>[];
|
||||||
|
allAvailableTags.addAll(filteredAutoNodes.map((o) => o['tag'] as String));
|
||||||
|
allAvailableTags.addAll(filteredRealNodes.map((o) => o['tag'] as String));
|
||||||
|
|
||||||
// 🔧 修复:生成完整的 SingBox 配置
|
// 🔧 修复:生成完整的 SingBox 配置
|
||||||
// 之前只保存 {"outbounds": [...]}, 导致 libcore 无法正确处理
|
// 现在保存完整配置,包含所有必需字段,并且 outbounds 顺序为:selector→自动组→真实节点→direct/block/dns-out
|
||||||
// 现在保存完整配置,包含所有必需字段
|
|
||||||
final Map<String, dynamic> fullConfig = {
|
final Map<String, dynamic> fullConfig = {
|
||||||
"log": {
|
"log": {
|
||||||
"level": "debug",
|
"level": "debug",
|
||||||
@ -1096,14 +1139,17 @@ class KRSingBoxImp {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outbounds": [
|
"outbounds": [
|
||||||
// 🔧 修复:添加 selector 组,让用户可以手动选择节点
|
// 🔧 修复:添加 selector 组,包含真实和自动节点标签
|
||||||
{
|
{
|
||||||
"type": "selector",
|
"type": "selector",
|
||||||
"tag": "proxy",
|
"tag": "proxy",
|
||||||
"outbounds": kr_outbounds.map((o) => o['tag'] as String).toList(),
|
"outbounds": allAvailableTags,
|
||||||
"default": kr_outbounds.isNotEmpty ? kr_outbounds[0]['tag'] : "direct",
|
"default": allAvailableTags.isNotEmpty ? allAvailableTags[0] : "direct",
|
||||||
},
|
},
|
||||||
...kr_outbounds,
|
// 🔧 修复:自动选择组排在真实节点前面
|
||||||
|
...filteredAutoNodes,
|
||||||
|
// 🔧 修复:真实节点排在后面
|
||||||
|
...filteredRealNodes,
|
||||||
{
|
{
|
||||||
"type": "direct",
|
"type": "direct",
|
||||||
"tag": "direct"
|
"tag": "direct"
|
||||||
@ -1135,7 +1181,8 @@ class KRSingBoxImp {
|
|||||||
final mapStr = jsonEncode(fullConfig);
|
final mapStr = jsonEncode(fullConfig);
|
||||||
|
|
||||||
KRLogUtil.kr_i('📄 完整配置文件长度: ${mapStr.length}', tag: 'SingBox');
|
KRLogUtil.kr_i('📄 完整配置文件长度: ${mapStr.length}', tag: 'SingBox');
|
||||||
KRLogUtil.kr_i('📄 Outbounds 数量: ${kr_outbounds.length}', tag: 'SingBox');
|
KRLogUtil.kr_i('📄 Outbounds 总数: ${filteredAutoNodes.length + filteredRealNodes.length}', tag: 'SingBox');
|
||||||
|
KRLogUtil.kr_i('📄 Selector 可用标签数: ${allAvailableTags.length}', tag: 'SingBox');
|
||||||
KRLogUtil.kr_i('📄 配置前800字符:\n${mapStr.substring(0, mapStr.length > 800 ? 800 : mapStr.length)}', tag: 'SingBox');
|
KRLogUtil.kr_i('📄 配置前800字符:\n${mapStr.substring(0, mapStr.length > 800 ? 800 : mapStr.length)}', tag: 'SingBox');
|
||||||
|
|
||||||
await file.writeAsString(mapStr);
|
await file.writeAsString(mapStr);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user