From 02533577a358c482f1ceb9bc80f4566b1c690561 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Fri, 20 Mar 2026 17:23:35 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E7=A1=AE=E5=AE=9A=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E7=BB=91=E5=AE=9A=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/models/user_Info.dart | 4 + lib/data/services/ble_manager.dart | 19 ++-- .../home/controllers/home_controller.dart | 5 +- .../controllers/mobile_elf_controller.dart | 50 +++++++- .../mobile_elf/views/mobile_elf_view.dart | 19 +++- .../controllers/pairing_controller.dart | 107 +++++++++++++++--- .../agent_service/AgentService.kt | 10 ++ .../agent_service/AgentServicePlugin.kt | 20 ++++ .../yunqiinnovation/agent_service/StsAgent.kt | 81 ++++++++++++- .../agent_service/lib/agent_service.dart | 29 +++++ 10 files changed, 311 insertions(+), 33 deletions(-) diff --git a/lib/data/models/user_Info.dart b/lib/data/models/user_Info.dart index 87e2cfbce..2467ba7bd 100644 --- a/lib/data/models/user_Info.dart +++ b/lib/data/models/user_Info.dart @@ -212,6 +212,7 @@ class User { class UserDevice { int id; String uid; + int productid; int devicetype; String devicename; String devicemac; @@ -219,6 +220,7 @@ class UserDevice { UserDevice({ required this.id, required this.uid, + required this.productid, required this.devicetype, required this.devicename, required this.devicemac, @@ -228,6 +230,7 @@ class UserDevice { factory UserDevice.fromJson(Map json) => UserDevice( id: json['id'] as int, uid: json['uid'] as String, + productid: (json['productid'] as num).toInt(), devicetype: (json['devicetype'] as num).toInt(), devicename: json['devicename'] as String, devicemac: json['devicemac'] as String, @@ -237,6 +240,7 @@ class UserDevice { toJson() => { 'id': id, 'uid': uid, + 'productid': productid, 'devicetype': devicetype, 'devicename': devicename, 'devicemac': devicemac, diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index ddf05eedc..7327929b6 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:get_storage/get_storage.dart'; import 'package:get/get.dart'; import 'package:ble_service/ble_service.dart'; @@ -587,23 +586,17 @@ class BleManager extends GetxService { } } - /// 更新设备id + /// 更新设备id,并检测是否为移动精灵设备 void _updateProductIdFromData(Map data) { try { - // 确保数据中包含版本信息 if (!data.containsKey('productId')) { Logger.warning('版本信息数据不完整'); return; } - // 提取版本信息 final String productId = data['productId'] as String? ?? ''; - // 更新设备信息 - _updateDeviceInfoIfNeeded( - firmwareProductId: productId, - ); - + _updateDeviceInfoIfNeeded(firmwareProductId: productId); Logger.info('设备设备id已更新: 设备id=$productId'); } catch (e) { Logger.error('更新设备id失败: ${e.toString()}'); @@ -959,14 +952,18 @@ class BleManager extends GetxService { } // STS 模式参数(移动精灵) - final agentType = AppConfig.env('AGENT_TYPE') ?? 'yidong'; + // 优先使用设备检测结果(YIDONG_PID 匹配时写入),否则用服务端 AGENT_TYPE + final agentType = _storage.read('device_agent_type') + ?? AppConfig.env('AGENT_TYPE') + ?? 'stt_llm_tts'; final agentMode = (agentType == 'yidong') ? 'sts' : 'stt_llm_tts'; final agentId = (agentType == 'yidong') ? (AppConfig.env('MOBILE_ELF_AGENT_ID') ?? 'mobile_elf') : 'default_agent'; final stsProductId = AppConfig.env('MOBILE_ELF_PRODUCT_ID') ?? ''; final stsProductKey = AppConfig.env('MOBILE_ELF_PRODUCT_KEY') ?? ''; - final stsDeviceNo = dotenv.env['MOBILE_ELF_DEVICE_NO'] ?? ''; + // 使用当前连接设备的 SN(配对时写入),认证失败由原生层上报 credential_failed 事件 + final stsDeviceNo = _storage.read('device_sts_code') ?? ''; final stsDeviceId = _storage.read('sts_device_id') ?? ''; final stsDeviceSecret = _storage.read('sts_device_secret') ?? ''; diff --git a/lib/modules/home/controllers/home_controller.dart b/lib/modules/home/controllers/home_controller.dart index 7df97989b..55d431d59 100644 --- a/lib/modules/home/controllers/home_controller.dart +++ b/lib/modules/home/controllers/home_controller.dart @@ -256,7 +256,10 @@ class HomeController extends GetxController { } void exploreAIFeatures() { - final agentType = AppConfig.env('AGENT_TYPE') ?? 'yidong'; + // 优先读设备检测写入的 storage(YIDONG_PID 匹配时),否则降级到服务端 AGENT_TYPE + final agentType = GetStorage().read('device_agent_type') + ?? AppConfig.env('AGENT_TYPE') + ?? 'stt_llm_tts'; if (agentType == 'yidong') { Get.toNamed(Routes.mobileElf); } else { diff --git a/lib/modules/mobile_elf/controllers/mobile_elf_controller.dart b/lib/modules/mobile_elf/controllers/mobile_elf_controller.dart index 6393fc975..c39399405 100644 --- a/lib/modules/mobile_elf/controllers/mobile_elf_controller.dart +++ b/lib/modules/mobile_elf/controllers/mobile_elf_controller.dart @@ -3,6 +3,7 @@ import 'package:agent_service/agent_service.dart'; import 'package:chat_storage/chat_storage.dart'; import 'package:flutter/widgets.dart'; import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; import 'package:permission_handler/permission_handler.dart'; import '../../../data/models/appconfig.dart'; import '../../../core/utils/logger.dart'; @@ -39,6 +40,9 @@ class MobileElfController extends GetxController { // 凭证状态(原生层负责,Dart 层始终无错) final credentialReady = true.obs; final credentialError = ''.obs; + // 设备认证失败(STS credential_failed 事件触发) + final credentialFailed = false.obs; + final credentialFailedReason = ''.obs; // ── 内部 ─────────────────────────────────────────────────────────────────── StreamSubscription? _eventSub; @@ -46,6 +50,9 @@ class MobileElfController extends GetxController { Timer? _pttTimer; bool _inCallMode = false; bool _callStarting = false; // 防止并发 startCall + // PTT 松开后仍有异步事件待处理(recognition_result / assistant_response / tts) + bool _pttResultPending = false; + final _storage = GetStorage(); // ═══════════════════════════════════════════════════════════════════════════ // 生命周期 @@ -62,6 +69,8 @@ class MobileElfController extends GetxController { ever(ttsEnabled, (bool enabled) { AgentService.setTtsMuted(!enabled); }); + // 主动触发认证检查(页面打开时,确保 credential_failed 事件不被错过) + AgentService.fetchCredentials(); } Future _loadChatHistory() async { @@ -107,8 +116,9 @@ class MobileElfController extends GetxController { void _onAgentEvent(AgentServiceEvent event) { Logger.info('$_tag event: ${event.type} data: ${event.data}'); - // 已挂断/停止时忽略原生层残留的状态事件 - final sessionActive = _inCallMode || pttRecording.value; + // PTT 松开后 pttRecording=false,但 fin_result/assistant_response/tts 仍异步到达 + // 用 _pttResultPending 保持会话激活,直到 ttsCompleted/error 才关闭 + final sessionActive = _inCallMode || pttRecording.value || _pttResultPending; switch (event.type) { case AgentServiceEventType.recognitionStarted: if (!sessionActive) break; @@ -157,6 +167,7 @@ class MobileElfController extends GetxController { case AgentServiceEventType.ttsCompleted: if (!sessionActive) break; + _pttResultPending = false; // PTT 一轮结束 if (_inCallMode) { status.value = MobileElfStatus.recording; micAlive.value = true; @@ -169,12 +180,14 @@ class MobileElfController extends GetxController { case AgentServiceEventType.ttsCanceled: case AgentServiceEventType.ttsStopped: if (!sessionActive) break; + _pttResultPending = false; // TTS 被打断也视为结束 status.value = _inCallMode ? MobileElfStatus.recording : MobileElfStatus.idle; micAlive.value = _inCallMode; break; case AgentServiceEventType.error: if (!sessionActive) break; + _pttResultPending = false; // 出错时结束等待 final msg = event.data['message']?.toString() ?? '未知错误'; status.value = MobileElfStatus.error; errorMsg.value = msg; @@ -186,6 +199,24 @@ class MobileElfController extends GetxController { micRms.value = rms; break; + case AgentServiceEventType.heartbeatReported: + // 记录心跳上报时间 + final t = (event.data['time'] as num?)?.toInt() ?? DateTime.now().millisecondsSinceEpoch; + _storage.write('sts_heartbeat_time', t); + Logger.info('$_tag 心跳上报成功,时间: $t'); + break; + + case AgentServiceEventType.credentialFailed: + final reason = event.data['reason']?.toString() ?? '设备未认证'; + Logger.error('$_tag 设备认证失败: $reason'); + credentialFailed.value = true; + credentialFailedReason.value = reason; + // 确保停止任何进行中的会话 + if (_inCallMode || pttRecording.value) { + disconnect(); + } + break; + default: break; } @@ -199,7 +230,11 @@ class MobileElfController extends GetxController { Future startCall() async { if (inCall.value || _callStarting) return; _callStarting = true; + _checkHeartbeat(); try { + // 先确保原生层完全停止(处理 onClose 未 await 导致的残留状态) + await AgentService.stopConversation(); + final granted = await PermissionUtil.instance.requestPermission( permissionType: Permission.microphone, permissionName: '麦克风', @@ -224,6 +259,7 @@ class MobileElfController extends GetxController { Future disconnect() async { _callStarting = false; _inCallMode = false; + _pttResultPending = false; // 强制结束所有等待 inCall.value = false; _callTimer?.cancel(); _callTimer = null; @@ -237,8 +273,17 @@ class MobileElfController extends GetxController { await AgentService.stopConversation(); } + /// 超过1小时未上报心跳则补报一次 + void _checkHeartbeat() { + final last = _storage.read('sts_heartbeat_time') ?? 0; + if (DateTime.now().millisecondsSinceEpoch - last > 3600000) { + AgentService.reportHeartbeat().catchError((_) {}); + } + } + Future startPtt() async { if (pttRecording.value) return; + _checkHeartbeat(); final granted = await PermissionUtil.instance.requestPermission( permissionType: Permission.microphone, permissionName: '麦克风', @@ -258,6 +303,7 @@ class MobileElfController extends GetxController { void stopPtt() { if (!pttRecording.value) return; pttRecording.value = false; + _pttResultPending = true; // 等待 fin_result/assistant_response/tts 异步回来 _pttTimer?.cancel(); _pttTimer = null; AgentService.stopConversation(); diff --git a/lib/modules/mobile_elf/views/mobile_elf_view.dart b/lib/modules/mobile_elf/views/mobile_elf_view.dart index da76aa701..dad19841f 100644 --- a/lib/modules/mobile_elf/views/mobile_elf_view.dart +++ b/lib/modules/mobile_elf/views/mobile_elf_view.dart @@ -143,6 +143,17 @@ class MobileElfView extends GetView { /// 右上角状态胶囊 + 通话计时 Widget _buildStatusPill() { + if (controller.credentialFailed.value) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 3.h), + decoration: BoxDecoration(color: const Color(0xFFFEF2F2), borderRadius: BorderRadius.circular(20.r)), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Icon(Icons.lock_outline_rounded, size: 11.sp, color: _red), + SizedBox(width: 4.w), + Text('认证失败', style: TextStyle(fontSize: 11.sp, color: _red, fontWeight: FontWeight.w500)), + ]), + ); + } final s = controller.status.value; final inCall = controller.inCall.value; final cfg = _pillConfig(s, inCall); @@ -444,6 +455,7 @@ class MobileElfView extends GetView { Widget _buildBottomBar(bool isDark) { return Obx(() { + final inCall = controller.inCall.value; final s = controller.status.value; final ptt = controller.pttRecording.value; @@ -621,9 +633,10 @@ class MobileElfView extends GetView { final s = controller.status.value; final ptt = controller.pttRecording.value; - final disabled = !ptt && s != MobileElfStatus.idle && s != MobileElfStatus.error; - final pttClickable = s == MobileElfStatus.speaking; - final sideDisabled = ptt || (disabled && !pttClickable); + final locked = controller.credentialFailed.value; + final disabled = locked || (!ptt && s != MobileElfStatus.idle && s != MobileElfStatus.error); + final pttClickable = !locked && s == MobileElfStatus.speaking; + final sideDisabled = locked || ptt || (disabled && !pttClickable); return Padding( padding: EdgeInsets.fromLTRB(12.w, 6.h, 12.w, 14.h), diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 2b63098c6..5156ea8b4 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -810,6 +810,7 @@ class PairingController extends GetxController with WidgetsBindingObserver { orElse: () => UserDevice( id: -1, uid: '', + productid: -1, devicetype: -1, devicename: '', devicemac: '', @@ -840,15 +841,21 @@ class PairingController extends GetxController with WidgetsBindingObserver { orElse: () => UserDevice( id: -1, uid: '', + productid: -1, devicetype: -1, devicename: '', devicemac: '', license: ''), ); if (existingDevice.id != -1) { + _verifiedDevicePid = existingDevice.productid; _verifiedDeviceName = device.name; _verifiedDeviceAddress = device.address; - // 设备时ble 蓝牙 且已绑定 + // 已绑定设备直接用 license 作为 deviceCode(即绑定时扫到的 SN) + if (existingDevice.license.isNotEmpty) { + deviceCode.value = existingDevice.license; + Logger.i(_tag, '[YIDONG] 已绑定设备,从license提取 deviceCode=${existingDevice.license}'); + } return 10; } } @@ -885,14 +892,15 @@ class PairingController extends GetxController with WidgetsBindingObserver { code = extractAndReverseBytes(hexData, 13, 22); _verifiedDevicePid = extractAndReverseBytesToInt(hexData, 4, 5); - print('找到匹配设备,获取到 code: $code'); + final pidHexIos = _verifiedDevicePid?.toRadixString(16).toUpperCase() ?? ''; + Logger.i(_tag, '[YIDONG] iOS BLE扫描 MAC匹配 device=${device.name} pidHex=$pidHexIos code=$code'); if (code != "") { _bleManager.stopScan(); deviceCode.value = code; String? uuids = deviceInfo["id"]; _verifiedDeviceName = device.name; _verifiedDeviceAddress = uuids; - print('找到匹配设备,获取到 uuids: $uuids'); + Logger.i(_tag, '[YIDONG] iOS deviceCode提取成功 code=$code uuid=$uuids'); //检查是否已经绑定 final existingDevice = User.instance.devices.firstWhere( (d) => @@ -902,6 +910,7 @@ class PairingController extends GetxController with WidgetsBindingObserver { orElse: () => UserDevice( id: -1, uid: '', + productid: -1, devicetype: -1, devicename: '', devicemac: '', @@ -921,12 +930,14 @@ class PairingController extends GetxController with WidgetsBindingObserver { code = extractAndReverseBytes(hexData, 15, 24); _verifiedDevicePid = extractAndReverseBytesToInt(hexData, 6, 7); - print('找到匹配设备,获取到 code: $code'); + final pidHexAndroid = _verifiedDevicePid?.toRadixString(16).toUpperCase() ?? ''; + Logger.i(_tag, '[YIDONG] Android BLE扫描 MAC匹配 device=${device.name} mac=${device.address} pidHex=$pidHexAndroid code=$code'); if (code != "") { _bleManager.stopScan(); deviceCode.value = code; _verifiedDeviceName = device.name; _verifiedDeviceAddress = device.address; + Logger.i(_tag, '[YIDONG] Android deviceCode提取成功 code=$code mac=${device.address}'); //检查是否已经绑定 final existingDevice = User.instance.devices.firstWhere( (d) => @@ -936,6 +947,7 @@ class PairingController extends GetxController with WidgetsBindingObserver { orElse: () => UserDevice( id: -1, uid: '', + productid: -1, devicetype: -1, devicename: '', devicemac: '', @@ -1178,26 +1190,92 @@ class PairingController extends GetxController with WidgetsBindingObserver { } } + /// 检测是否为移动精灵(YIDONG)设备,并写入 GetStorage + void _detectAndSaveYidongDevice() { + final yidongPid = AppConfig.env('YIDONG_PID') ?? ''; + if (yidongPid.isEmpty) { + Logger.i(_tag, '[YIDONG] 未配置 YIDONG_PID,跳过检测'); + return; + } + + // 优先使用本次扫描解析的 pid,否则从已绑定设备列表中查找 + int? pid = _verifiedDevicePid; + String pidSource = 'BLE扫描'; + if (pid == null) { + final mac = _verifiedDeviceAddress ?? _currentClassicDevice?.address ?? ''; + Logger.i(_tag, '[YIDONG] 本次未扫描到pid,从已绑定设备查找 mac=$mac'); + if (mac.isNotEmpty) { + final matched = User.instance.devices.firstWhere( + (d) => + d.devicemac.toUpperCase().replaceAll(':', '') == + mac.toUpperCase().replaceAll(':', ''), + orElse: () => UserDevice( + id: -1, uid: '', productid: -1, + devicetype: -1, devicename: '', devicemac: '', license: ''), + ); + if (matched.id != -1) { + pid = matched.productid; + pidSource = '已绑定设备'; + } + } + } + + if (pid == null) { + Logger.i(_tag, '[YIDONG] 无法获取设备pid,跳过检测'); + return; + } + + // 将 int pid 转为十六进制字符串(不补零,大写)与 YIDONG_PID 比对 + final pidHex = pid.toRadixString(16).toUpperCase(); + final isYidong = pidHex == yidongPid.toUpperCase(); + + Logger.i(_tag, '[YIDONG] 设备检测: pidHex=$pidHex yidongPid=${yidongPid.toUpperCase()} source=$pidSource matched=$isYidong'); + + if (isYidong) { + _storage.write('device_agent_type', 'yidong'); + // deviceCode 来源: + // 新设备 → BLE 扫描从广播包提取 + // 已绑定设备 → authbindingDevice 从 existingDevice.license 赋值 + if (deviceCode.value.isNotEmpty) { + final prevCode = _storage.read('device_sts_code') ?? ''; + // SN 变了说明换了设备,清除旧凭证,强制重新向移动平台认证 + if (prevCode != deviceCode.value) { + _storage.remove('sts_device_id'); + _storage.remove('sts_device_secret'); + Logger.i(_tag, '[YIDONG] SN 变更($prevCode → ${deviceCode.value}),已清除旧凭证,强制重新认证'); + } + _storage.write('device_sts_code', deviceCode.value); + Logger.i(_tag, '[YIDONG] 识别成功,写入 device_sts_code=${deviceCode.value}'); + } else { + // 既无扫描结果也无 license,清除所有凭证,认证失败会提示用户 + _storage.remove('device_sts_code'); + _storage.remove('sts_device_id'); + _storage.remove('sts_device_secret'); + Logger.i(_tag, '[YIDONG] deviceCode 为空,已清除所有凭证'); + } + } else { + _storage.remove('device_agent_type'); + _storage.remove('device_sts_code'); + _storage.remove('sts_device_id'); + _storage.remove('sts_device_secret'); + Logger.i(_tag, '[YIDONG] 非移动精灵设备,已清除相关存储'); + } + } + /// 导航到主页 void _navigateToHome() { - // Future.delayed(const Duration(milliseconds: 500), () async { + // 检测是否为移动精灵(YIDONG)设备 + _detectAndSaveYidongDevice(); // 启动AgentService服务 try { Logger.i(_tag, '启动AgentService服务'); _bleManager.startAgentService(); - // _bleManager.registerBackgroundScan(macAddress: _verifiedDeviceAddress); } catch (e) { Logger.e(_tag, '启动AgentService失败: $e'); } - //print('lxm-上一个路由: ${Get.previousRoute.toString()}'); - // if (Get.previousRoute == Routes.home) { - // 如果是从home页过来的,则直接返回 - // Get.back(); - // } else { - // 否则,跳转到home页并清空之前的路由栈 + // 已在 Home 页时不重复导航,避免两个 HomeView 同时存在导致 Duplicate GlobalKey + if (Get.currentRoute == Routes.home) return; Get.offAllNamed(Routes.home); - // } - // }); } // ==================== 用户操作 ==================== @@ -1259,6 +1337,7 @@ class PairingController extends GetxController with WidgetsBindingObserver { UserDevice( id: 0, // 临时设备ID uid: User.instance.uid, + productid: _verifiedDevicePid ?? -1, devicetype: 1, devicemac: _currentClassicDevice?.address ?? '', devicename: _currentClassicDevice?.name ?? 'Unknown Device', diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 3a56af051..bbad03eaa 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -987,6 +987,16 @@ object AgentService : CoroutineScope { stsAgent?.ttsMuted = muted } + /** 手动触发心跳上报(Flutter 层调用) */ + fun reportHeartbeat() { + stsAgent?.reportHeartbeat() + } + + /** 主动触发认证检查(移动精灵页面打开时调用,确保认证事件被 Flutter 层接收) */ + fun fetchCredentials() { + stsAgent?.fetchCredentials() + } + fun interruptCurrentResponse() { if (agentMode == "sts") { stsAgent?.interruptSpeaking() diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt index e6a08f893..b513800fa 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt @@ -349,6 +349,26 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream } } + "reportHeartbeat" -> { + try { + AgentService.reportHeartbeat() + result.success(true) + } catch (e: Exception) { + Log.e(TAG, "心跳上报失败", e) + result.error("HEARTBEAT_ERROR", "心跳上报失败: ${e.message}", null) + } + } + + "fetchCredentials" -> { + try { + AgentService.fetchCredentials() + result.success(true) + } catch (e: Exception) { + Log.e(TAG, "触发认证检查失败", e) + result.error("FETCH_CREDENTIALS_ERROR", "触发认证检查失败: ${e.message}", null) + } + } + "setCanWakeUp" -> { try { val canWakeUp = call.argument("canWakeUp") diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/StsAgent.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/StsAgent.kt index 8454d3eab..111cd2006 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/StsAgent.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/StsAgent.kt @@ -58,6 +58,7 @@ class StsAgent( override val coroutineContext = Dispatchers.IO + job init { + Log.i(TAG, "[YIDONG] StsAgent初始化: productId=${config.productId} deviceNo=${config.deviceNo} deviceId=${config.deviceId} agentId=${config.agentId} hasSecret=${config.deviceSecret.isNotEmpty()}") fetchCredentials() } @@ -153,8 +154,11 @@ class StsAgent( fun fetchCredentials() { if (config.deviceSecret.isNotEmpty()) { credentialReady = true + Log.i(TAG, "[YIDONG] fetchCredentials: 凭证已存在,跳过请求 deviceNo=${config.deviceNo} deviceId=${config.deviceId}") + reportHeartbeat() // 凭证已有,直接上报心跳 return } + Log.i(TAG, "[YIDONG] fetchCredentials: 开始请求密钥 productId=${config.productId} deviceNo=${config.deviceNo}") launch { try { val body = JSONObject().apply { @@ -180,14 +184,82 @@ class StsAgent( ) credentialReady = config.deviceSecret.isNotEmpty() if (credentialReady) { + Log.i(TAG, "[YIDONG] fetchCredentials: 密钥获取成功 deviceId=${config.deviceId} deviceNo=${config.deviceNo}") onCredentialsFetched?.invoke(config.deviceId, config.deviceSecret, config.deviceNo) + reportHeartbeat() // 凭证获取成功后立即上报心跳 + } else { + Log.e(TAG, "[YIDONG] fetchCredentials: 响应成功但deviceSecret为空") + emit("credential_failed", mapOf("reason" to "设备未认证,deviceSecret为空")) } } else { - Log.e(TAG, "凭证获取失败: ${json.optString("message")}") + val msg = json.optString("message", "未知错误") + Log.e(TAG, "[YIDONG] fetchCredentials: 密钥获取失败 code=${json.optString("code")} msg=$msg") + emit("credential_failed", mapOf("reason" to "设备未认证:$msg")) } } } catch (e: Exception) { - Log.e(TAG, "凭证获取异常: ${e.message}") + Log.e(TAG, "[YIDONG] fetchCredentials: 请求异常 ${e.message}") + emit("credential_failed", mapOf("reason" to "设备认证请求失败:${e.message}")) + } + } + } + + /** 向中国移动平台上报心跳 */ + fun reportHeartbeat() { + if (!credentialReady) { + Log.w(TAG, "[YIDONG] reportHeartbeat: 凭证未就绪,跳过上报") + return + } + Log.i(TAG, "[YIDONG] reportHeartbeat: 开始上报 deviceId=${config.deviceId} deviceNo=${config.deviceNo}") + launch { + try { + val netType = try { + val cm = context.getSystemService(android.content.Context.CONNECTIVITY_SERVICE) + as android.net.ConnectivityManager + val net = cm.activeNetworkInfo + when (net?.type) { + android.net.ConnectivityManager.TYPE_WIFI -> "WiFi" + android.net.ConnectivityManager.TYPE_MOBILE -> "5G" + else -> "WiFi" + } + } catch (_: Exception) { "WiFi" } + + val body = JSONObject().apply { + put("deviceId", config.deviceId) + put("deviceSecret", config.deviceSecret) + put("productId", config.productId) + put("productKey", config.productKey) + put("params", JSONObject().apply { + put("innerIp", JSONArray().put("127.0.0.1")) + put("netSpeed", "0Mbps") + put("netType", netType) + put("platform", "Android${android.os.Build.VERSION.RELEASE}") + put("sdkVersion", "ai_http_1.0") + put("firmwareVersion", "1.0.0") + put("imei", "") + put("cmei", config.deviceNo) // 非蜂窝设备使用 SN 作为 cmei + put("mac", "") + }) + }.toString().toRequestBody("application/json".toMediaType()) + + val req = Request.Builder() + .url("https://ivs.chinamobiledevice.com:11443/v2/customer/device/report") + .post(body) + .build() + + okHttpClient.newCall(req).execute().use { resp -> + val json = JSONObject(resp.body!!.string()) + val code = json.optString("code") + val msg = json.optString("message") + if (code == "200") { + Log.i(TAG, "[YIDONG] reportHeartbeat: 上报成功 code=$code msg=$msg") + emit("heartbeat_reported", mapOf("time" to System.currentTimeMillis())) + } else { + Log.e(TAG, "[YIDONG] reportHeartbeat: 上报失败 code=$code msg=$msg") + } + } + } catch (e: Exception) { + Log.e(TAG, "[YIDONG] reportHeartbeat: 请求异常 ${e.message}") } } } @@ -202,6 +274,7 @@ class StsAgent( if (ttsPlaying) interruptTts() if (pttRecording) stopPtt(cancel = true) if (!credentialReady) { emitError("移动精灵凭证未就绪,请稍后重试"); return } + Log.i(TAG, "[YIDONG] startCall: 启动通话模式 deviceNo=${config.deviceNo} agentId=${config.agentId}") openMic() startMicWatchdog() inCall = true @@ -213,6 +286,7 @@ class StsAgent( if (pttRecording) return if (ttsPlaying) interruptTts() if (!credentialReady) { emitError("移动精灵凭证未就绪"); return } + Log.i(TAG, "[YIDONG] startPtt: 启动PTT模式 deviceNo=${config.deviceNo} agentId=${config.agentId}") pttRecording = true openMic() @@ -445,6 +519,7 @@ class StsAgent( "?deviceNo=${config.deviceNo}&deviceId=${config.deviceId}" + "&productId=${config.productId}&productKey=${config.productKey}" + "&sign=$sign&sn=$sn&ts=$ts" + Log.i(TAG, "[YIDONG] connectWs: 连接WSS deviceNo=${config.deviceNo} deviceId=${config.deviceId} sn=$sn") val req = Request.Builder().url(url) .header("Origin", "https://ivs.chinamobiledevice.com:11443") @@ -462,6 +537,7 @@ class StsAgent( try { val msg = JSONObject(text) if (msg.optString("type") == "ready") { + Log.i(TAG, "[YIDONG] connectWs: 收到ready,WSS连接成功") if (!resolved) { resolved = true; cont.resume(Unit) {} } } else { launch { handleMessage(msg) } @@ -472,6 +548,7 @@ class StsAgent( override fun onMessage(ws: WebSocket, bytes: okio.ByteString) {} override fun onFailure(ws: WebSocket, t: Throwable, response: Response?) { + Log.e(TAG, "[YIDONG] connectWs: WSS连接失败 ${t.message}") if (webSocket === ws) webSocket = null // 只清除自己的引用,避免覆盖新 WS if (!resolved) { resolved = true; cont.resumeWithException(t) } else launch { handleWsDone() } diff --git a/local_plugins/agent_service/lib/agent_service.dart b/local_plugins/agent_service/lib/agent_service.dart index 1cdec8d65..9478c66fb 100644 --- a/local_plugins/agent_service/lib/agent_service.dart +++ b/local_plugins/agent_service/lib/agent_service.dart @@ -77,6 +77,12 @@ enum AgentServiceEventType { /// 麦克风音量(RMS) micRms, + /// 心跳上报成功 + heartbeatReported, + + /// STS 设备认证失败 + credentialFailed, + /// 未知事件 unknown } @@ -186,6 +192,10 @@ class AgentService { return AgentServiceEventType.stsCredentials; case 'mic_rms': return AgentServiceEventType.micRms; + case 'heartbeat_reported': + return AgentServiceEventType.heartbeatReported; + case 'credential_failed': + return AgentServiceEventType.credentialFailed; default: return AgentServiceEventType.unknown; } @@ -468,6 +478,25 @@ class AgentService { } } + /// 手动触发心跳上报(移动精灵) + static Future reportHeartbeat() async { + try { + await _channel.invokeMethod('reportHeartbeat'); + } on PlatformException catch (e) { + throw AgentServiceException(e.code, e.message ?? '心跳上报失败', e.details); + } + } + + /// 主动触发认证检查(移动精灵页面打开时调用) + /// 若凭证不存在则重新请求,结果通过事件流返回(credentialFailed / heartbeatReported) + static Future fetchCredentials() async { + try { + await _channel.invokeMethod('fetchCredentials'); + } on PlatformException catch (_) { + // 忽略异常,认证结果通过事件流反馈 + } + } + /// 中断当前响应 /// /// 停止语音合成和AI流输出 From a51d56ebf80166613407b293282f12ab75a43055 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Fri, 20 Mar 2026 18:01:27 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E5=94=A4=E9=86=92=E4=B8=8D=E7=8E=B0?= =?UTF-8?q?=E5=AE=9E=E6=96=87=E6=9C=AC=20bug=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mobile_elf/controllers/mobile_elf_controller.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/modules/mobile_elf/controllers/mobile_elf_controller.dart b/lib/modules/mobile_elf/controllers/mobile_elf_controller.dart index c39399405..eb711cc44 100644 --- a/lib/modules/mobile_elf/controllers/mobile_elf_controller.dart +++ b/lib/modules/mobile_elf/controllers/mobile_elf_controller.dart @@ -52,6 +52,8 @@ class MobileElfController extends GetxController { bool _callStarting = false; // 防止并发 startCall // PTT 松开后仍有异步事件待处理(recognition_result / assistant_response / tts) bool _pttResultPending = false; + // BLE/AI 唤醒触发的会话(非通话、非PTT模式下的唤醒) + bool _bleWakeupActive = false; final _storage = GetStorage(); // ═══════════════════════════════════════════════════════════════════════════ @@ -118,10 +120,11 @@ class MobileElfController extends GetxController { Logger.info('$_tag event: ${event.type} data: ${event.data}'); // PTT 松开后 pttRecording=false,但 fin_result/assistant_response/tts 仍异步到达 // 用 _pttResultPending 保持会话激活,直到 ttsCompleted/error 才关闭 - final sessionActive = _inCallMode || pttRecording.value || _pttResultPending; + final sessionActive = _inCallMode || pttRecording.value || _pttResultPending || _bleWakeupActive; switch (event.type) { case AgentServiceEventType.recognitionStarted: - if (!sessionActive) break; + // BLE/AI 唤醒:即使没有主动会话也激活 + if (!sessionActive) _bleWakeupActive = true; status.value = MobileElfStatus.recording; realtimeText.value = ''; micAlive.value = true; @@ -168,6 +171,7 @@ class MobileElfController extends GetxController { case AgentServiceEventType.ttsCompleted: if (!sessionActive) break; _pttResultPending = false; // PTT 一轮结束 + _bleWakeupActive = false; // BLE 唤醒一轮结束 if (_inCallMode) { status.value = MobileElfStatus.recording; micAlive.value = true; @@ -181,6 +185,7 @@ class MobileElfController extends GetxController { case AgentServiceEventType.ttsStopped: if (!sessionActive) break; _pttResultPending = false; // TTS 被打断也视为结束 + _bleWakeupActive = false; status.value = _inCallMode ? MobileElfStatus.recording : MobileElfStatus.idle; micAlive.value = _inCallMode; break; @@ -188,6 +193,7 @@ class MobileElfController extends GetxController { case AgentServiceEventType.error: if (!sessionActive) break; _pttResultPending = false; // 出错时结束等待 + _bleWakeupActive = false; final msg = event.data['message']?.toString() ?? '未知错误'; status.value = MobileElfStatus.error; errorMsg.value = msg; @@ -260,6 +266,7 @@ class MobileElfController extends GetxController { _callStarting = false; _inCallMode = false; _pttResultPending = false; // 强制结束所有等待 + _bleWakeupActive = false; inCall.value = false; _callTimer?.cancel(); _callTimer = null;