|
|
@ -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,13 @@ 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; |
|
|
|
|
|
// PTT 正在异步启动(await 权限期间用户已松手) |
|
|
|
|
|
bool _pttStarting = false; |
|
|
|
|
|
// BLE/AI 唤醒触发的会话(非通话、非PTT模式下的唤醒) |
|
|
|
|
|
bool _bleWakeupActive = false; |
|
|
|
|
|
final _storage = GetStorage(); |
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════ |
|
|
// ═══════════════════════════════════════════════════════════════════════════ |
|
|
// 生命周期 |
|
|
// 生命周期 |
|
|
@ -62,6 +73,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,11 +120,13 @@ 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 || _bleWakeupActive; |
|
|
switch (event.type) { |
|
|
switch (event.type) { |
|
|
case AgentServiceEventType.recognitionStarted: |
|
|
case AgentServiceEventType.recognitionStarted: |
|
|
if (!sessionActive) break; |
|
|
// BLE/AI 唤醒:即使没有主动会话也激活 |
|
|
|
|
|
if (!sessionActive) _bleWakeupActive = true; |
|
|
status.value = MobileElfStatus.recording; |
|
|
status.value = MobileElfStatus.recording; |
|
|
realtimeText.value = ''; |
|
|
realtimeText.value = ''; |
|
|
micAlive.value = true; |
|
|
micAlive.value = true; |
|
|
@ -157,6 +172,8 @@ class MobileElfController extends GetxController { |
|
|
|
|
|
|
|
|
case AgentServiceEventType.ttsCompleted: |
|
|
case AgentServiceEventType.ttsCompleted: |
|
|
if (!sessionActive) break; |
|
|
if (!sessionActive) break; |
|
|
|
|
|
_pttResultPending = false; // PTT 一轮结束 |
|
|
|
|
|
_bleWakeupActive = false; // BLE 唤醒一轮结束 |
|
|
if (_inCallMode) { |
|
|
if (_inCallMode) { |
|
|
status.value = MobileElfStatus.recording; |
|
|
status.value = MobileElfStatus.recording; |
|
|
micAlive.value = true; |
|
|
micAlive.value = true; |
|
|
@ -169,12 +186,16 @@ 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 被打断也视为结束 |
|
|
|
|
|
_bleWakeupActive = false; |
|
|
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; // 出错时结束等待 |
|
|
|
|
|
_bleWakeupActive = 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 +207,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 +238,18 @@ 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 { |
|
|
|
|
|
// 若有正在播报的 PTT/BLE 唤醒会话,先强制中断 TTS/AI 流, |
|
|
|
|
|
// 使原生层立刻进入可停止状态,避免 stopConversation 长时间等待 |
|
|
|
|
|
if (_pttResultPending || _bleWakeupActive) { |
|
|
|
|
|
_pttResultPending = false; |
|
|
|
|
|
_bleWakeupActive = false; |
|
|
|
|
|
await AgentService.interruptCurrentResponse(); |
|
|
|
|
|
} |
|
|
|
|
|
// 再确保原生层完全停止 |
|
|
|
|
|
await AgentService.stopConversation(); |
|
|
|
|
|
|
|
|
final granted = await PermissionUtil.instance.requestPermission( |
|
|
final granted = await PermissionUtil.instance.requestPermission( |
|
|
permissionType: Permission.microphone, |
|
|
permissionType: Permission.microphone, |
|
|
permissionName: '麦克风', |
|
|
permissionName: '麦克风', |
|
|
@ -216,6 +266,9 @@ class MobileElfController extends GetxController { |
|
|
_callTimer = Timer.periodic( |
|
|
_callTimer = Timer.periodic( |
|
|
const Duration(seconds: 1), (_) => callSeconds.value++); |
|
|
const Duration(seconds: 1), (_) => callSeconds.value++); |
|
|
await AgentService.startConversation(mode: 'phone_call'); |
|
|
await AgentService.startConversation(mode: 'phone_call'); |
|
|
|
|
|
// startCall 返回即表示麦克风已打开、VAD 已在监听,直接显示聆听状态 |
|
|
|
|
|
// (call 模式下 recognition_started 只在 VAD 检测到语音时才发出,不能等它来转状态) |
|
|
|
|
|
if (_inCallMode) status.value = MobileElfStatus.recording; |
|
|
} finally { |
|
|
} finally { |
|
|
_callStarting = false; |
|
|
_callStarting = false; |
|
|
} |
|
|
} |
|
|
@ -224,6 +277,9 @@ class MobileElfController extends GetxController { |
|
|
Future<void> disconnect() async { |
|
|
Future<void> disconnect() async { |
|
|
_callStarting = false; |
|
|
_callStarting = false; |
|
|
_inCallMode = false; |
|
|
_inCallMode = false; |
|
|
|
|
|
_pttStarting = false; |
|
|
|
|
|
_pttResultPending = false; // 强制结束所有等待 |
|
|
|
|
|
_bleWakeupActive = false; |
|
|
inCall.value = false; |
|
|
inCall.value = false; |
|
|
_callTimer?.cancel(); |
|
|
_callTimer?.cancel(); |
|
|
_callTimer = null; |
|
|
_callTimer = null; |
|
|
@ -237,15 +293,30 @@ 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 || _pttStarting) return; |
|
|
|
|
|
_pttStarting = true; |
|
|
|
|
|
_checkHeartbeat(); |
|
|
final granted = await PermissionUtil.instance.requestPermission( |
|
|
final granted = await PermissionUtil.instance.requestPermission( |
|
|
permissionType: Permission.microphone, |
|
|
permissionType: Permission.microphone, |
|
|
permissionName: '麦克风', |
|
|
permissionName: '麦克风', |
|
|
explanationText: '移动精灵需要麦克风权限才能进行语音对话', |
|
|
explanationText: '移动精灵需要麦克风权限才能进行语音对话', |
|
|
permanentDenialText: '请在设置中开启麦克风权限以使用语音功能', |
|
|
permanentDenialText: '请在设置中开启麦克风权限以使用语音功能', |
|
|
); |
|
|
); |
|
|
if (!granted) return; |
|
|
// 权限等待期间用户已松手(stopPtt 将 _pttStarting 置 false),直接放弃 |
|
|
|
|
|
if (!granted || !_pttStarting) { |
|
|
|
|
|
_pttStarting = false; |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
_pttStarting = false; |
|
|
pttRecording.value = true; |
|
|
pttRecording.value = true; |
|
|
pttSeconds.value = 0; |
|
|
pttSeconds.value = 0; |
|
|
status.value = MobileElfStatus.connecting; |
|
|
status.value = MobileElfStatus.connecting; |
|
|
@ -256,8 +327,14 @@ class MobileElfController extends GetxController { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
void stopPtt() { |
|
|
void stopPtt() { |
|
|
|
|
|
// 若仍在等待权限授予,取消本次启动 |
|
|
|
|
|
if (_pttStarting) { |
|
|
|
|
|
_pttStarting = false; |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
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(); |
|
|
|