diff --git a/android/app/build.gradle b/android/app/build.gradle index cc5255a..782d479 100755 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -57,7 +57,7 @@ android { defaultConfig { applicationId "app.hifastvpn.com" minSdkVersion flutter.minSdkVersion - targetSdkVersion 36 + targetSdkVersion 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName multiDexEnabled true diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 147943f..d94a247 100755 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -34,6 +34,11 @@ + + + + + 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(); protocol = nodeListItem.protocol; latitude = nodeListItem.latitude; @@ -56,167 +110,165 @@ class KROutboundItem { return; } - // 兜底:尝试解析 config 字段(旧API格式) - if (nodeListItem.config.isEmpty) { - if (kDebugMode) { - print('❌ 节点 ${nodeListItem.name} 缺少配置信息(无port或config)'); - } - config = {}; - return; - } - - late Map json; - try { - json = jsonDecode(nodeListItem.config) as Map; - } 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? ?? {}; - - // 智能设置 server_name - String serverName = securityConfig["sni"] ?? ""; - if (serverName.isEmpty) { - serverName = nodeListItem.serverAddr; + // 兜底:尝试从 config 字段解析(旧API格式) + if (nodeListItem.config.isNotEmpty) { + try { + final json = jsonDecode(nodeListItem.config) as Map; + if (kDebugMode) { + print('📄 解析到 config JSON: $json'); } - config = { - "type": "vless", - "tag": nodeListItem.name, - "server": nodeListItem.serverAddr, - "server_port": json["port"], - "uuid": nodeListItem.uuid, - if (json["flow"] != null && json["flow"] != "none") - "flow": json["flow"], - 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": securityConfig["fingerprint"] ?? "chrome" + // 提取 transport 配置 + Map? transportConfig; + if (json['transport'] != null && json['transport'] != 'tcp') { + transportConfig = _buildTransport(json); + if (kDebugMode) { + print('✅ 找到 transport 配置: $transportConfig'); + } + } + + // 提取 security_config + Map? securityConfig; + if (json['security_config'] != null) { + securityConfig = json['security_config'] as Map; + if (kDebugMode) { + print('✅ 找到 security_config: $securityConfig'); + } + } + + // 根据协议类型构建配置 + 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? ?? {}; + 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}'); } - } - }; - break; - case "vmess": - final securityConfig = - json["security_config"] as Map? ?? {}; - - // 智能设置 server_name - String serverName = securityConfig["sni"] ?? ""; - if (serverName.isEmpty) { - serverName = nodeListItem.serverAddr; + config = {}; } - config = { - "type": "vmess", - "tag": nodeListItem.name, - "server": nodeListItem.serverAddr, - "server_port": json["port"], - "uuid": nodeListItem.uuid, - "alter_id": 0, - "security": "auto", - 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"} + // 检查 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 } - }; - 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? ?? {}; - 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? ?? {}; - - // 智能设置 server_name - String serverName = securityConfig["sni"] ?? ""; - if (serverName.isEmpty) { - // 如果没有配置 SNI,使用服务器地址 - serverName = nodeListItem.serverAddr; } - - config = { - "type": "trojan", - "tag": nodeListItem.name, - "server": nodeListItem.serverAddr, - "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 + } catch (e) { + if (kDebugMode) { + print('⚠️ 解析 config 字段失败: $e'); + } + config = {}; } } - // 解析配置 } @override String toString() { - // 现在它可以正确地访问已保存的 nodeListItem 成员 return 'KROutboundItem(name: ${nodeListItem.name}, protocol: ${nodeListItem.protocol}, server: ${nodeListItem.serverAddr}, port: ${nodeListItem.port})'; } + /// 构建传输配置 Map _buildTransport(Map json) { final transportType = json["transport"] as String?; @@ -252,27 +304,6 @@ class KROutboundItem { if (kDebugMode) { 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 配置 Map? transportConfig; diff --git a/lib/app/model/business/kr_outbounds_list.dart b/lib/app/model/business/kr_outbounds_list.dart index c7279af..41853a6 100755 --- a/lib/app/model/business/kr_outbounds_list.dart +++ b/lib/app/model/business/kr_outbounds_list.dart @@ -88,6 +88,9 @@ class KrOutboundsList { } } + // 生成国家自动选择虚拟节点 + _generateCountryAutoNodes(countryGroups); + // 将标签分组转换为 KRGroupOutboundList 并添加到 groupOutboundList for (var tag in tagGroups.keys) { final item = KRGroupOutboundList( @@ -108,7 +111,40 @@ class KrOutboundsList { country: country, outboundList: countryGroups[country]!)); // 添加国家分组到列表 } + } + /// 生成国家自动选择虚拟节点 + void _generateCountryAutoNodes(Map> 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()}'); + } + } } } diff --git a/lib/app/modules/hi_node_list/controllers/hi_node_list_controller.dart b/lib/app/modules/hi_node_list/controllers/hi_node_list_controller.dart index 2e6c79e..a5e41d9 100644 --- a/lib/app/modules/hi_node_list/controllers/hi_node_list_controller.dart +++ b/lib/app/modules/hi_node_list/controllers/hi_node_list_controller.dart @@ -12,6 +12,15 @@ class HINodeListController extends GetxController { /// 首页服务 final KRHomeController homeController = Get.find(); + /// 调试模式状态 + final RxBool isDebugMode = false.obs; + + /// 模式按钮点击计数器 + int modeButtonClickCount = 0; + + /// 最后一次点击时间 + DateTime? lastModeButtonClickTime; + /// 获取连接类型字符串 String kr_getConnectionTypeString() { 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 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'); + } + } diff --git a/lib/app/modules/hi_node_list/views/hi_node_list_view.dart b/lib/app/modules/hi_node_list/views/hi_node_list_view.dart index 9cc1f35..86044e4 100755 --- a/lib/app/modules/hi_node_list/views/hi_node_list_view.dart +++ b/lib/app/modules/hi_node_list/views/hi_node_list_view.dart @@ -231,8 +231,9 @@ class HINodeListView extends GetView { padding: EdgeInsets.symmetric(vertical: 8.w), // 2. 使用 children 属性,并一次性构建所有列表项 children: [ - if (controller.kr_subscribeService.allList.isEmpty) - _buildEmptyListPlaceholder(context, AppTranslations.kr_home.noNodes) + if (controller.kr_getFilteredNodeList().isEmpty) + _buildEmptyListPlaceholder(context, + controller.isDebugMode.value ? '调试模式:无节点数据' : AppTranslations.kr_home.noNodes) else ...[ InkWell( // 🔧 修复:改为 async,等待节点切换完成后再关闭列表 @@ -292,13 +293,28 @@ class HINodeListView extends GetView { ), ), // 2. 第二个 Text: "根据网络IP自动匹配最快线路" - Text( - '根据网络IP自动匹配最快线路', // 您指定的文本 - style: KrAppTextStyle( - fontSize: 10, - color: Colors.white, - ), - ), + Obx(() { + // 当选择全局 auto 时,显示当前选中的节点信息 + if (controller.homeController.kr_cutTag.value == 'auto') { + final autoNodeInfo = controller.homeController.kr_getGlobalAutoSelectedNode(); + 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 { ), ), ), - ...controller.kr_subscribeService.allList.map((item) { + ...controller.kr_getFilteredNodeList().map((item) { return InkWell( // 🔧 修复:改为 async,等待节点切换完成后再关闭列表 onTap: () async { @@ -390,78 +406,75 @@ class HINodeListView extends GetView { KRCountryFlag(countryCode: item.country, width: 30.w, height: 20.w, isCircle: false, maintainSize: false), SizedBox(width: 12.w), Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Flexible( - child: Text( - '${controller.homeController.kr_getCountryFullName(item.country)}-${item.tag}', - style: KrAppTextStyle(fontSize: 14, color: Colors.white), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ), - 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()), - ], + child: Obx(() { + final isDebug = controller.isDebugMode.value; + + final text = isDebug + ? '${controller.homeController.kr_getCountryFullName(item.country)} - ${item.tag}' + : item.country; + + return Text( + text, + style: KrAppTextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.white, ), - 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 ? KrLocalImage( imageName: 'radio-active-icon', diff --git a/lib/app/modules/hi_node_list/views/hi_page_node_view.dart b/lib/app/modules/hi_node_list/views/hi_page_node_view.dart index 3f80e09..9f36e50 100644 --- a/lib/app/modules/hi_node_list/views/hi_page_node_view.dart +++ b/lib/app/modules/hi_node_list/views/hi_page_node_view.dart @@ -23,7 +23,49 @@ class HINodePageView extends GetView { Column( crossAxisAlignment: CrossAxisAlignment.start, 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: EdgeInsets.only(left: 100.w, right: 60.w), child: Row( @@ -159,7 +201,12 @@ class HINodePageView extends GetView { }) { return Expanded( child: GestureDetector( - onTap: onTap, + onTap: () { + // 处理模式按钮点击(用于调试模式激活) + controller.kr_handleModeButtonClick(); + // 执行原有的点击逻辑 + onTap(); + }, child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: EdgeInsets.symmetric(vertical: 4.w), diff --git a/lib/app/modules/kr_home/controllers/kr_home_controller.dart b/lib/app/modules/kr_home/controllers/kr_home_controller.dart index f2eaed5..b1d539c 100755 --- a/lib/app/modules/kr_home/controllers/kr_home_controller.dart +++ b/lib/app/modules/kr_home/controllers/kr_home_controller.dart @@ -18,6 +18,7 @@ import '../../../common/app_config.dart'; import '../../../localization/app_translations.dart'; import '../../../localization/kr_language_utils.dart'; import '../../../model/business/kr_group_outbound_list.dart'; +import '../../../model/business/kr_outbound_item.dart'; import '../../../services/kr_announcement_service.dart'; import '../../../utils/kr_event_bus.dart'; import '../../../utils/kr_update_util.dart'; @@ -1421,8 +1422,8 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { /// 获取真实连接的节点信息(auto 模式下获取实际连接的节点) Map kr_getRealConnectedNodeInfo() { - // 如果不是 auto 模式,直接返回当前选中的节点信息 - if (kr_cutTag.value != 'auto') { + // 如果不是 auto 模式,也不是 country-auto 模式,直接返回当前选中的节点信息 + if (kr_cutTag.value != 'auto' && !kr_cutTag.value.endsWith('-auto')) { final node = kr_subscribeService.keyList[kr_cutSeletedTag.value]; return { '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}'); for (var group in KRSingBoxImp.instance.kr_activeGroups) { 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 node = kr_subscribeService.keyList[selectedNode]; return { @@ -1444,12 +1447,39 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { '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}', ); + // 处理 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 组,返回默认值 return { - 'nodeName': 'auto', + 'nodeName': kr_cutTag.value, 'delay': -2, 'country': '', }; @@ -1470,9 +1500,10 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { /// 获取真实连接的节点国家 String kr_getRealConnectedNodeCountry() { final info = kr_getRealConnectedNodeInfo(); - final delay = kr_currentNodeLatency.value; + final delay = info['delay'] as int; // 使用真实连接的节点延迟,而不是 kr_currentNodeLatency.value final country1 = kr_getCurrentNodeCountry(); print('country----$country1'); + print('kr_getRealConnectedNodeCountry - delay from info: $delay, country from info: ${info['country']}'); final country = kr_getCountryFullName(info['country']); if (delay == -2) { return '--'; @@ -2181,6 +2212,225 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { }); } + /// 获取国家-auto组的当前选中子节点信息 + Map? 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? _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? _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? 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> kr_getCountryRealNodeDelays(String countryCode) { + final delays = >[]; + + 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() { 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 { for (var item in group.items) { diff --git a/lib/app/services/singbox_imp/kr_sing_box_imp.dart b/lib/app/services/singbox_imp/kr_sing_box_imp.dart index 94cad42..bdfe69e 100755 --- a/lib/app/services/singbox_imp/kr_sing_box_imp.dart +++ b/lib/app/services/singbox_imp/kr_sing_box_imp.dart @@ -1025,10 +1025,25 @@ class KRSingBoxImp { KRLogUtil.kr_i('💾 开始保存配置文件...', tag: 'SingBox'); KRLogUtil.kr_i('📊 出站节点数量: ${outbounds.length}', tag: 'SingBox'); + // 分离真实节点和自动选择节点 + final realNodes = >[]; + final autoNodes = >[]; + + 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++) { - final outbound = outbounds[i]; - KRLogUtil.kr_i('📋 节点[$i] 配置:', tag: 'SingBox'); + for (int i = 0; i < realNodes.length; i++) { + final outbound = realNodes[i]; + KRLogUtil.kr_i('📋 真实节点[$i] 配置:', tag: 'SingBox'); KRLogUtil.kr_i(' - type: ${outbound['type']}', tag: 'SingBox'); KRLogUtil.kr_i(' - tag: ${outbound['tag']}', tag: 'SingBox'); KRLogUtil.kr_i(' - server: ${outbound['server']}', tag: 'SingBox'); @@ -1042,11 +1057,20 @@ class KRSingBoxImp { if (outbound['uuid'] != null) { 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 崩溃 - kr_outbounds = outbounds.where((outbound) { + final filteredRealNodes = realNodes.where((outbound) { final type = outbound['type']; if (type == 'hysteria2' || type == 'hysteria') { KRLogUtil.kr_w('⚠️ 跳过 Hysteria2 节点: ${outbound['tag']} (libcore bug)', tag: 'SingBox'); @@ -1055,11 +1079,30 @@ class KRSingBoxImp { return true; }).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; + // 检查是否包含 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 = []; + allAvailableTags.addAll(filteredAutoNodes.map((o) => o['tag'] as String)); + allAvailableTags.addAll(filteredRealNodes.map((o) => o['tag'] as String)); // 🔧 修复:生成完整的 SingBox 配置 - // 之前只保存 {"outbounds": [...]}, 导致 libcore 无法正确处理 - // 现在保存完整配置,包含所有必需字段 + // 现在保存完整配置,包含所有必需字段,并且 outbounds 顺序为:selector→自动组→真实节点→direct/block/dns-out final Map fullConfig = { "log": { "level": "debug", @@ -1096,14 +1139,17 @@ class KRSingBoxImp { } ], "outbounds": [ - // 🔧 修复:添加 selector 组,让用户可以手动选择节点 + // 🔧 修复:添加 selector 组,包含真实和自动节点标签 { "type": "selector", "tag": "proxy", - "outbounds": kr_outbounds.map((o) => o['tag'] as String).toList(), - "default": kr_outbounds.isNotEmpty ? kr_outbounds[0]['tag'] : "direct", + "outbounds": allAvailableTags, + "default": allAvailableTags.isNotEmpty ? allAvailableTags[0] : "direct", }, - ...kr_outbounds, + // 🔧 修复:自动选择组排在真实节点前面 + ...filteredAutoNodes, + // 🔧 修复:真实节点排在后面 + ...filteredRealNodes, { "type": "direct", "tag": "direct" @@ -1135,7 +1181,8 @@ class KRSingBoxImp { final mapStr = jsonEncode(fullConfig); 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'); await file.writeAsString(mapStr);