Browse Source

确定移动设备绑定逻辑

newdev_shunjiawei
liwei1dao 7 months ago
parent
commit
02533577a3
  1. 4
      lib/data/models/user_Info.dart
  2. 19
      lib/data/services/ble_manager.dart
  3. 5
      lib/modules/home/controllers/home_controller.dart
  4. 50
      lib/modules/mobile_elf/controllers/mobile_elf_controller.dart
  5. 19
      lib/modules/mobile_elf/views/mobile_elf_view.dart
  6. 107
      lib/modules/pairing/controllers/pairing_controller.dart
  7. 10
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  8. 20
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  9. 81
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/StsAgent.kt
  10. 29
      local_plugins/agent_service/lib/agent_service.dart

4
lib/data/models/user_Info.dart

@ -212,6 +212,7 @@ class User {
class UserDevice { class UserDevice {
int id; int id;
String uid; String uid;
int productid;
int devicetype; int devicetype;
String devicename; String devicename;
String devicemac; String devicemac;
@ -219,6 +220,7 @@ class UserDevice {
UserDevice({ UserDevice({
required this.id, required this.id,
required this.uid, required this.uid,
required this.productid,
required this.devicetype, required this.devicetype,
required this.devicename, required this.devicename,
required this.devicemac, required this.devicemac,
@ -228,6 +230,7 @@ class UserDevice {
factory UserDevice.fromJson(Map<String, dynamic> json) => UserDevice( factory UserDevice.fromJson(Map<String, dynamic> json) => UserDevice(
id: json['id'] as int, id: json['id'] as int,
uid: json['uid'] as String, uid: json['uid'] as String,
productid: (json['productid'] as num).toInt(),
devicetype: (json['devicetype'] as num).toInt(), devicetype: (json['devicetype'] as num).toInt(),
devicename: json['devicename'] as String, devicename: json['devicename'] as String,
devicemac: json['devicemac'] as String, devicemac: json['devicemac'] as String,
@ -237,6 +240,7 @@ class UserDevice {
toJson() => { toJson() => {
'id': id, 'id': id,
'uid': uid, 'uid': uid,
'productid': productid,
'devicetype': devicetype, 'devicetype': devicetype,
'devicename': devicename, 'devicename': devicename,
'devicemac': devicemac, 'devicemac': devicemac,

19
lib/data/services/ble_manager.dart

@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:ble_service/ble_service.dart'; import 'package:ble_service/ble_service.dart';
@ -587,23 +586,17 @@ class BleManager extends GetxService {
} }
} }
/// 更新设备id /// 更新设备id,并检测是否为移动精灵设备
void _updateProductIdFromData(Map<dynamic, dynamic> data) { void _updateProductIdFromData(Map<dynamic, dynamic> data) {
try { try {
// 确保数据中包含版本信息
if (!data.containsKey('productId')) { if (!data.containsKey('productId')) {
Logger.warning('版本信息数据不完整'); Logger.warning('版本信息数据不完整');
return; return;
} }
// 提取版本信息
final String productId = data['productId'] as String? ?? ''; final String productId = data['productId'] as String? ?? '';
// 更新设备信息 _updateDeviceInfoIfNeeded(firmwareProductId: productId);
_updateDeviceInfoIfNeeded(
firmwareProductId: productId,
);
Logger.info('设备设备id已更新: 设备id=$productId'); Logger.info('设备设备id已更新: 设备id=$productId');
} catch (e) { } catch (e) {
Logger.error('更新设备id失败: ${e.toString()}'); Logger.error('更新设备id失败: ${e.toString()}');
@ -959,14 +952,18 @@ class BleManager extends GetxService {
} }
// STS 模式参数(移动精灵) // STS 模式参数(移动精灵)
final agentType = AppConfig.env('AGENT_TYPE') ?? 'yidong'; // 优先使用设备检测结果(YIDONG_PID 匹配时写入),否则用服务端 AGENT_TYPE
final agentType = _storage.read<String>('device_agent_type')
?? AppConfig.env('AGENT_TYPE')
?? 'stt_llm_tts';
final agentMode = (agentType == 'yidong') ? 'sts' : 'stt_llm_tts'; final agentMode = (agentType == 'yidong') ? 'sts' : 'stt_llm_tts';
final agentId = (agentType == 'yidong') final agentId = (agentType == 'yidong')
? (AppConfig.env('MOBILE_ELF_AGENT_ID') ?? 'mobile_elf') ? (AppConfig.env('MOBILE_ELF_AGENT_ID') ?? 'mobile_elf')
: 'default_agent'; : 'default_agent';
final stsProductId = AppConfig.env('MOBILE_ELF_PRODUCT_ID') ?? ''; final stsProductId = AppConfig.env('MOBILE_ELF_PRODUCT_ID') ?? '';
final stsProductKey = AppConfig.env('MOBILE_ELF_PRODUCT_KEY') ?? ''; final stsProductKey = AppConfig.env('MOBILE_ELF_PRODUCT_KEY') ?? '';
final stsDeviceNo = dotenv.env['MOBILE_ELF_DEVICE_NO'] ?? ''; // 使用当前连接设备的 SN(配对时写入),认证失败由原生层上报 credential_failed 事件
final stsDeviceNo = _storage.read<String>('device_sts_code') ?? '';
final stsDeviceId = _storage.read<String>('sts_device_id') ?? ''; final stsDeviceId = _storage.read<String>('sts_device_id') ?? '';
final stsDeviceSecret = _storage.read<String>('sts_device_secret') ?? ''; final stsDeviceSecret = _storage.read<String>('sts_device_secret') ?? '';

5
lib/modules/home/controllers/home_controller.dart

@ -256,7 +256,10 @@ class HomeController extends GetxController {
} }
void exploreAIFeatures() { void exploreAIFeatures() {
final agentType = AppConfig.env('AGENT_TYPE') ?? 'yidong'; // 优先读设备检测写入的 storage(YIDONG_PID 匹配时),否则降级到服务端 AGENT_TYPE
final agentType = GetStorage().read<String>('device_agent_type')
?? AppConfig.env('AGENT_TYPE')
?? 'stt_llm_tts';
if (agentType == 'yidong') { if (agentType == 'yidong') {
Get.toNamed(Routes.mobileElf); Get.toNamed(Routes.mobileElf);
} else { } else {

50
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:chat_storage/chat_storage.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import '../../../data/models/appconfig.dart'; import '../../../data/models/appconfig.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
@ -39,6 +40,9 @@ class MobileElfController extends GetxController {
// 凭证状态(原生层负责,Dart 层始终无错) // 凭证状态(原生层负责,Dart 层始终无错)
final credentialReady = true.obs; final credentialReady = true.obs;
final credentialError = ''.obs; final credentialError = ''.obs;
// 设备认证失败(STS credential_failed 事件触发)
final credentialFailed = false.obs;
final credentialFailedReason = ''.obs;
// ── 内部 ─────────────────────────────────────────────────────────────────── // ── 内部 ───────────────────────────────────────────────────────────────────
StreamSubscription<AgentServiceEvent>? _eventSub; StreamSubscription<AgentServiceEvent>? _eventSub;
@ -46,6 +50,9 @@ class MobileElfController extends GetxController {
Timer? _pttTimer; Timer? _pttTimer;
bool _inCallMode = false; bool _inCallMode = false;
bool _callStarting = false; // 防止并发 startCall 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) { ever(ttsEnabled, (bool enabled) {
AgentService.setTtsMuted(!enabled); AgentService.setTtsMuted(!enabled);
}); });
// 主动触发认证检查(页面打开时,确保 credential_failed 事件不被错过)
AgentService.fetchCredentials();
} }
Future<void> _loadChatHistory() async { Future<void> _loadChatHistory() async {
@ -107,8 +116,9 @@ class MobileElfController extends GetxController {
void _onAgentEvent(AgentServiceEvent event) { void _onAgentEvent(AgentServiceEvent event) {
Logger.info('$_tag event: ${event.type} data: ${event.data}'); Logger.info('$_tag event: ${event.type} data: ${event.data}');
// 已挂断/停止时忽略原生层残留的状态事件 // PTT 松开后 pttRecording=false,但 fin_result/assistant_response/tts 仍异步到达
final sessionActive = _inCallMode || pttRecording.value; // 用 _pttResultPending 保持会话激活,直到 ttsCompleted/error 才关闭
final sessionActive = _inCallMode || pttRecording.value || _pttResultPending;
switch (event.type) { switch (event.type) {
case AgentServiceEventType.recognitionStarted: case AgentServiceEventType.recognitionStarted:
if (!sessionActive) break; if (!sessionActive) break;
@ -157,6 +167,7 @@ class MobileElfController extends GetxController {
case AgentServiceEventType.ttsCompleted: case AgentServiceEventType.ttsCompleted:
if (!sessionActive) break; if (!sessionActive) break;
_pttResultPending = false; // PTT 一轮结束
if (_inCallMode) { if (_inCallMode) {
status.value = MobileElfStatus.recording; status.value = MobileElfStatus.recording;
micAlive.value = true; micAlive.value = true;
@ -169,12 +180,14 @@ class MobileElfController extends GetxController {
case AgentServiceEventType.ttsCanceled: case AgentServiceEventType.ttsCanceled:
case AgentServiceEventType.ttsStopped: case AgentServiceEventType.ttsStopped:
if (!sessionActive) break; if (!sessionActive) break;
_pttResultPending = false; // TTS 被打断也视为结束
status.value = _inCallMode ? MobileElfStatus.recording : MobileElfStatus.idle; status.value = _inCallMode ? MobileElfStatus.recording : MobileElfStatus.idle;
micAlive.value = _inCallMode; micAlive.value = _inCallMode;
break; break;
case AgentServiceEventType.error: case AgentServiceEventType.error:
if (!sessionActive) break; if (!sessionActive) break;
_pttResultPending = false; // 出错时结束等待
final msg = event.data['message']?.toString() ?? '未知错误'; final msg = event.data['message']?.toString() ?? '未知错误';
status.value = MobileElfStatus.error; status.value = MobileElfStatus.error;
errorMsg.value = msg; errorMsg.value = msg;
@ -186,6 +199,24 @@ class MobileElfController extends GetxController {
micRms.value = rms; micRms.value = rms;
break; 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: default:
break; break;
} }
@ -199,7 +230,11 @@ class MobileElfController extends GetxController {
Future<void> startCall() async { Future<void> startCall() async {
if (inCall.value || _callStarting) return; if (inCall.value || _callStarting) return;
_callStarting = true; _callStarting = true;
_checkHeartbeat();
try { try {
// 先确保原生层完全停止(处理 onClose 未 await 导致的残留状态)
await AgentService.stopConversation();
final granted = await PermissionUtil.instance.requestPermission( final granted = await PermissionUtil.instance.requestPermission(
permissionType: Permission.microphone, permissionType: Permission.microphone,
permissionName: '麦克风', permissionName: '麦克风',
@ -224,6 +259,7 @@ class MobileElfController extends GetxController {
Future<void> disconnect() async { Future<void> disconnect() async {
_callStarting = false; _callStarting = false;
_inCallMode = false; _inCallMode = false;
_pttResultPending = false; // 强制结束所有等待
inCall.value = false; inCall.value = false;
_callTimer?.cancel(); _callTimer?.cancel();
_callTimer = null; _callTimer = null;
@ -237,8 +273,17 @@ class MobileElfController extends GetxController {
await AgentService.stopConversation(); await AgentService.stopConversation();
} }
/// 超过1小时未上报心跳则补报一次
void _checkHeartbeat() {
final last = _storage.read<int>('sts_heartbeat_time') ?? 0;
if (DateTime.now().millisecondsSinceEpoch - last > 3600000) {
AgentService.reportHeartbeat().catchError((_) {});
}
}
Future<void> startPtt() async { Future<void> startPtt() async {
if (pttRecording.value) return; if (pttRecording.value) return;
_checkHeartbeat();
final granted = await PermissionUtil.instance.requestPermission( final granted = await PermissionUtil.instance.requestPermission(
permissionType: Permission.microphone, permissionType: Permission.microphone,
permissionName: '麦克风', permissionName: '麦克风',
@ -258,6 +303,7 @@ class MobileElfController extends GetxController {
void stopPtt() { void stopPtt() {
if (!pttRecording.value) return; if (!pttRecording.value) return;
pttRecording.value = false; pttRecording.value = false;
_pttResultPending = true; // 等待 fin_result/assistant_response/tts 异步回来
_pttTimer?.cancel(); _pttTimer?.cancel();
_pttTimer = null; _pttTimer = null;
AgentService.stopConversation(); AgentService.stopConversation();

19
lib/modules/mobile_elf/views/mobile_elf_view.dart

@ -143,6 +143,17 @@ class MobileElfView extends GetView<MobileElfController> {
/// 右上角状态胶囊 + 通话计时 /// 右上角状态胶囊 + 通话计时
Widget _buildStatusPill() { 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 s = controller.status.value;
final inCall = controller.inCall.value; final inCall = controller.inCall.value;
final cfg = _pillConfig(s, inCall); final cfg = _pillConfig(s, inCall);
@ -444,6 +455,7 @@ class MobileElfView extends GetView<MobileElfController> {
Widget _buildBottomBar(bool isDark) { Widget _buildBottomBar(bool isDark) {
return Obx(() { return Obx(() {
final inCall = controller.inCall.value; final inCall = controller.inCall.value;
final s = controller.status.value; final s = controller.status.value;
final ptt = controller.pttRecording.value; final ptt = controller.pttRecording.value;
@ -621,9 +633,10 @@ class MobileElfView extends GetView<MobileElfController> {
final s = controller.status.value; final s = controller.status.value;
final ptt = controller.pttRecording.value; final ptt = controller.pttRecording.value;
final disabled = !ptt && s != MobileElfStatus.idle && s != MobileElfStatus.error; final locked = controller.credentialFailed.value;
final pttClickable = s == MobileElfStatus.speaking; final disabled = locked || (!ptt && s != MobileElfStatus.idle && s != MobileElfStatus.error);
final sideDisabled = ptt || (disabled && !pttClickable); final pttClickable = !locked && s == MobileElfStatus.speaking;
final sideDisabled = locked || ptt || (disabled && !pttClickable);
return Padding( return Padding(
padding: EdgeInsets.fromLTRB(12.w, 6.h, 12.w, 14.h), padding: EdgeInsets.fromLTRB(12.w, 6.h, 12.w, 14.h),

107
lib/modules/pairing/controllers/pairing_controller.dart

@ -810,6 +810,7 @@ class PairingController extends GetxController with WidgetsBindingObserver {
orElse: () => UserDevice( orElse: () => UserDevice(
id: -1, id: -1,
uid: '', uid: '',
productid: -1,
devicetype: -1, devicetype: -1,
devicename: '', devicename: '',
devicemac: '', devicemac: '',
@ -840,15 +841,21 @@ class PairingController extends GetxController with WidgetsBindingObserver {
orElse: () => UserDevice( orElse: () => UserDevice(
id: -1, id: -1,
uid: '', uid: '',
productid: -1,
devicetype: -1, devicetype: -1,
devicename: '', devicename: '',
devicemac: '', devicemac: '',
license: ''), license: ''),
); );
if (existingDevice.id != -1) { if (existingDevice.id != -1) {
_verifiedDevicePid = existingDevice.productid;
_verifiedDeviceName = device.name; _verifiedDeviceName = device.name;
_verifiedDeviceAddress = device.address; _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; return 10;
} }
} }
@ -885,14 +892,15 @@ class PairingController extends GetxController with WidgetsBindingObserver {
code = extractAndReverseBytes(hexData, 13, 22); code = extractAndReverseBytes(hexData, 13, 22);
_verifiedDevicePid = _verifiedDevicePid =
extractAndReverseBytesToInt(hexData, 4, 5); 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 != "") { if (code != "") {
_bleManager.stopScan(); _bleManager.stopScan();
deviceCode.value = code; deviceCode.value = code;
String? uuids = deviceInfo["id"]; String? uuids = deviceInfo["id"];
_verifiedDeviceName = device.name; _verifiedDeviceName = device.name;
_verifiedDeviceAddress = uuids; _verifiedDeviceAddress = uuids;
print('找到匹配设备,获取到 uuids: $uuids'); Logger.i(_tag, '[YIDONG] iOS deviceCode提取成功 code=$code uuid=$uuids');
//检查是否已经绑定 //检查是否已经绑定
final existingDevice = User.instance.devices.firstWhere( final existingDevice = User.instance.devices.firstWhere(
(d) => (d) =>
@ -902,6 +910,7 @@ class PairingController extends GetxController with WidgetsBindingObserver {
orElse: () => UserDevice( orElse: () => UserDevice(
id: -1, id: -1,
uid: '', uid: '',
productid: -1,
devicetype: -1, devicetype: -1,
devicename: '', devicename: '',
devicemac: '', devicemac: '',
@ -921,12 +930,14 @@ class PairingController extends GetxController with WidgetsBindingObserver {
code = extractAndReverseBytes(hexData, 15, 24); code = extractAndReverseBytes(hexData, 15, 24);
_verifiedDevicePid = _verifiedDevicePid =
extractAndReverseBytesToInt(hexData, 6, 7); 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 != "") { if (code != "") {
_bleManager.stopScan(); _bleManager.stopScan();
deviceCode.value = code; deviceCode.value = code;
_verifiedDeviceName = device.name; _verifiedDeviceName = device.name;
_verifiedDeviceAddress = device.address; _verifiedDeviceAddress = device.address;
Logger.i(_tag, '[YIDONG] Android deviceCode提取成功 code=$code mac=${device.address}');
//检查是否已经绑定 //检查是否已经绑定
final existingDevice = User.instance.devices.firstWhere( final existingDevice = User.instance.devices.firstWhere(
(d) => (d) =>
@ -936,6 +947,7 @@ class PairingController extends GetxController with WidgetsBindingObserver {
orElse: () => UserDevice( orElse: () => UserDevice(
id: -1, id: -1,
uid: '', uid: '',
productid: -1,
devicetype: -1, devicetype: -1,
devicename: '', devicename: '',
devicemac: '', 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<String>('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() { void _navigateToHome() {
// Future.delayed(const Duration(milliseconds: 500), () async { // 检测是否为移动精灵(YIDONG)设备
_detectAndSaveYidongDevice();
// 启动AgentService服务 // 启动AgentService服务
try { try {
Logger.i(_tag, '启动AgentService服务'); Logger.i(_tag, '启动AgentService服务');
_bleManager.startAgentService(); _bleManager.startAgentService();
// _bleManager.registerBackgroundScan(macAddress: _verifiedDeviceAddress);
} catch (e) { } catch (e) {
Logger.e(_tag, '启动AgentService失败: $e'); Logger.e(_tag, '启动AgentService失败: $e');
} }
//print('lxm-上一个路由: ${Get.previousRoute.toString()}'); // 已在 Home 页时不重复导航,避免两个 HomeView 同时存在导致 Duplicate GlobalKey
// if (Get.previousRoute == Routes.home) { if (Get.currentRoute == Routes.home) return;
// 如果是从home页过来的,则直接返回
// Get.back();
// } else {
// 否则,跳转到home页并清空之前的路由栈
Get.offAllNamed(Routes.home); Get.offAllNamed(Routes.home);
// }
// });
} }
// ==================== 用户操作 ==================== // ==================== 用户操作 ====================
@ -1259,6 +1337,7 @@ class PairingController extends GetxController with WidgetsBindingObserver {
UserDevice( UserDevice(
id: 0, // 临时设备ID id: 0, // 临时设备ID
uid: User.instance.uid, uid: User.instance.uid,
productid: _verifiedDevicePid ?? -1,
devicetype: 1, devicetype: 1,
devicemac: _currentClassicDevice?.address ?? '', devicemac: _currentClassicDevice?.address ?? '',
devicename: _currentClassicDevice?.name ?? 'Unknown Device', devicename: _currentClassicDevice?.name ?? 'Unknown Device',

10
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -987,6 +987,16 @@ object AgentService : CoroutineScope {
stsAgent?.ttsMuted = muted stsAgent?.ttsMuted = muted
} }
/** 手动触发心跳上报(Flutter 层调用) */
fun reportHeartbeat() {
stsAgent?.reportHeartbeat()
}
/** 主动触发认证检查(移动精灵页面打开时调用,确保认证事件被 Flutter 层接收) */
fun fetchCredentials() {
stsAgent?.fetchCredentials()
}
fun interruptCurrentResponse() { fun interruptCurrentResponse() {
if (agentMode == "sts") { if (agentMode == "sts") {
stsAgent?.interruptSpeaking() stsAgent?.interruptSpeaking()

20
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" -> { "setCanWakeUp" -> {
try { try {
val canWakeUp = call.argument<Boolean>("canWakeUp") val canWakeUp = call.argument<Boolean>("canWakeUp")

81
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 override val coroutineContext = Dispatchers.IO + job
init { init {
Log.i(TAG, "[YIDONG] StsAgent初始化: productId=${config.productId} deviceNo=${config.deviceNo} deviceId=${config.deviceId} agentId=${config.agentId} hasSecret=${config.deviceSecret.isNotEmpty()}")
fetchCredentials() fetchCredentials()
} }
@ -153,8 +154,11 @@ class StsAgent(
fun fetchCredentials() { fun fetchCredentials() {
if (config.deviceSecret.isNotEmpty()) { if (config.deviceSecret.isNotEmpty()) {
credentialReady = true credentialReady = true
Log.i(TAG, "[YIDONG] fetchCredentials: 凭证已存在,跳过请求 deviceNo=${config.deviceNo} deviceId=${config.deviceId}")
reportHeartbeat() // 凭证已有,直接上报心跳
return return
} }
Log.i(TAG, "[YIDONG] fetchCredentials: 开始请求密钥 productId=${config.productId} deviceNo=${config.deviceNo}")
launch { launch {
try { try {
val body = JSONObject().apply { val body = JSONObject().apply {
@ -180,14 +184,82 @@ class StsAgent(
) )
credentialReady = config.deviceSecret.isNotEmpty() credentialReady = config.deviceSecret.isNotEmpty()
if (credentialReady) { if (credentialReady) {
Log.i(TAG, "[YIDONG] fetchCredentials: 密钥获取成功 deviceId=${config.deviceId} deviceNo=${config.deviceNo}")
onCredentialsFetched?.invoke(config.deviceId, config.deviceSecret, 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 { } 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) { } 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 (ttsPlaying) interruptTts()
if (pttRecording) stopPtt(cancel = true) if (pttRecording) stopPtt(cancel = true)
if (!credentialReady) { emitError("移动精灵凭证未就绪,请稍后重试"); return } if (!credentialReady) { emitError("移动精灵凭证未就绪,请稍后重试"); return }
Log.i(TAG, "[YIDONG] startCall: 启动通话模式 deviceNo=${config.deviceNo} agentId=${config.agentId}")
openMic() openMic()
startMicWatchdog() startMicWatchdog()
inCall = true inCall = true
@ -213,6 +286,7 @@ class StsAgent(
if (pttRecording) return if (pttRecording) return
if (ttsPlaying) interruptTts() if (ttsPlaying) interruptTts()
if (!credentialReady) { emitError("移动精灵凭证未就绪"); return } if (!credentialReady) { emitError("移动精灵凭证未就绪"); return }
Log.i(TAG, "[YIDONG] startPtt: 启动PTT模式 deviceNo=${config.deviceNo} agentId=${config.agentId}")
pttRecording = true pttRecording = true
openMic() openMic()
@ -445,6 +519,7 @@ class StsAgent(
"?deviceNo=${config.deviceNo}&deviceId=${config.deviceId}" + "?deviceNo=${config.deviceNo}&deviceId=${config.deviceId}" +
"&productId=${config.productId}&productKey=${config.productKey}" + "&productId=${config.productId}&productKey=${config.productKey}" +
"&sign=$sign&sn=$sn&ts=$ts" "&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) val req = Request.Builder().url(url)
.header("Origin", "https://ivs.chinamobiledevice.com:11443") .header("Origin", "https://ivs.chinamobiledevice.com:11443")
@ -462,6 +537,7 @@ class StsAgent(
try { try {
val msg = JSONObject(text) val msg = JSONObject(text)
if (msg.optString("type") == "ready") { if (msg.optString("type") == "ready") {
Log.i(TAG, "[YIDONG] connectWs: 收到ready,WSS连接成功")
if (!resolved) { resolved = true; cont.resume(Unit) {} } if (!resolved) { resolved = true; cont.resume(Unit) {} }
} else { } else {
launch { handleMessage(msg) } launch { handleMessage(msg) }
@ -472,6 +548,7 @@ class StsAgent(
override fun onMessage(ws: WebSocket, bytes: okio.ByteString) {} override fun onMessage(ws: WebSocket, bytes: okio.ByteString) {}
override fun onFailure(ws: WebSocket, t: Throwable, response: Response?) { override fun onFailure(ws: WebSocket, t: Throwable, response: Response?) {
Log.e(TAG, "[YIDONG] connectWs: WSS连接失败 ${t.message}")
if (webSocket === ws) webSocket = null // 只清除自己的引用,避免覆盖新 WS if (webSocket === ws) webSocket = null // 只清除自己的引用,避免覆盖新 WS
if (!resolved) { resolved = true; cont.resumeWithException(t) } if (!resolved) { resolved = true; cont.resumeWithException(t) }
else launch { handleWsDone() } else launch { handleWsDone() }

29
local_plugins/agent_service/lib/agent_service.dart

@ -77,6 +77,12 @@ enum AgentServiceEventType {
/// 麦克风音量(RMS) /// 麦克风音量(RMS)
micRms, micRms,
/// 心跳上报成功
heartbeatReported,
/// STS 设备认证失败
credentialFailed,
/// 未知事件 /// 未知事件
unknown unknown
} }
@ -186,6 +192,10 @@ class AgentService {
return AgentServiceEventType.stsCredentials; return AgentServiceEventType.stsCredentials;
case 'mic_rms': case 'mic_rms':
return AgentServiceEventType.micRms; return AgentServiceEventType.micRms;
case 'heartbeat_reported':
return AgentServiceEventType.heartbeatReported;
case 'credential_failed':
return AgentServiceEventType.credentialFailed;
default: default:
return AgentServiceEventType.unknown; return AgentServiceEventType.unknown;
} }
@ -468,6 +478,25 @@ class AgentService {
} }
} }
/// 手动触发心跳上报(移动精灵)
static Future<void> reportHeartbeat() async {
try {
await _channel.invokeMethod('reportHeartbeat');
} on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '心跳上报失败', e.details);
}
}
/// 主动触发认证检查(移动精灵页面打开时调用)
/// 若凭证不存在则重新请求,结果通过事件流返回(credentialFailed / heartbeatReported)
static Future<void> fetchCredentials() async {
try {
await _channel.invokeMethod('fetchCredentials');
} on PlatformException catch (_) {
// 忽略异常,认证结果通过事件流反馈
}
}
/// 中断当前响应 /// 中断当前响应
/// ///
/// 停止语音合成和AI流输出 /// 停止语音合成和AI流输出

Loading…
Cancel
Save