|
|
@ -3,8 +3,10 @@ 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: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'; |
|
|
|
|
|
import '../../../core/utils/permission_util.dart'; |
|
|
|
|
|
|
|
|
enum MobileElfStatus { idle, connecting, recording, thinking, speaking, error } |
|
|
enum MobileElfStatus { idle, connecting, recording, thinking, speaking, error } |
|
|
|
|
|
|
|
|
@ -43,6 +45,7 @@ class MobileElfController extends GetxController { |
|
|
Timer? _callTimer; |
|
|
Timer? _callTimer; |
|
|
Timer? _pttTimer; |
|
|
Timer? _pttTimer; |
|
|
bool _inCallMode = false; |
|
|
bool _inCallMode = false; |
|
|
|
|
|
bool _callStarting = false; // 防止并发 startCall |
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════ |
|
|
// ═══════════════════════════════════════════════════════════════════════════ |
|
|
// 生命周期 |
|
|
// 生命周期 |
|
|
@ -104,19 +107,24 @@ 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}'); |
|
|
|
|
|
// 已挂断/停止时忽略原生层残留的状态事件 |
|
|
|
|
|
final sessionActive = _inCallMode || pttRecording.value; |
|
|
switch (event.type) { |
|
|
switch (event.type) { |
|
|
case AgentServiceEventType.recognitionStarted: |
|
|
case AgentServiceEventType.recognitionStarted: |
|
|
|
|
|
if (!sessionActive) break; |
|
|
status.value = MobileElfStatus.recording; |
|
|
status.value = MobileElfStatus.recording; |
|
|
realtimeText.value = ''; |
|
|
realtimeText.value = ''; |
|
|
micAlive.value = true; |
|
|
micAlive.value = true; |
|
|
break; |
|
|
break; |
|
|
|
|
|
|
|
|
case AgentServiceEventType.recognizing: |
|
|
case AgentServiceEventType.recognizing: |
|
|
|
|
|
if (!sessionActive) break; |
|
|
realtimeText.value = event.data['text']?.toString() ?? ''; |
|
|
realtimeText.value = event.data['text']?.toString() ?? ''; |
|
|
status.value = MobileElfStatus.recording; |
|
|
status.value = MobileElfStatus.recording; |
|
|
break; |
|
|
break; |
|
|
|
|
|
|
|
|
case AgentServiceEventType.recognitionResult: |
|
|
case AgentServiceEventType.recognitionResult: |
|
|
|
|
|
if (!sessionActive) break; |
|
|
final text = event.data['text']?.toString() ?? ''; |
|
|
final text = event.data['text']?.toString() ?? ''; |
|
|
realtimeText.value = ''; |
|
|
realtimeText.value = ''; |
|
|
micAlive.value = false; |
|
|
micAlive.value = false; |
|
|
@ -126,14 +134,15 @@ class MobileElfController extends GetxController { |
|
|
|
|
|
|
|
|
case AgentServiceEventType.recognitionStopped: |
|
|
case AgentServiceEventType.recognitionStopped: |
|
|
case AgentServiceEventType.recognitionCanceled: |
|
|
case AgentServiceEventType.recognitionCanceled: |
|
|
|
|
|
if (!sessionActive) break; |
|
|
realtimeText.value = ''; |
|
|
realtimeText.value = ''; |
|
|
micAlive.value = false; |
|
|
micAlive.value = false; |
|
|
break; |
|
|
break; |
|
|
|
|
|
|
|
|
case AgentServiceEventType.assistantResponse: |
|
|
case AgentServiceEventType.assistantResponse: |
|
|
|
|
|
if (!sessionActive) break; |
|
|
final text = event.data['response']?.toString() ?? ''; |
|
|
final text = event.data['response']?.toString() ?? ''; |
|
|
if (text.isNotEmpty) _updateOrAddAssistantMessage(text); |
|
|
if (text.isNotEmpty) _updateOrAddAssistantMessage(text); |
|
|
// 静音模式下收到回复后直接恢复状态(不等 TTS 事件) |
|
|
|
|
|
if (!ttsEnabled.value) { |
|
|
if (!ttsEnabled.value) { |
|
|
status.value = |
|
|
status.value = |
|
|
_inCallMode ? MobileElfStatus.recording : MobileElfStatus.idle; |
|
|
_inCallMode ? MobileElfStatus.recording : MobileElfStatus.idle; |
|
|
@ -142,12 +151,13 @@ class MobileElfController extends GetxController { |
|
|
break; |
|
|
break; |
|
|
|
|
|
|
|
|
case AgentServiceEventType.ttsStarted: |
|
|
case AgentServiceEventType.ttsStarted: |
|
|
|
|
|
if (!sessionActive) break; |
|
|
status.value = MobileElfStatus.speaking; |
|
|
status.value = MobileElfStatus.speaking; |
|
|
break; |
|
|
break; |
|
|
|
|
|
|
|
|
case AgentServiceEventType.ttsCompleted: |
|
|
case AgentServiceEventType.ttsCompleted: |
|
|
|
|
|
if (!sessionActive) break; |
|
|
if (_inCallMode) { |
|
|
if (_inCallMode) { |
|
|
// 通话模式:TTS 播完后自动回到录音等待状态 |
|
|
|
|
|
status.value = MobileElfStatus.recording; |
|
|
status.value = MobileElfStatus.recording; |
|
|
micAlive.value = true; |
|
|
micAlive.value = true; |
|
|
} else { |
|
|
} else { |
|
|
@ -158,11 +168,13 @@ class MobileElfController extends GetxController { |
|
|
|
|
|
|
|
|
case AgentServiceEventType.ttsCanceled: |
|
|
case AgentServiceEventType.ttsCanceled: |
|
|
case AgentServiceEventType.ttsStopped: |
|
|
case AgentServiceEventType.ttsStopped: |
|
|
|
|
|
if (!sessionActive) break; |
|
|
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; |
|
|
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; |
|
|
@ -185,18 +197,32 @@ class MobileElfController extends GetxController { |
|
|
// ═══════════════════════════════════════════════════════════════════════════ |
|
|
// ═══════════════════════════════════════════════════════════════════════════ |
|
|
|
|
|
|
|
|
Future<void> startCall() async { |
|
|
Future<void> startCall() async { |
|
|
if (inCall.value) return; |
|
|
if (inCall.value || _callStarting) return; |
|
|
|
|
|
_callStarting = true; |
|
|
|
|
|
try { |
|
|
|
|
|
final granted = await PermissionUtil.instance.requestPermission( |
|
|
|
|
|
permissionType: Permission.microphone, |
|
|
|
|
|
permissionName: '麦克风', |
|
|
|
|
|
explanationText: '移动精灵需要麦克风权限才能进行语音对话', |
|
|
|
|
|
permanentDenialText: '请在设置中开启麦克风权限以使用语音功能', |
|
|
|
|
|
); |
|
|
|
|
|
if (!granted || inCall.value) return; // 权限等待期间可能已被挂断或重复调用 |
|
|
_inCallMode = true; |
|
|
_inCallMode = true; |
|
|
inCall.value = true; |
|
|
inCall.value = true; |
|
|
callSeconds.value = 0; |
|
|
callSeconds.value = 0; |
|
|
|
|
|
micAlive.value = true; |
|
|
status.value = MobileElfStatus.connecting; |
|
|
status.value = MobileElfStatus.connecting; |
|
|
_callTimer?.cancel(); |
|
|
_callTimer?.cancel(); |
|
|
_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'); |
|
|
|
|
|
} finally { |
|
|
|
|
|
_callStarting = false; |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
Future<void> disconnect() async { |
|
|
Future<void> disconnect() async { |
|
|
|
|
|
_callStarting = false; |
|
|
_inCallMode = false; |
|
|
_inCallMode = false; |
|
|
inCall.value = false; |
|
|
inCall.value = false; |
|
|
_callTimer?.cancel(); |
|
|
_callTimer?.cancel(); |
|
|
@ -211,8 +237,15 @@ class MobileElfController extends GetxController { |
|
|
await AgentService.stopConversation(); |
|
|
await AgentService.stopConversation(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
void startPtt() { |
|
|
Future<void> startPtt() async { |
|
|
if (pttRecording.value) return; |
|
|
if (pttRecording.value) return; |
|
|
|
|
|
final granted = await PermissionUtil.instance.requestPermission( |
|
|
|
|
|
permissionType: Permission.microphone, |
|
|
|
|
|
permissionName: '麦克风', |
|
|
|
|
|
explanationText: '移动精灵需要麦克风权限才能进行语音对话', |
|
|
|
|
|
permanentDenialText: '请在设置中开启麦克风权限以使用语音功能', |
|
|
|
|
|
); |
|
|
|
|
|
if (!granted) return; |
|
|
pttRecording.value = true; |
|
|
pttRecording.value = true; |
|
|
pttSeconds.value = 0; |
|
|
pttSeconds.value = 0; |
|
|
status.value = MobileElfStatus.connecting; |
|
|
status.value = MobileElfStatus.connecting; |
|
|
|