diff --git a/lib/data/models/appconfig.dart b/lib/data/models/appconfig.dart index d24941c85..8ce31d29f 100644 --- a/lib/data/models/appconfig.dart +++ b/lib/data/models/appconfig.dart @@ -7,7 +7,7 @@ class AppConfig { // true => 首页/发现页切换为精简布局,新手引导跳过 AI 引导步骤 // false => 保持原有布局 // TODO: 后续接入 BLE PID 判断逻辑,B017 设备连接时自动置 true - static bool hideAiAssistant = true; + static bool hideAiAssistant = false; static late UserGetAppConfigResp config; diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index 61dac4a4c..7855674d8 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -539,11 +539,45 @@ class BleManager extends GetxService { ); Logger.info('设备版本信息已更新: 版本=$version'); + + // 主动透传给原生 STS 心跳所需字段,避免上报数据不完整 + pushStsDeviceInfoToNative(); } catch (e) { Logger.error('更新版本信息失败: ${e.toString()}'); } } + /// 主动透传当前 BLE 设备 MAC / 固件版本(左3段) / CMEI 给原生 STS Agent + /// + /// 任一字段被首次填上都会在原生侧触发一次心跳补报。非 STS 模式下原生侧自然 no-op。 + void pushStsDeviceInfoToNative() { + try { + final mac = currentDeviceAddress?.toUpperCase() ?? ''; + // 固件版本原始为 "L1.L2.L3.R1.R2.R3",STS 平台只取左 3 段 + final rawFw = deviceInfo.value?.firmwareVersion ?? ''; + final fwParts = rawFw.split('.'); + final stsFirmwareVersion = + fwParts.length >= 3 ? fwParts.take(3).join('.') : rawFw; + String stsCmei = ''; + if (mac.isNotEmpty) { + try { + final matchedDevice = userInfo.User.instance.devices.firstWhere( + (d) => d.devicemac.toUpperCase() == mac, + ); + stsCmei = matchedDevice.cmei; + } catch (_) {} + } + if (mac.isEmpty && stsFirmwareVersion.isEmpty && stsCmei.isEmpty) return; + AgentService.updateStsDeviceInfo( + stsMac: mac, + stsFirmwareVersion: stsFirmwareVersion, + stsCmei: stsCmei, + ); + } catch (e) { + Logger.error('透传STS设备信息失败: $e'); + } + } + /// 更新设备id,并检测是否为移动精灵设备 void _updateProductIdFromData(Map data) { try { @@ -1376,6 +1410,9 @@ class BleManager extends GetxService { Logger.info( '保存设备信息: address=$currentDeviceAddress, name=$_currentDeviceName'); + + // 主动透传给原生 STS 心跳所需字段(mac/cmei 此时已知,固件版本待后续回调补齐) + pushStsDeviceInfoToNative(); } return await _bleService.registerBackgroundScan( diff --git a/lib/modules/mobile_elf/controllers/mobile_elf_controller.dart b/lib/modules/mobile_elf/controllers/mobile_elf_controller.dart index a615ef412..9ca497e6e 100644 --- a/lib/modules/mobile_elf/controllers/mobile_elf_controller.dart +++ b/lib/modules/mobile_elf/controllers/mobile_elf_controller.dart @@ -8,6 +8,7 @@ import 'package:permission_handler/permission_handler.dart'; import '../../../core/utils/logger.dart'; import '../../../core/utils/permission_util.dart'; +import '../../../data/services/ble_manager.dart'; enum MobileElfStatus { idle, connecting, recording, thinking, speaking, error } @@ -80,6 +81,10 @@ class MobileElfController extends GetxController { }); // 主动触发认证检查(页面打开时,确保 credential_failed 事件不被错过) AgentService.fetchCredentials(); + // 页面打开时再透传一次 STS 心跳所需的设备信息,兜底覆盖 BLE 早于 StsAgent 完成连接的场景 + try { + BleManager.to.pushStsDeviceInfoToNative(); + } catch (_) {} } Future _loadChatHistory() async { 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 029bfa8c1..9ee6ff3f1 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 @@ -1292,6 +1292,17 @@ object AgentService : CoroutineScope { stsAgent?.fetchCredentials() } + /** + * Flutter 层在拿到 BLE 设备 MAC / 固件版本 / CMEI 之后主动透传。 + * 凡是非空字段都会写入 StsAgent,若某字段从空首次填上则会触发一次心跳上报; + * 心跳本身在原生层做完整性校验,缺字段会自动跳过。 + */ + fun updateStsDeviceInfo(mac: String?, firmwareVersion: String?, cmei: String?) { + if (!mac.isNullOrEmpty()) stsAgent?.updateMac(mac) + if (!firmwareVersion.isNullOrEmpty()) stsAgent?.updateFirmwareVersion(firmwareVersion) + if (!cmei.isNullOrEmpty()) stsAgent?.updateCmei(cmei) + } + 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 f4078607c..081d4c61f 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 @@ -106,6 +106,8 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream config["stsDeviceId"]?.toString()?.let { intent.putExtra("stsDeviceId", it) } config["stsDeviceSecret"]?.toString()?.let { intent.putExtra("stsDeviceSecret", it) } config["stsCmei"]?.toString()?.let { intent.putExtra("stsCmei", it) } + config["stsMac"]?.toString()?.let { intent.putExtra("stsMac", it) } + config["stsFirmwareVersion"]?.toString()?.let { intent.putExtra("stsFirmwareVersion", it) } config["vipexptime"]?.toString()?.let { intent.putExtra("vipexptime", it) //打印日志 @@ -384,6 +386,19 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream } } + "updateStsDeviceInfo" -> { + try { + val mac = call.argument("stsMac") + val firmwareVersion = call.argument("stsFirmwareVersion") + val cmei = call.argument("stsCmei") + AgentService.updateStsDeviceInfo(mac, firmwareVersion, cmei) + result.success(true) + } catch (e: Exception) { + Log.e(TAG, "更新STS设备信息失败", e) + result.error("UPDATE_STS_DEVICE_INFO_ERROR", "更新STS设备信息失败: ${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/BleMediaService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt index 574af96ff..5ebe41882 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt @@ -86,6 +86,8 @@ class BleMediaService : Service(), CoroutineScope { private const val KEY_STS_DEVICE_ID = "stsDeviceId" private const val KEY_STS_DEVICE_SECRET = "stsDeviceSecret" private const val KEY_STS_CMEI = "stsCmei" + private const val KEY_STS_MAC = "stsMac" + private const val KEY_STS_FIRMWARE_VERSION = "stsFirmwareVersion" // 用于跟踪服务是否正在运行 private val isRunning = AtomicBoolean(false) @@ -623,6 +625,8 @@ private var companionManager: BleCompanionManager? = null it.getStringExtra(KEY_STS_DEVICE_ID)?.let { value -> config[KEY_STS_DEVICE_ID] = value } it.getStringExtra(KEY_STS_DEVICE_SECRET)?.let { value -> config[KEY_STS_DEVICE_SECRET] = value } it.getStringExtra(KEY_STS_CMEI)?.let { value -> config[KEY_STS_CMEI] = value } + it.getStringExtra(KEY_STS_MAC)?.let { value -> config[KEY_STS_MAC] = value } + it.getStringExtra(KEY_STS_FIRMWARE_VERSION)?.let { value -> config[KEY_STS_FIRMWARE_VERSION] = value } Log.d(TAG, "liwei-------vipexptime 3-1 ${ config[KEY_VIP_EXPTIME]}") // 提取 supportedLanguages 参数 it.getStringArrayListExtra(KEY_SUPPORTED_LANGUAGES)?.let { languages -> @@ -698,6 +702,8 @@ private var companionManager: BleCompanionManager? = null config[KEY_STS_DEVICE_ID] = getConfigString(prefs, KEY_STS_DEVICE_ID, "") config[KEY_STS_DEVICE_SECRET] = getConfigString(prefs, KEY_STS_DEVICE_SECRET, "") config[KEY_STS_CMEI] = getConfigString(prefs, KEY_STS_CMEI, "") + config[KEY_STS_MAC] = getConfigString(prefs, KEY_STS_MAC, "") + config[KEY_STS_FIRMWARE_VERSION] = getConfigString(prefs, KEY_STS_FIRMWARE_VERSION, "") Log.d(TAG, "liwei-------vipexptime 3-0 ${ config[KEY_VIP_EXPTIME]}") // 添加 supportedLanguages 参数处理 val supportedLanguagesStr = getConfigString(prefs, KEY_SUPPORTED_LANGUAGES, "") 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 d3c279494..ad533c229 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 @@ -70,24 +70,30 @@ class StsAgent( /** 动态更新 CMEI(首次初始化时可能尚未获取到) */ fun updateCmei(cmei: String) { if (cmei.isNotEmpty() && cmei != config.cmei) { + val wasEmpty = config.cmei.isEmpty() config = config.copy(cmei = cmei) Log.i(TAG, "[YIDONG] updateCmei: cmei已更新为 $cmei") + if (wasEmpty) reportHeartbeat() } } /** 动态更新 MAC(首次初始化时可能尚未获取到) */ fun updateMac(mac: String) { if (mac.isNotEmpty() && mac != config.mac) { + val wasEmpty = config.mac.isEmpty() config = config.copy(mac = mac) Log.i(TAG, "[YIDONG] updateMac: mac已更新为 $mac") + if (wasEmpty) reportHeartbeat() } } /** 动态更新固件版本(设备连接后由 BLE 上报,首次初始化时通常尚未获取到) */ fun updateFirmwareVersion(firmwareVersion: String) { if (firmwareVersion.isNotEmpty() && firmwareVersion != config.firmwareVersion) { + val wasEmpty = config.firmwareVersion.isEmpty() config = config.copy(firmwareVersion = firmwareVersion) Log.i(TAG, "[YIDONG] updateFirmwareVersion: firmwareVersion已更新为 $firmwareVersion") + if (wasEmpty) reportHeartbeat() } } @@ -181,7 +187,12 @@ class StsAgent( private set // ── 心跳冷却 ────────────────────────────────────────────────────────────── + // 同一进程内 StsAgent 会被反复重建(切页面/BLE 重连等),需避免重复上报; + // 但进程冷启动必须能再报一次,所以用进程级静态变量而非 SharedPreferences。 private val HEARTBEAT_COOLDOWN_MS = 3_600_000L // 1 小时 + companion object { + @Volatile private var lastHeartbeatTs: Long = 0L + } // ═════════════════════════════════════════════════════════════════════════ // 凭证获取 @@ -246,16 +257,29 @@ class StsAgent( Log.w(TAG, "[YIDONG] reportHeartbeat: 凭证未就绪,跳过上报") return } - // 1 小时冷却:StsAgent 会被反复重建,init/fetchCredentials 都会调到这里, - // 必须在原生层拦,否则每次重建都会打一发心跳 - val prefs = context.getSharedPreferences("sts_agent", Context.MODE_PRIVATE) - val last = prefs.getLong("last_heartbeat_ts", 0L) + // 数据完整性校验:deviceId / cmei / mac / firmwareVersion 任一为空都不上报。 + // 这些字段在 BLE 配对/连接成功后才会透传过来,首次初始化时通常缺失; + // 空数据上报既无意义,又会把 1 小时冷却时间戳写死,阻塞 update*() 触发的补报。 + val missing = buildList { + if (config.deviceId.isEmpty()) add("deviceId") + if (config.cmei.isEmpty()) add("cmei") + if (config.mac.isEmpty()) add("mac") + if (config.firmwareVersion.isEmpty()) add("firmwareVersion") + } + if (missing.isNotEmpty()) { + Log.i(TAG, "[YIDONG] reportHeartbeat: 数据不完整,跳过上报 missing=${missing.joinToString(",")} deviceId=${config.deviceId} cmei=${config.cmei} mac=${config.mac} firmwareVersion=${config.firmwareVersion}") + return + } + // 进程级 1 小时冷却:同一进程内 StsAgent 会被反复重建(切页面/BLE 重连等), + // init/fetchCredentials 都会调到这里,必须在原生层拦; + // 但冷却仅在进程生命周期内有效,进程冷启动后归零,保证每次启动都能上报一次。 + val last = lastHeartbeatTs val now = System.currentTimeMillis() if (last > 0L && now - last < HEARTBEAT_COOLDOWN_MS) { Log.i(TAG, "[YIDONG] reportHeartbeat: 距上次 ${(now - last) / 1000}s,冷却中跳过") return } - prefs.edit().putLong("last_heartbeat_ts", now).apply() + lastHeartbeatTs = now Log.i(TAG, "[YIDONG] reportHeartbeat: 开始上报 deviceId=${config.deviceId} deviceNo=${config.deviceNo} cmei=${config.cmei} mac=${config.mac} firmwareVersion=${config.firmwareVersion}") launch { try { @@ -303,13 +327,13 @@ class StsAgent( } else { Log.e(TAG, "[YIDONG] reportHeartbeat: 上报失败 code=$code msg=$msg") // 上报失败回滚时间戳,避免卡 1 小时无法重试 - prefs.edit().putLong("last_heartbeat_ts", last).apply() + lastHeartbeatTs = last } } } catch (e: Exception) { Log.e(TAG, "[YIDONG] reportHeartbeat: 请求异常 ${e.message}") // 异常同样回滚时间戳 - prefs.edit().putLong("last_heartbeat_ts", last).apply() + lastHeartbeatTs = last } } } diff --git a/local_plugins/agent_service/lib/agent_service.dart b/local_plugins/agent_service/lib/agent_service.dart index 533f76ddf..31bc2acd3 100644 --- a/local_plugins/agent_service/lib/agent_service.dart +++ b/local_plugins/agent_service/lib/agent_service.dart @@ -514,6 +514,27 @@ class AgentService { } } + /// 主动透传 BLE 设备 MAC / 固件版本 / CMEI 到原生层(移动精灵) + /// + /// 这些字段在 startAgentService 时可能尚未拿到,需要 Flutter 在后续 + /// 拿到后主动调用此方法补齐;原生侧任一空字段被首次填上都会触发一次心跳上报。 + /// 传 null 或空字符串的字段会被忽略,不会覆盖已有值。 + static Future updateStsDeviceInfo({ + String? stsMac, + String? stsFirmwareVersion, + String? stsCmei, + }) async { + try { + await _channel.invokeMethod('updateStsDeviceInfo', { + 'stsMac': stsMac, + 'stsFirmwareVersion': stsFirmwareVersion, + 'stsCmei': stsCmei, + }); + } on PlatformException catch (_) { + // 透传失败不影响主流程,原生侧会在下次调用时再补 + } + } + /// 中断当前响应 /// /// 停止语音合成和AI流输出