diff --git a/lib/app/model/business/kr_outbound_item.dart b/lib/app/model/business/kr_outbound_item.dart index 30db256..fa00045 100755 --- a/lib/app/model/business/kr_outbound_item.dart +++ b/lib/app/model/business/kr_outbound_item.dart @@ -243,6 +243,111 @@ class KROutboundItem { print(' - port: ${nodeListItem.port}'); print(' - uuid: ${nodeListItem.uuid}'); print(' - method: ${nodeListItem.method}'); + print(' - config: ${nodeListItem.config}'); + print(' - protocols: ${nodeListItem.protocols}'); + + // 🔧 尝试从 config 字段解析 transport 配置 + Map? transportConfig; + Map? securityConfig; + + // 🔧 关键修复:优先从 protocols 字段解析配置 + if (nodeListItem.protocols.isNotEmpty) { + try { + final protocolsList = jsonDecode(nodeListItem.protocols) as List; + print('📄 解析到 protocols 数组,共 ${protocolsList.length} 个协议'); + + // 查找匹配当前协议类型的配置 + Map? matchedProtocol; + for (var protocol in protocolsList) { + final protocolMap = protocol as Map; + final type = protocolMap['type']?.toString().toLowerCase() ?? ''; + final enable = protocolMap['enable'] ?? true; + + print(' 📋 协议: type=$type, enable=$enable'); + + // 匹配协议类型(注意 hysteria 和 hysteria2 都匹配 hysteria) + if (type == nodeListItem.protocol.toLowerCase() || + (nodeListItem.protocol == 'hysteria' && type == 'hysteria2') || + (nodeListItem.protocol == 'hysteria2' && type == 'hysteria')) { + matchedProtocol = protocolMap; + print(' ✅ 找到匹配的协议配置: $type'); + break; + } + } + + if (matchedProtocol != null) { + // 提取 transport 配置 + if (matchedProtocol['network'] != null || matchedProtocol['transport'] != null) { + final network = matchedProtocol['network'] ?? matchedProtocol['transport']; + print(' 📡 传输协议: $network'); + + if (network == 'ws' || network == 'websocket') { + transportConfig = { + 'type': 'ws', + 'path': matchedProtocol['ws_path'] ?? matchedProtocol['path'] ?? '/', + }; + final host = matchedProtocol['ws_host'] ?? matchedProtocol['host']; + if (host != null && host.toString().isNotEmpty) { + transportConfig['headers'] = {'Host': host.toString()}; + } + print(' ✅ WebSocket transport: $transportConfig'); + } else if (network == 'grpc') { + transportConfig = { + 'type': 'grpc', + 'service_name': matchedProtocol['grpc_service_name'] ?? matchedProtocol['service_name'] ?? '', + }; + print(' ✅ gRPC transport: $transportConfig'); + } else if (network == 'http' || network == 'h2') { + transportConfig = { + 'type': 'http', + 'host': [matchedProtocol['http_host'] ?? matchedProtocol['host'] ?? ''], + 'path': matchedProtocol['http_path'] ?? matchedProtocol['path'] ?? '/', + }; + print(' ✅ HTTP transport: $transportConfig'); + } + } + + // 提取 security 配置 + if (matchedProtocol['tls'] != null || matchedProtocol['security'] != null) { + // 🔧 关键修复:读取 security 字段判断是否启用 TLS + final security = matchedProtocol['security']?.toString().toLowerCase() ?? ''; + final tlsEnabled = security == 'tls' || security == 'reality'; + + securityConfig = { + 'tls_enabled': tlsEnabled, // ← 新增:记录 TLS 是否启用 + 'sni': matchedProtocol['sni'] ?? matchedProtocol['server_name'], + 'allow_insecure': matchedProtocol['allow_insecure'] ?? matchedProtocol['insecure'] ?? true, + 'fingerprint': matchedProtocol['fingerprint'] ?? 'chrome', + }; + print(' ✅ Security config: security=$security, tls_enabled=$tlsEnabled, config=$securityConfig'); + } + } + } catch (e) { + print('⚠️ 解析 protocols 字段失败: $e'); + } + } + + // 兜底:尝试从 config 字段解析(旧API格式) + if (transportConfig == null && nodeListItem.config.isNotEmpty) { + try { + final configJson = jsonDecode(nodeListItem.config) as Map; + print('📄 解析到 config JSON: $configJson'); + + // 提取 transport 配置 + if (configJson['transport'] != null && configJson['transport'] != 'tcp') { + transportConfig = _buildTransport(configJson); + print('✅ 从 config 找到 transport 配置: $transportConfig'); + } + + // 提取 security_config + if (configJson['security_config'] != null) { + securityConfig = configJson['security_config'] as Map; + print('✅ 从 config 找到 security_config: $securityConfig'); + } + } catch (e) { + print('⚠️ 解析 config 字段失败: $e'); + } + } switch (nodeListItem.protocol) { case "shadowsocks": @@ -268,19 +373,30 @@ class KROutboundItem { final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$') .hasMatch(nodeListItem.serverAddr); + // 🔧 优先使用 security_config 中的 SNI + String serverName = nodeListItem.serverAddr; + if (securityConfig != null && securityConfig['sni'] != null && securityConfig['sni'].toString().isNotEmpty) { + serverName = securityConfig['sni'].toString(); + } + + // 🔧 关键修复:根据 security_config 判断是否启用 TLS + final bool vlessTlsEnabled = securityConfig?['tls_enabled'] ?? false; + print('🔐 VLESS TLS 状态: enabled=$vlessTlsEnabled'); + config = { "type": "vless", "tag": nodeListItem.name, "server": nodeListItem.serverAddr, "server_port": nodeListItem.port, "uuid": nodeListItem.uuid, - "tls": { + if (transportConfig != null) "transport": transportConfig, + if (vlessTlsEnabled) "tls": { "enabled": true, - if (isDomain) "server_name": nodeListItem.serverAddr, - "insecure": true, + if (isDomain) "server_name": serverName, + "insecure": securityConfig?['allow_insecure'] ?? true, "utls": { "enabled": true, - "fingerprint": "chrome" + "fingerprint": securityConfig?['fingerprint'] ?? "chrome" } } }; @@ -292,6 +408,16 @@ class KROutboundItem { final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$') .hasMatch(nodeListItem.serverAddr); + // 🔧 优先使用 security_config 中的 SNI + String serverName = nodeListItem.serverAddr; + if (securityConfig != null && securityConfig['sni'] != null && securityConfig['sni'].toString().isNotEmpty) { + serverName = securityConfig['sni'].toString(); + } + + // 🔧 关键修复:根据 security_config 判断是否启用 TLS + final bool tlsEnabled = securityConfig?['tls_enabled'] ?? false; + print('🔐 TLS 状态: enabled=$tlsEnabled'); + config = { "type": "vmess", "tag": nodeListItem.name, @@ -300,11 +426,15 @@ class KROutboundItem { "uuid": nodeListItem.uuid, "alter_id": 0, "security": "auto", - "tls": { + if (transportConfig != null) "transport": transportConfig, + if (tlsEnabled) "tls": { "enabled": true, - if (isDomain) "server_name": nodeListItem.serverAddr, - "insecure": true, - "utls": {"enabled": true, "fingerprint": "chrome"} + if (isDomain) "server_name": serverName, + "insecure": securityConfig?['allow_insecure'] ?? true, + "utls": { + "enabled": true, + "fingerprint": securityConfig?['fingerprint'] ?? "chrome" + } } }; print('✅ VMess 节点配置构建成功: ${nodeListItem.name}'); @@ -315,17 +445,27 @@ class KROutboundItem { final bool isDomain = !RegExp(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$') .hasMatch(nodeListItem.serverAddr); + // 🔧 优先使用 security_config 中的 SNI + String serverName = nodeListItem.serverAddr; + if (securityConfig != null && securityConfig['sni'] != null && securityConfig['sni'].toString().isNotEmpty) { + serverName = securityConfig['sni'].toString(); + } + config = { "type": "trojan", "tag": nodeListItem.name, "server": nodeListItem.serverAddr, "server_port": nodeListItem.port, "password": nodeListItem.uuid, + if (transportConfig != null) "transport": transportConfig, "tls": { "enabled": true, - if (isDomain) "server_name": nodeListItem.serverAddr, - "insecure": true, - "utls": {"enabled": true, "fingerprint": "chrome"} + if (isDomain) "server_name": serverName, + "insecure": securityConfig?['allow_insecure'] ?? true, + "utls": { + "enabled": true, + "fingerprint": securityConfig?['fingerprint'] ?? "chrome" + } } }; print('✅ Trojan 节点配置构建成功: ${nodeListItem.name}'); 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 dd042a3..d814aa6 100755 --- a/lib/app/modules/kr_home/controllers/kr_home_controller.dart +++ b/lib/app/modules/kr_home/controllers/kr_home_controller.dart @@ -525,6 +525,8 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { break; case SingboxStarted(): KRLogUtil.kr_i('🟢 状态: 已启动', tag: 'HomeController'); + print('🔵 状态变为 Started, 当前延迟=${kr_currentNodeLatency.value}'); + // 取消连接超时处理 _cancelConnectionTimeout(); kr_connectText.value = AppTranslations.kr_home.connected; @@ -533,6 +535,14 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { kr_isLatency.value = false; kr_isConnected.value = true; + // 🔧 关键修复:如果延迟还是-1(连接中状态),立即设置为0(已连接但延迟未知) + if (kr_currentNodeLatency.value == -1) { + print('🔵 强制将延迟从 -1 更新为 0'); + kr_currentNodeLatency.value = 0; + kr_currentNodeLatency.refresh(); // 强制刷新延迟值 + print('🔵 延迟值已刷新'); + } + // 🔧 修复:立即尝试更新延迟值 _kr_updateLatencyOnConnected(); @@ -540,6 +550,7 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { kr_isConnected.refresh(); // 强制更新UI update(); + print('🔵 状态更新完成,当前延迟=${kr_currentNodeLatency.value}'); break; case SingboxStopping(): KRLogUtil.kr_i('🟠 状态: 正在停止', tag: 'HomeController'); @@ -1110,9 +1121,8 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { } // 选择节点 - void kr_selectNode(String tag) { + void kr_selectNode(String tag) async { try { - kr_currentNodeLatency.value = -1; kr_cutTag.value = tag; kr_currentNodeName.value = tag; @@ -1124,7 +1134,18 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { // 🔧 修复:只有在核心已启动时才选择节点,避免触发重启 if (KRSingBoxImp.instance.kr_status.value == SingboxStarted()) { - KRSingBoxImp.instance.kr_selectOutbound(tag); + print('🔵 节点已选择且VPN正在运行,切换到: $tag'); + // 🔧 关键修复:切换节点时设置为-1(切换中) + kr_currentNodeLatency.value = -1; + + // 🔧 关键修复:使用 await 等待节点切换完成 + try { + await KRSingBoxImp.instance.kr_selectOutbound(tag); + print('🔵 节点切换命令已执行完成: $tag'); + } catch (e) { + KRLogUtil.kr_e('❌ 节点切换失败: $e', tag: 'HomeController'); + print('🔵 节点切换失败: $e'); + } // 🔧 修复:选择节点后启动延迟值更新(带超时保护) Future.delayed(const Duration(milliseconds: 500), () { @@ -1132,10 +1153,22 @@ class KRHomeController extends GetxController with WidgetsBindingObserver { KRLogUtil.kr_w('⚠️ 选择节点后延迟值未更新,尝试手动更新', tag: 'HomeController'); if (!_kr_tryUpdateDelayFromActiveGroups()) { kr_currentNodeLatency.value = 0; + kr_currentNodeLatency.refresh(); } } }); } else { + // 🔧 关键修复:核心未启动时,根据连接状态设置延迟 + print('🔵 节点已选择但VPN未运行: $tag, 当前状态=${KRSingBoxImp.instance.kr_status.value}'); + if (kr_isConnected.value) { + // 如果显示已连接但实际未运行,重置状态 + kr_currentNodeLatency.value = -2; + } else { + // 正常未连接状态 + kr_currentNodeLatency.value = -2; + } + kr_currentNodeLatency.refresh(); + // 🔧 修复:核心未启动时,仍需保存用户选择,以便启动VPN时应用 KRLogUtil.kr_i('💾 核心未启动,保存节点选择以便稍后应用: $tag', tag: 'HomeController'); KRSecureStorage().kr_saveData(key: 'SELECTED_NODE_TAG', value: tag).then((_) { diff --git a/lib/app/modules/kr_home/views/kr_home_connection_info_view.dart b/lib/app/modules/kr_home/views/kr_home_connection_info_view.dart index b9d19e7..9db87c9 100755 --- a/lib/app/modules/kr_home/views/kr_home_connection_info_view.dart +++ b/lib/app/modules/kr_home/views/kr_home_connection_info_view.dart @@ -99,6 +99,7 @@ class KRHomeConnectionInfoView extends GetView { children: [ Obx(() { final delay = controller.kr_currentNodeLatency.value; + print('🔵 UI延迟显示更新: delay=$delay'); // 获取延迟颜色 Color getLatencyColor(int delay) { 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 cb23221..ee429d6 100755 --- a/lib/app/services/singbox_imp/kr_sing_box_imp.dart +++ b/lib/app/services/singbox_imp/kr_sing_box_imp.dart @@ -1081,6 +1081,34 @@ class KRSingBoxImp { } }); } + + // 🔧 关键修复:恢复用户选择的节点 + try { + final selectedNode = await KRSecureStorage().kr_readData(key: _keySelectedNode); + if (selectedNode != null && selectedNode.isNotEmpty) { + KRLogUtil.kr_i('🎯 恢复用户选择的节点: $selectedNode', tag: 'SingBox'); + print('🔵 启动后恢复节点选择: $selectedNode'); + + // 延迟500ms确保sing-box完全启动 + await Future.delayed(const Duration(milliseconds: 500)); + + // 🔧 关键修复:使用 await 等待节点切换完成 + try { + await kr_selectOutbound(selectedNode); + KRLogUtil.kr_i('✅ 节点已切换到用户选择: $selectedNode', tag: 'SingBox'); + print('🔵 节点切换成功: $selectedNode'); + } catch (e) { + KRLogUtil.kr_e('❌ 节点切换失败: $e', tag: 'SingBox'); + print('🔵 节点切换失败: $e'); + } + } else { + KRLogUtil.kr_i('ℹ️ 没有保存的节点选择,使用默认配置', tag: 'SingBox'); + print('🔵 没有保存的节点选择,使用默认'); + } + } catch (e) { + KRLogUtil.kr_e('❌ 恢复节点选择失败: $e', tag: 'SingBox'); + print('🔵 恢复节点选择失败: $e'); + } }); } catch (e, stackTrace) { KRLogUtil.kr_e('💥 SingBox 启动异常: $e', tag: 'SingBox'); @@ -1267,29 +1295,30 @@ class KRSingBoxImp { // 节点选择监控定时器 Timer? _nodeSelectionTimer; - void kr_selectOutbound(String tag) async { + Future kr_selectOutbound(String tag) async { KRLogUtil.kr_i('🎯 [v2.1] 开始选择出站节点: $tag', tag: 'SingBox'); KRLogUtil.kr_i('📊 当前活动组数量: ${kr_activeGroups.length}', tag: 'SingBox'); - // 持久化用户选择的节点(异步执行,不阻塞UI) - KRSecureStorage().kr_saveData(key: _keySelectedNode, value: tag).then((_) { + // 🔧 关键修复:使用 await 确保保存完成 + try { + await KRSecureStorage().kr_saveData(key: _keySelectedNode, value: tag); KRLogUtil.kr_i('✅ 节点选择已保存: $tag', tag: 'SingBox'); - }).catchError((e) { + } catch (e) { KRLogUtil.kr_e('❌ 保存节点选择失败: $e', tag: 'SingBox'); - }); + } - // ⚠️ 关键修复:确保 command client 已初始化 - // 借鉴 Hiddify:通过触发 watchGroups() 来初始化 command client - _kr_ensureCommandClientInitialized().then((_) { + // 🔧 关键修复:使用 await 确保 command client 初始化完成 + try { + await _kr_ensureCommandClientInitialized(); KRLogUtil.kr_i('✅ Command client 已就绪,执行节点切换', tag: 'SingBox'); - // 🔄 使用重试机制执行节点选择(异步执行,不阻塞UI) - _kr_selectOutboundWithRetry("select", tag, maxAttempts: 3, initialDelay: 50).catchError((e) { - KRLogUtil.kr_e('❌ 节点选择失败: $e', tag: 'SingBox'); - }); - }).catchError((e) { - KRLogUtil.kr_e('❌ Command client 初始化失败,节点切换取消: $e', tag: 'SingBox'); - }); + // 🔧 关键修复:使用 await 等待节点选择完成 + await _kr_selectOutboundWithRetry("select", tag, maxAttempts: 3, initialDelay: 50); + KRLogUtil.kr_i('✅ 节点切换完成: $tag', tag: 'SingBox'); + } catch (e) { + KRLogUtil.kr_e('❌ 节点选择失败: $e', tag: 'SingBox'); + rethrow; // 抛出异常,让调用者知道失败了 + } // 🔄 如果用户选择了具体节点(不是 auto),启动定期检查和重新选择 // 这是为了防止 urltest 自动覆盖用户的手动选择