|
|
|
@ -1,6 +1,8 @@ |
|
|
|
import 'dart:async'; |
|
|
|
import 'package:get/get.dart'; |
|
|
|
import 'package:flutter/widgets.dart'; |
|
|
|
import 'package:get_storage/get_storage.dart'; |
|
|
|
import '../../../data/services/speech_impl/voice_clone_tts_service.dart'; |
|
|
|
import '../models/message_model.dart'; |
|
|
|
import '../../../core/utils/logger.dart'; |
|
|
|
import '../../../data/services/volcano_ai_service.dart'; |
|
|
|
@ -15,10 +17,11 @@ import '../../../data/services/voice_interaction_service.dart'; |
|
|
|
class ChatController extends GetxController { |
|
|
|
// 服务 |
|
|
|
final VolcanoAIService _aiService = Get.find<VolcanoAIService>(); |
|
|
|
final TtsService _ttsService = Get.find<TtsService>(); |
|
|
|
late TtsService _ttsService; |
|
|
|
final AsrService _asrService = Get.find<AsrService>(); |
|
|
|
final ChatHistoryService _historyService = Get.find<ChatHistoryService>(); |
|
|
|
final VoiceInteractionService _voiceInteractionService = Get.find<VoiceInteractionService>(); |
|
|
|
final VoiceInteractionService _voiceInteractionService = |
|
|
|
Get.find<VoiceInteractionService>(); |
|
|
|
|
|
|
|
// 添加ScrollController用于控制列表滚动 |
|
|
|
final ScrollController scrollController = ScrollController(); |
|
|
|
@ -37,7 +40,7 @@ class ChatController extends GetxController { |
|
|
|
final RxBool isSending = false.obs; |
|
|
|
final RxBool isTtsEnabled = true.obs; |
|
|
|
final RxBool isVoiceRecognitionActive = false.obs; |
|
|
|
|
|
|
|
|
|
|
|
// 语音交互模式 |
|
|
|
bool _isVoiceInteractionMode = false; |
|
|
|
|
|
|
|
@ -48,7 +51,7 @@ class ChatController extends GetxController { |
|
|
|
|
|
|
|
// 临时消息索引,用于更新临时消息 |
|
|
|
int _tempMessageIndex = -1; |
|
|
|
|
|
|
|
|
|
|
|
// 防抖计时器 |
|
|
|
Timer? _scrollDebounceTimer; |
|
|
|
|
|
|
|
@ -59,10 +62,10 @@ class ChatController extends GetxController { |
|
|
|
|
|
|
|
// 取消标志 |
|
|
|
bool _shouldCancelAiResponse = false; |
|
|
|
|
|
|
|
|
|
|
|
// 自动停止语音识别计时器 |
|
|
|
Timer? _autoStopTimer; |
|
|
|
|
|
|
|
|
|
|
|
// 记录最后一次活动时间 |
|
|
|
DateTime _lastActivityTime = DateTime.now(); |
|
|
|
|
|
|
|
@ -70,11 +73,11 @@ class ChatController extends GetxController { |
|
|
|
Future<void> onInit() async { |
|
|
|
super.onInit(); |
|
|
|
Logger.info('ChatController onInit'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 从路由参数获取Agent信息 |
|
|
|
final Map<String, dynamic> args = Get.arguments ?? {}; |
|
|
|
final String agentId = args['agent'] ?? args['agentId'] ?? 'personal_assistant'; |
|
|
|
final String agentId = |
|
|
|
args['agent'] ?? args['agentId'] ?? 'personal_assistant'; |
|
|
|
|
|
|
|
// 获取Agent |
|
|
|
final Agent? foundAgent = AgentProvider.getAgentById(agentId); |
|
|
|
@ -85,10 +88,20 @@ class ChatController extends GetxController { |
|
|
|
} |
|
|
|
|
|
|
|
agent = foundAgent; |
|
|
|
|
|
|
|
|
|
|
|
final GetStorage storage = GetStorage(); |
|
|
|
String timbre = storage.read('timbre') ?? '1'; |
|
|
|
|
|
|
|
if (timbre == '1') { |
|
|
|
_ttsService = Get.find<TtsService>(); |
|
|
|
} else { |
|
|
|
Get.put(VoiceCloneTtsService()); |
|
|
|
_ttsService = Get.find<VoiceCloneTtsService>(); |
|
|
|
} |
|
|
|
|
|
|
|
_ttsService.initialize(); |
|
|
|
_asrService.initialize(); |
|
|
|
|
|
|
|
|
|
|
|
// 设置语音交互模式 |
|
|
|
_isVoiceInteractionMode = args['isVoiceInteraction'] == true; |
|
|
|
|
|
|
|
@ -130,7 +143,8 @@ class ChatController extends GetxController { |
|
|
|
/// 订阅VoiceInteractionService事件 |
|
|
|
void _subscribeToVoiceInteractionEvents() { |
|
|
|
if (_voiceInteractionService.eventStream != null) { |
|
|
|
_voiceInteractionSubscription = _voiceInteractionService.eventStream.listen((event) { |
|
|
|
_voiceInteractionSubscription = |
|
|
|
_voiceInteractionService.eventStream.listen((event) { |
|
|
|
if (event is RecognitionStartedEvent) { |
|
|
|
// 收到recognitionStarted事件时,停止当前的AI交互、录音和TTS |
|
|
|
_stopCurrentInteraction(); |
|
|
|
@ -145,26 +159,27 @@ class ChatController extends GetxController { |
|
|
|
/// 处理聊天历史事件 |
|
|
|
void _processChatHistoryEvent(ChatHistoryEvent event) { |
|
|
|
Logger.info('收到后台聊天历史事件: agentId=${event.agentId}'); |
|
|
|
|
|
|
|
|
|
|
|
// 如果消息不是空的且不是当前Agent的,不处理 |
|
|
|
if (event.userMessage.isEmpty || event.agentId != agent.id) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 添加用户消息 |
|
|
|
final userMsg = Message( |
|
|
|
role: 'user', |
|
|
|
content: event.userMessage, |
|
|
|
timestamp: DateTime.fromMillisecondsSinceEpoch(event.timestamp), |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
// 添加助手消息 |
|
|
|
final assistantMsg = Message( |
|
|
|
role: 'assistant', |
|
|
|
content: event.assistantMessage, |
|
|
|
timestamp: DateTime.fromMillisecondsSinceEpoch(event.timestamp + 1), // 助手消息稍后一毫秒 |
|
|
|
timestamp: |
|
|
|
DateTime.fromMillisecondsSinceEpoch(event.timestamp + 1), // 助手消息稍后一毫秒 |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
// 防止消息重复 |
|
|
|
if (!_isDuplicateMessage(userMsg) && !_isDuplicateMessage(assistantMsg)) { |
|
|
|
messages.add(userMsg); |
|
|
|
@ -176,15 +191,15 @@ class ChatController extends GetxController { |
|
|
|
/// 停止当前交互 |
|
|
|
void _stopCurrentInteraction() { |
|
|
|
Logger.info('收到后台语音识别开始事件,停止当前AI交互'); |
|
|
|
|
|
|
|
|
|
|
|
// 停止当前AI交互 |
|
|
|
if (isSending.value || _aiResponseSubscription != null) { |
|
|
|
_interruptCurrentResponse("收到后台语音识别开始事件,中断当前响应"); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 停止TTS |
|
|
|
_ttsService.stop(); |
|
|
|
|
|
|
|
|
|
|
|
// 停止语音识别 |
|
|
|
if (isVoiceRecognitionActive.value) { |
|
|
|
stopVoiceRecognition(); |
|
|
|
@ -194,7 +209,7 @@ class ChatController extends GetxController { |
|
|
|
@override |
|
|
|
void onClose() { |
|
|
|
Logger.info('ChatController onClose'); |
|
|
|
|
|
|
|
|
|
|
|
// 清理资源 |
|
|
|
scrollController.dispose(); |
|
|
|
_aiResponseSubscription?.cancel(); |
|
|
|
@ -202,10 +217,10 @@ class ChatController extends GetxController { |
|
|
|
_voiceInteractionSubscription?.cancel(); |
|
|
|
_autoStopTimer?.cancel(); |
|
|
|
_scrollDebounceTimer?.cancel(); // 清理防抖计时器 |
|
|
|
|
|
|
|
|
|
|
|
// 停止TTS |
|
|
|
_ttsService.stop(); |
|
|
|
|
|
|
|
|
|
|
|
if (isVoiceRecognitionActive.value) { |
|
|
|
_asrService.stopContinuousRecognition(); |
|
|
|
} |
|
|
|
@ -220,33 +235,32 @@ class ChatController extends GetxController { |
|
|
|
|
|
|
|
/// 滚动到列表底部方法,添加防抖功能 |
|
|
|
void _scrollToBottom() { |
|
|
|
|
|
|
|
if (scrollController.hasClients && messages.isNotEmpty) { |
|
|
|
try { |
|
|
|
scrollController.animateTo( |
|
|
|
scrollController.position.maxScrollExtent, |
|
|
|
duration: Duration(milliseconds: 300), |
|
|
|
curve: Curves.easeOut, |
|
|
|
); |
|
|
|
} catch (e) { |
|
|
|
Logger.error('滚动到底部失败', e); |
|
|
|
} |
|
|
|
if (scrollController.hasClients && messages.isNotEmpty) { |
|
|
|
try { |
|
|
|
scrollController.animateTo( |
|
|
|
scrollController.position.maxScrollExtent, |
|
|
|
duration: Duration(milliseconds: 300), |
|
|
|
curve: Curves.easeOut, |
|
|
|
); |
|
|
|
} catch (e) { |
|
|
|
Logger.error('滚动到底部失败', e); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 中断当前响应 |
|
|
|
void _interruptCurrentResponse(String reason) { |
|
|
|
Logger.info('${reason}'); |
|
|
|
|
|
|
|
|
|
|
|
// 停止TTS |
|
|
|
_ttsService.stop(); |
|
|
|
|
|
|
|
|
|
|
|
// 取消AI响应流订阅 |
|
|
|
if (_aiResponseSubscription != null) { |
|
|
|
_shouldCancelAiResponse = true; |
|
|
|
_aiResponseSubscription!.cancel(); |
|
|
|
_aiResponseSubscription = null; |
|
|
|
|
|
|
|
|
|
|
|
// 将加载中的消息转换为正常消息,保留已输出的内容 |
|
|
|
final index = messages.indexWhere((m) => m.isLoading); |
|
|
|
if (index != -1) { |
|
|
|
@ -255,7 +269,7 @@ class ChatController extends GetxController { |
|
|
|
); |
|
|
|
messages.refresh(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 重置发送状态 |
|
|
|
isSending.value = false; |
|
|
|
} |
|
|
|
@ -278,14 +292,13 @@ class ChatController extends GetxController { |
|
|
|
); |
|
|
|
messages.add(userMessage); |
|
|
|
|
|
|
|
|
|
|
|
// 清空输入框 |
|
|
|
inputText.value = ''; |
|
|
|
|
|
|
|
// 添加加载中的消息 |
|
|
|
final loadingMessage = Message.loading(); |
|
|
|
messages.add(loadingMessage); |
|
|
|
|
|
|
|
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) { |
|
|
|
_scrollToBottom(); |
|
|
|
}); |
|
|
|
@ -298,7 +311,7 @@ class ChatController extends GetxController { |
|
|
|
|
|
|
|
// 重置取消标志 |
|
|
|
_shouldCancelAiResponse = false; |
|
|
|
|
|
|
|
|
|
|
|
String fullResponse = ''; |
|
|
|
|
|
|
|
// 获取AI响应流 |
|
|
|
@ -327,12 +340,12 @@ class ChatController extends GetxController { |
|
|
|
|
|
|
|
// 完成响应,将isLoading设为false |
|
|
|
_finalizeResponse(fullResponse); |
|
|
|
|
|
|
|
|
|
|
|
// 如果TTS启用,处理缓冲区中的剩余文本 |
|
|
|
if (isTtsEnabled.value) { |
|
|
|
_ttsService.flushStream(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 保存聊天历史 |
|
|
|
_saveChatHistory(); |
|
|
|
}, |
|
|
|
@ -345,7 +358,7 @@ class ChatController extends GetxController { |
|
|
|
); |
|
|
|
} catch (e) { |
|
|
|
Logger.error('发送消息失败', e); |
|
|
|
|
|
|
|
|
|
|
|
_handleResponseError(); |
|
|
|
} |
|
|
|
} |
|
|
|
@ -353,10 +366,11 @@ class ChatController extends GetxController { |
|
|
|
// 构建消息历史 |
|
|
|
List<Map<String, String>> _buildMessageHistory() { |
|
|
|
final List<Map<String, String>> messageHistory = []; |
|
|
|
|
|
|
|
|
|
|
|
// 只取最近的10条消息作为上下文 |
|
|
|
final recentMessages = messages.where((m) => !m.isLoading).toList(); |
|
|
|
final startIndex = recentMessages.length > 10 ? recentMessages.length - 10 : 0; |
|
|
|
final startIndex = |
|
|
|
recentMessages.length > 10 ? recentMessages.length - 10 : 0; |
|
|
|
|
|
|
|
for (int i = startIndex; i < recentMessages.length; i++) { |
|
|
|
final m = recentMessages[i]; |
|
|
|
@ -365,7 +379,7 @@ class ChatController extends GetxController { |
|
|
|
'content': m.content, |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return messageHistory; |
|
|
|
} |
|
|
|
|
|
|
|
@ -393,7 +407,7 @@ class ChatController extends GetxController { |
|
|
|
messages.refresh(); |
|
|
|
_scrollToBottom(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
isSending.value = false; |
|
|
|
_aiResponseSubscription = null; |
|
|
|
} |
|
|
|
@ -408,7 +422,7 @@ class ChatController extends GetxController { |
|
|
|
); |
|
|
|
messages.refresh(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
isSending.value = false; |
|
|
|
_aiResponseSubscription = null; |
|
|
|
} |
|
|
|
@ -429,18 +443,17 @@ class ChatController extends GetxController { |
|
|
|
|
|
|
|
_voiceInteractionService.pauseVoiceInteraction(); |
|
|
|
try { |
|
|
|
|
|
|
|
// 启动连续语音识别 |
|
|
|
// 注意:在UI更新后,使用模拟波形动画替代实际麦克风输入的波形数据 |
|
|
|
final recognitionStream = await _asrService.startContinuousRecognition(); |
|
|
|
|
|
|
|
|
|
|
|
isVoiceRecognitionActive.value = true; |
|
|
|
isListening.value = true; |
|
|
|
recognizedText.value = ''; |
|
|
|
|
|
|
|
|
|
|
|
// 重置临时消息索引 |
|
|
|
_tempMessageIndex = -1; |
|
|
|
|
|
|
|
|
|
|
|
// 启动自动停止计时器 |
|
|
|
_startAutoStopTimer(); |
|
|
|
|
|
|
|
@ -460,7 +473,7 @@ class ChatController extends GetxController { |
|
|
|
void _handleRecognitionEvent(RecognitionEvent event) { |
|
|
|
// 更新活动时间 |
|
|
|
_updateActivityTime(); |
|
|
|
|
|
|
|
|
|
|
|
switch (event.type) { |
|
|
|
case RecognitionEventType.intermediateResult: |
|
|
|
_handleRecognizing(event.text); |
|
|
|
@ -486,24 +499,24 @@ class ChatController extends GetxController { |
|
|
|
void _startAutoStopTimer() { |
|
|
|
// 取消现有计时器 |
|
|
|
_autoStopTimer?.cancel(); |
|
|
|
|
|
|
|
|
|
|
|
// 初始化最后活动时间 |
|
|
|
_updateActivityTime(); |
|
|
|
|
|
|
|
|
|
|
|
// 根据模式设置超时时间 |
|
|
|
final int timeoutSeconds = _isVoiceInteractionMode ? 8 : 15; |
|
|
|
|
|
|
|
|
|
|
|
// 创建新的计时器,每秒检查一次 |
|
|
|
_autoStopTimer = Timer.periodic(Duration(seconds: 1), (timer) { |
|
|
|
// 检查是否有活动 |
|
|
|
bool isActive = _ttsService.isSpeaking || isSpeechDetected.value; |
|
|
|
|
|
|
|
|
|
|
|
// 如果有活动,更新时间并继续等待 |
|
|
|
if (isActive) { |
|
|
|
_updateActivityTime(); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 检查空闲时间 |
|
|
|
int idleSeconds = DateTime.now().difference(_lastActivityTime).inSeconds; |
|
|
|
if (idleSeconds >= timeoutSeconds) { |
|
|
|
@ -516,17 +529,17 @@ class ChatController extends GetxController { |
|
|
|
// 处理语音识别中的状态 |
|
|
|
void _handleRecognizing(String text) { |
|
|
|
if (text.isEmpty) return; |
|
|
|
|
|
|
|
|
|
|
|
recognizedText.value = text; |
|
|
|
|
|
|
|
|
|
|
|
// 设置语音检测状态为true,用于控制波形动画 |
|
|
|
if (!isSpeechDetected.value) { |
|
|
|
isSpeechDetected.value = true; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 更新活动时间 |
|
|
|
_updateActivityTime(); |
|
|
|
|
|
|
|
|
|
|
|
// 用户开始讲话时,立即停止当前TTS和中断AI响应 |
|
|
|
if (isSending.value || _aiResponseSubscription != null) { |
|
|
|
_interruptCurrentResponse("检测到用户开始讲话,中断当前响应"); |
|
|
|
@ -558,10 +571,10 @@ class ChatController extends GetxController { |
|
|
|
// 处理语音识别最终结果 |
|
|
|
void _handleFinalResult(String text) { |
|
|
|
if (text.isEmpty) return; |
|
|
|
|
|
|
|
|
|
|
|
Logger.info('处理语音识别最终结果: $text'); |
|
|
|
recognizedText.value = text; |
|
|
|
|
|
|
|
|
|
|
|
// 更新活动时间 |
|
|
|
_updateActivityTime(); |
|
|
|
|
|
|
|
@ -569,7 +582,7 @@ class ChatController extends GetxController { |
|
|
|
if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) { |
|
|
|
messages.removeAt(_tempMessageIndex); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 重置临时消息索引 |
|
|
|
_tempMessageIndex = -1; |
|
|
|
|
|
|
|
@ -578,7 +591,7 @@ class ChatController extends GetxController { |
|
|
|
|
|
|
|
// 重置识别文本,但不关闭语音识别框 |
|
|
|
recognizedText.value = ''; |
|
|
|
|
|
|
|
|
|
|
|
// 重置语音检测状态 |
|
|
|
isSpeechDetected.value = false; |
|
|
|
} |
|
|
|
@ -586,14 +599,14 @@ class ChatController extends GetxController { |
|
|
|
// 处理语音识别事件错误 |
|
|
|
void _handleRecognitionEventError(String? error) { |
|
|
|
Logger.error('语音识别错误: ${error ?? "未知错误"}'); |
|
|
|
|
|
|
|
|
|
|
|
_cleanupRecognition(); |
|
|
|
} |
|
|
|
|
|
|
|
// 处理语音识别流错误 |
|
|
|
void _handleRecognitionError(dynamic error) { |
|
|
|
Logger.error('语音识别错误', error); |
|
|
|
|
|
|
|
|
|
|
|
_cleanupRecognition(); |
|
|
|
} |
|
|
|
|
|
|
|
@ -605,10 +618,10 @@ class ChatController extends GetxController { |
|
|
|
messages.removeAt(_tempMessageIndex); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 重置临时消息索引 |
|
|
|
_tempMessageIndex = -1; |
|
|
|
|
|
|
|
|
|
|
|
// 停止语音识别 |
|
|
|
isVoiceRecognitionActive.value = false; |
|
|
|
isListening.value = false; |
|
|
|
@ -636,7 +649,7 @@ class ChatController extends GetxController { |
|
|
|
messages.removeAt(_tempMessageIndex); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 重置临时消息索引 |
|
|
|
_tempMessageIndex = -1; |
|
|
|
} catch (e) { |
|
|
|
@ -691,10 +704,12 @@ class ChatController extends GetxController { |
|
|
|
|
|
|
|
/// 检查是否为重复消息 |
|
|
|
bool _isDuplicateMessage(Message message) { |
|
|
|
return messages.any((m) => |
|
|
|
m.role == message.role && |
|
|
|
m.content == message.content && |
|
|
|
(m.timestamp.millisecondsSinceEpoch - message.timestamp.millisecondsSinceEpoch).abs() < 5000 |
|
|
|
); |
|
|
|
return messages.any((m) => |
|
|
|
m.role == message.role && |
|
|
|
m.content == message.content && |
|
|
|
(m.timestamp.millisecondsSinceEpoch - |
|
|
|
message.timestamp.millisecondsSinceEpoch) |
|
|
|
.abs() < |
|
|
|
5000); |
|
|
|
} |
|
|
|
} |
|
|
|
|