You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1008 lines
29 KiB
1008 lines
29 KiB
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'package:amap_flutter/amap_flutter.dart';
|
|
import '../../../data/services/spotify_service.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:get_storage/get_storage.dart';
|
|
import 'package:haptic_feedback/haptic_feedback.dart';
|
|
import 'package:just_audio/just_audio.dart';
|
|
import 'package:chat_storage/chat_storage.dart';
|
|
import 'package:path/path.dart';
|
|
import '../../../data/services/ai_service.dart';
|
|
import '../../../data/services/deapsound_ai_service.dart';
|
|
import '../../../data/services/kouzi_ai_service.dart';
|
|
import '../../../data/services/speech_impl/voice_clone_tts_service.dart';
|
|
import '../models/message_model.dart';
|
|
import '../../../core/utils/logger.dart';
|
|
import '../../../data/services/tts_service.dart';
|
|
import '../../../data/providers/agent_provider.dart';
|
|
import '../../../data/models/agent.dart';
|
|
import '../../../data/services/asr_service.dart';
|
|
import '../../../data/services/open_ai_service_adapter.dart';
|
|
|
|
class ChatController extends GetxController {
|
|
// 服务
|
|
late AiService _aiService;
|
|
late TtsService _ttsService;
|
|
final AsrService _asrService = Get.find<AsrService>();
|
|
|
|
late SpotifService _spotifService;
|
|
|
|
// 添加ScrollController用于控制列表滚动
|
|
final ScrollController scrollController = ScrollController();
|
|
|
|
final _amapFlutter = AmapFlutter();
|
|
StreamSubscription? _locationStream;
|
|
String _latLng = '';
|
|
String _city = '深圳市';
|
|
|
|
// Agent 信息
|
|
late final Agent agent;
|
|
|
|
// 消息列表
|
|
final RxList<Message> messages = <Message>[].obs;
|
|
|
|
// 输入控制
|
|
final RxString inputText = ''.obs;
|
|
|
|
// 状态控制
|
|
final RxBool isLoading = false.obs;
|
|
final RxBool isSending = false.obs;
|
|
final RxBool isTtsEnabled = true.obs;
|
|
final RxBool isVoiceRecognitionActive = false.obs;
|
|
|
|
// 分页加载状态
|
|
final RxInt _currentPage = 1.obs;
|
|
final RxInt _totalPages = 1.obs;
|
|
final RxBool _isLoadingMore = false.obs;
|
|
final RxBool _hasMoreHistory = true.obs;
|
|
static const int _pageSize = 20; // 每页加载的消息数量
|
|
|
|
// 语音交互模式
|
|
bool _isVoiceInteractionMode = false;
|
|
|
|
// 语音识别状态
|
|
final RxString recognizedText = ''.obs;
|
|
final RxBool isListening = false.obs;
|
|
final RxBool isSpeechDetected = false.obs;
|
|
|
|
// 防抖计时器
|
|
Timer? _scrollDebounceTimer;
|
|
|
|
// 流式响应控制
|
|
StreamSubscription? _aiResponseSubscription;
|
|
StreamSubscription? _recognitionSubscription;
|
|
StreamSubscription? _voiceInteractionSubscription;
|
|
|
|
// 取消标志
|
|
bool _shouldCancelAiResponse = false;
|
|
|
|
// 自动停止语音识别计时器
|
|
Timer? _autoStopTimer;
|
|
|
|
// 记录最后一次活动时间
|
|
DateTime _lastActivityTime = DateTime.now();
|
|
|
|
// 音乐播放
|
|
final player = AudioPlayer();
|
|
final RxString audiourl = ''.obs;
|
|
final RxBool isPlay = false.obs;
|
|
|
|
// 按住说话、文字输入切换
|
|
final RxBool isTextInput = false.obs;
|
|
final RxBool isHoldSpeak = false.obs;
|
|
String finalResult = '';
|
|
|
|
// 按住说话防抖
|
|
bool isDebounce = false;
|
|
Timer? _talkDebounceTimer;
|
|
Timer? _talkStartDebounceTimer;
|
|
|
|
@override
|
|
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';
|
|
|
|
// 获取Agent
|
|
final Agent? foundAgent = AgentProvider.getAgentById(agentId);
|
|
if (foundAgent == null) {
|
|
Get.back();
|
|
Logger.error('无法找到Agent: $agentId');
|
|
|
|
return;
|
|
}
|
|
|
|
agent = foundAgent;
|
|
|
|
// 根据Agent ID选择不同的AI服务
|
|
switch (agent.id) {
|
|
case 'cyber_girlfriend':
|
|
// 亲子陪伴
|
|
Get.put(KouziAIService());
|
|
_aiService = Get.find<KouziAIService>();
|
|
break;
|
|
case 'health_advisor':
|
|
// 多功能AI聊天
|
|
Get.put(DeapsoundAIService());
|
|
_aiService = Get.find<DeapsoundAIService>();
|
|
break;
|
|
default:
|
|
// 默认使用OpenAIServiceAdapter
|
|
_aiService = Get.find<OpenAIServiceAdapter>();
|
|
Logger.info('使用OpenAIServiceAdapter');
|
|
}
|
|
Get.put(SpotifService());
|
|
_spotifService = Get.find<SpotifService>();
|
|
// 使用克隆音色语音合成
|
|
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;
|
|
|
|
// 设置TTS服务
|
|
await _ttsService.setVoice(agent.voice);
|
|
|
|
// 添加滚动监听器,用于触发加载更多
|
|
scrollController.addListener(_scrollListener);
|
|
|
|
// 加载聊天历史 (加载第一页)
|
|
await _loadChatHistory();
|
|
|
|
// 如果没有历史记录,添加欢迎消息
|
|
if (messages.isEmpty && agent.welcomeMessage.isNotEmpty) {
|
|
messages.add(Message(
|
|
role: 'assistant',
|
|
content: agent.welcomeMessage,
|
|
timestamp: DateTime.now(),
|
|
));
|
|
// 如果启用了TTS,播放欢迎消息
|
|
// if (isTtsEnabled.value) {
|
|
// _ttsService.speak(agent.welcomeMessage);
|
|
// }
|
|
}
|
|
|
|
// 如果isVoiceInteraction为true,自动激活语音识别
|
|
if (args['isVoiceInteraction'] == true) {
|
|
_ttsService.speakOnce(agent.voiceWelcomeMessage ?? agent.welcomeMessage);
|
|
startVoiceRecognition();
|
|
// 自动停止计时器会在startVoiceRecognition中启动
|
|
}
|
|
|
|
|
|
_getLocation();
|
|
|
|
player.playerStateStream.listen((state) async {
|
|
if (state.processingState == ProcessingState.completed) {
|
|
await player.seek(Duration.zero);
|
|
player.pause();
|
|
isPlay.value = false;
|
|
}
|
|
});
|
|
|
|
// 使用微任务在布局完成后自动滚动到底部
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_scrollToBottom();
|
|
});
|
|
}
|
|
|
|
void _getLocation() async {
|
|
await _amapFlutter.initLocation();
|
|
_locationStream = _amapFlutter.locationStream().listen((data) async {
|
|
_latLng = '${data['longitude']},${data['latitude']}}';
|
|
_city = data['city'];
|
|
_amapFlutter.stopLocation();
|
|
});
|
|
_amapFlutter.startLocation();
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
void onClose() {
|
|
Logger.info('ChatController onClose');
|
|
|
|
// 清理资源
|
|
scrollController.removeListener(_scrollListener); // 移除监听器
|
|
scrollController.dispose();
|
|
_aiResponseSubscription?.cancel();
|
|
_recognitionSubscription?.cancel();
|
|
_voiceInteractionSubscription?.cancel();
|
|
_autoStopTimer?.cancel();
|
|
_scrollDebounceTimer?.cancel(); // 清理防抖计时器
|
|
_talkDebounceTimer?.cancel();
|
|
_talkStartDebounceTimer?.cancel();
|
|
|
|
_amapFlutter.dispose();
|
|
_locationStream?.cancel();
|
|
|
|
// 停止TTS
|
|
_ttsService.stop();
|
|
|
|
if (isVoiceRecognitionActive.value) {
|
|
_asrService.stopContinuousRecognition();
|
|
}
|
|
|
|
// 保存当前聊天记录
|
|
if (agent.id != null) {
|
|
// _historyService.saveHistory(agent.id!, messages.toList());
|
|
}
|
|
|
|
player.dispose();
|
|
|
|
super.onClose();
|
|
}
|
|
|
|
/// 滚动到列表底部方法,添加防抖功能
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 中断当前响应
|
|
void _interruptCurrentResponse(String reason) {
|
|
Logger.info('${reason}');
|
|
|
|
// 停止TTS
|
|
_ttsService.stop();
|
|
|
|
// 取消AI响应流订阅
|
|
if (_aiResponseSubscription != null) {
|
|
_shouldCancelAiResponse = true;
|
|
_aiResponseSubscription!.cancel();
|
|
_aiResponseSubscription = null;
|
|
|
|
// 将AI中间消息转换为正常消息,保留已输出的内容
|
|
final index =
|
|
messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant');
|
|
if (index != -1) {
|
|
messages[index] = messages[index].copyWith(
|
|
isIntermediate: false,
|
|
);
|
|
messages.refresh();
|
|
}
|
|
|
|
// 重置发送状态
|
|
isSending.value = false;
|
|
}
|
|
}
|
|
|
|
// 发送消息
|
|
Future<void> sendMessage(String text) async {
|
|
if (text.trim().isEmpty) return;
|
|
|
|
|
|
// 中断当前响应
|
|
_interruptCurrentResponse("发送新消息,中断当前响应");
|
|
|
|
// 添加用户消息
|
|
final userMessage = Message(
|
|
role: 'user',
|
|
content: text,
|
|
timestamp: DateTime.now(),
|
|
);
|
|
messages.add(userMessage);
|
|
_saveMessageToDb(userMessage);
|
|
|
|
// 清空输入框
|
|
inputText.value = '';
|
|
|
|
// 添加中间状态的AI消息
|
|
final intermediateMessage = Message.intermediate('assistant');
|
|
messages.add(intermediateMessage);
|
|
messages.refresh();
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_scrollToBottom();
|
|
});
|
|
|
|
isSending.value = true;
|
|
|
|
try {
|
|
// 构建消息历史
|
|
final List<Map<String, String>> messageHistory = _buildMessageHistory();
|
|
|
|
// 重置取消标志
|
|
_shouldCancelAiResponse = false;
|
|
|
|
String fullResponse = '';
|
|
|
|
// 获取AI响应流
|
|
final responseStream = _aiService.sendMessageStream(
|
|
messages: messageHistory,
|
|
systemPrompt: agent.systemPrompt,
|
|
);
|
|
Logger.info(
|
|
'liwei----------- 发送用户提问 ${text}',
|
|
);
|
|
// 订阅响应流
|
|
_aiResponseSubscription = responseStream.listen(
|
|
(chunk) {
|
|
if (_shouldCancelAiResponse) return;
|
|
Logger.info('chat ${chunk}');
|
|
if (chunk is Map) {
|
|
Logger.info(
|
|
'liwei-----------收到卡片消息 ${chunk}',
|
|
);
|
|
_updateIntermediateMessage(meta: chunk);
|
|
if (chunk['card_spotify'] != null) {
|
|
play_spotify(chunk['card_spotify']);
|
|
}
|
|
if (chunk['card_music'] != null) {
|
|
play_music(chunk['card_music']);
|
|
}
|
|
} else if (chunk is String) {
|
|
fullResponse += chunk;
|
|
|
|
// 更新中间状态的消息内容
|
|
_updateIntermediateMessage(content: fullResponse);
|
|
|
|
// 如果TTS启用,使用流式处理发送到TTS
|
|
if (isTtsEnabled.value) {
|
|
String str = chunk.replaceAll('*', '');
|
|
_ttsService.speakStream(str);
|
|
}
|
|
}
|
|
},
|
|
onDone: () {
|
|
Logger.info(
|
|
'liwei-----------结束当前问题 ${text}',
|
|
);
|
|
if (_shouldCancelAiResponse) return;
|
|
|
|
// 完成响应,将isIntermediate设为false
|
|
_finalizeResponse(fullResponse);
|
|
|
|
// 如果TTS启用,处理缓冲区中的剩余文本
|
|
if (isTtsEnabled.value) {
|
|
_ttsService.flushStream();
|
|
}
|
|
},
|
|
onError: (error) {
|
|
Logger.error('AI响应错误', error);
|
|
|
|
// 更新中间状态的消息为错误消息
|
|
_handleResponseError();
|
|
},
|
|
);
|
|
} catch (e) {
|
|
Logger.error('发送消息失败', e);
|
|
|
|
_handleResponseError();
|
|
}
|
|
}
|
|
|
|
// 构建消息历史
|
|
List<Map<String, String>> _buildMessageHistory() {
|
|
final List<Map<String, String>> messageHistory = [];
|
|
|
|
// 只取最近的10条消息作为上下文
|
|
final recentMessages = messages.where((m) => !m.isIntermediate).toList();
|
|
final startIndex =
|
|
recentMessages.length > 10 ? recentMessages.length - 10 : 0;
|
|
messageHistory.add({
|
|
'role': 'user',
|
|
'content': '我当前所在城市是$_city,我的经纬度是$_latLng',
|
|
});
|
|
|
|
for (int i = startIndex; i < recentMessages.length; i++) {
|
|
final m = recentMessages[i];
|
|
messageHistory.add({
|
|
'role': m.role,
|
|
'content': m.content,
|
|
});
|
|
}
|
|
|
|
return messageHistory;
|
|
}
|
|
|
|
// 更新中间状态的消息
|
|
void _updateIntermediateMessage({Map? meta, String? content}) {
|
|
final index =
|
|
messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant');
|
|
if (index != -1) {
|
|
if (meta != null) {
|
|
Map messagesData = messages[index].meta ?? {};
|
|
messages[index] = messages[index].copyWith(
|
|
meta: {...messagesData, ...meta},
|
|
);
|
|
}
|
|
messages[index] = messages[index].copyWith(
|
|
content: content,
|
|
);
|
|
messages.refresh();
|
|
_scrollToBottom();
|
|
}
|
|
}
|
|
|
|
// 播放音乐
|
|
void play_music(Map card) async {
|
|
try {
|
|
isPlay.value = false;
|
|
if (audiourl.value != card['url']) {
|
|
audiourl.value = '';
|
|
await player.setUrl(card['url']);
|
|
audiourl.value = card['url'];
|
|
player.play();
|
|
} else {
|
|
player.play();
|
|
}
|
|
isPlay.value = true;
|
|
} catch (e) {
|
|
Logger.error('加载异常: $e');
|
|
}
|
|
}
|
|
|
|
// 播放音乐
|
|
void pause_music() async {
|
|
await player.pause();
|
|
isPlay.value = false;
|
|
}
|
|
|
|
// 播放音乐
|
|
void play_spotify(Map card) async {
|
|
try {
|
|
await _spotifService.initialize();
|
|
_spotifService.play(card['url'] as String);
|
|
isPlay.value = true;
|
|
audiourl.value = card['url'] as String;
|
|
} catch (e) {
|
|
Logger.error('加载异常: $e');
|
|
}
|
|
}
|
|
|
|
// 暂停播放音乐
|
|
void pause_spotify() async {
|
|
try {
|
|
_spotifService.pause();
|
|
isPlay.value = false;
|
|
} catch (e) {
|
|
Logger.error('加载异常: $e');
|
|
}
|
|
}
|
|
|
|
// 更新或添加最终响应
|
|
void _finalizeResponse(String content) {
|
|
// 查找AI的中间状态消息
|
|
final index =
|
|
messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant');
|
|
if (index != -1) {
|
|
// 更新中间状态消息为最终状态
|
|
messages[index] = messages[index].copyWith(
|
|
content: content,
|
|
isIntermediate: false,
|
|
timestamp: DateTime.now(), // 更新时间戳为最终时间
|
|
);
|
|
// 保存最终的助手消息到数据库
|
|
_saveMessageToDb(messages[index]);
|
|
} else {
|
|
// 如果没有中间状态消息,直接添加新消息 (理论上不应发生)
|
|
final assistantMsg = Message(
|
|
role: 'assistant',
|
|
content: content,
|
|
timestamp: DateTime.now(),
|
|
);
|
|
messages.add(assistantMsg);
|
|
_saveMessageToDb(assistantMsg); // 保存到数据库
|
|
}
|
|
|
|
isLoading.value = false;
|
|
isSending.value = false;
|
|
_aiResponseSubscription = null; // 清理订阅
|
|
_shouldCancelAiResponse = false; // 重置取消标志
|
|
|
|
// 响应完成后,重新启动语音识别的自动停止计时器
|
|
if (_isVoiceInteractionMode) {
|
|
_startAutoStopTimer();
|
|
}
|
|
}
|
|
|
|
// 处理响应错误
|
|
void _handleResponseError() {
|
|
// 查找AI的中间状态消息
|
|
final index =
|
|
messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant');
|
|
if (index != -1) {
|
|
messages[index] = messages[index].copyWith(
|
|
content: '抱歉,我遇到了一些问题,无法回应您的请求。',
|
|
isIntermediate: false,
|
|
);
|
|
messages.refresh();
|
|
}
|
|
|
|
isSending.value = false;
|
|
_aiResponseSubscription = null;
|
|
}
|
|
|
|
// 切换TTS状态
|
|
void toggleTts() {
|
|
isTtsEnabled.toggle();
|
|
|
|
// 如果关闭TTS,停止当前播放
|
|
if (!isTtsEnabled.value) {
|
|
_ttsService.stop();
|
|
}
|
|
}
|
|
|
|
// 按下开始说话
|
|
void talkLongPress() async {
|
|
_talkStartDebounceTimer?.cancel();
|
|
_talkDebounceTimer?.cancel();
|
|
isHoldSpeak.value = true;
|
|
Haptics.vibrate(HapticsType.success);
|
|
|
|
_talkStartDebounceTimer =
|
|
Timer(const Duration(milliseconds: 260), () async {
|
|
if (isDebounce) return;
|
|
|
|
|
|
// 中断当前响应
|
|
_interruptCurrentResponse("发送新消息,中断当前响应");
|
|
try {
|
|
final recognitionStream =
|
|
await _asrService.startContinuousRecognition();
|
|
await Future.delayed(const Duration(milliseconds: 260));
|
|
isDebounce = true;
|
|
recognizedText.value = '';
|
|
finalResult = '';
|
|
|
|
// 监听识别事件
|
|
_recognitionSubscription = recognitionStream.listen(
|
|
_handleRecognitionEvent,
|
|
onError: _handleRecognitionError,
|
|
);
|
|
} catch (e) {
|
|
Logger.error('启动语音识别失败', e);
|
|
isHoldSpeak.value = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
// 松开停止说话
|
|
void talkLongPressUp() async {
|
|
_talkStartDebounceTimer?.cancel();
|
|
_talkDebounceTimer?.cancel();
|
|
isHoldSpeak.value = false;
|
|
|
|
_talkDebounceTimer = Timer(const Duration(milliseconds: 300), () async {
|
|
if (!isDebounce) return;
|
|
try {
|
|
await _asrService.stopContinuousRecognition();
|
|
_recognitionSubscription?.cancel();
|
|
_recognitionSubscription = null;
|
|
|
|
String sendText = '';
|
|
|
|
final intermediateIndex =
|
|
messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
|
|
if (intermediateIndex >= 0) {
|
|
sendText = messages[intermediateIndex].content;
|
|
messages.removeAt(intermediateIndex);
|
|
}
|
|
|
|
sendMessage(sendText);
|
|
|
|
recognizedText.value = '';
|
|
finalResult = '';
|
|
|
|
isDebounce = false;
|
|
} catch (e) {
|
|
Logger.error('停止语音识别失败', e);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 开始语音识别
|
|
Future<void> startVoiceRecognition() async {
|
|
if (isVoiceRecognitionActive.value) return;
|
|
|
|
|
|
try {
|
|
// 启动连续语音识别
|
|
// 注意:在UI更新后,使用模拟波形动画替代实际麦克风输入的波形数据
|
|
final recognitionStream = await _asrService.startContinuousRecognition();
|
|
|
|
isVoiceRecognitionActive.value = true;
|
|
isListening.value = true;
|
|
recognizedText.value = '';
|
|
|
|
// 启动自动停止计时器
|
|
_startAutoStopTimer();
|
|
|
|
// 监听识别事件
|
|
_recognitionSubscription = recognitionStream.listen(
|
|
_handleRecognitionEvent,
|
|
onError: _handleRecognitionError,
|
|
);
|
|
} catch (e) {
|
|
Logger.error('启动语音识别失败', e);
|
|
isVoiceRecognitionActive.value = false;
|
|
isListening.value = false;
|
|
}
|
|
}
|
|
|
|
// 处理语音识别事件
|
|
void _handleRecognitionEvent(RecognitionEvent event) {
|
|
// 更新活动时间
|
|
_updateActivityTime();
|
|
|
|
switch (event.type) {
|
|
case RecognitionEventType.intermediateResult:
|
|
_handleRecognizing(event.text);
|
|
break;
|
|
case RecognitionEventType.finalResult:
|
|
_handleFinalResult(event.text);
|
|
break;
|
|
case RecognitionEventType.error:
|
|
_handleRecognitionEventError(event.error);
|
|
break;
|
|
default:
|
|
// 不做任何处理
|
|
break;
|
|
}
|
|
}
|
|
|
|
// 更新最后活动时间
|
|
void _updateActivityTime() {
|
|
_lastActivityTime = DateTime.now();
|
|
}
|
|
|
|
// 启动自动停止计时器
|
|
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) {
|
|
timer.cancel();
|
|
_stopVoiceRecognitionAutomatically();
|
|
}
|
|
});
|
|
}
|
|
|
|
// 处理语音识别中的状态
|
|
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("检测到用户开始讲话,中断当前响应");
|
|
}
|
|
|
|
// 查找或创建用户的中间状态消息
|
|
final intermediateIndex =
|
|
messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
|
|
|
|
if (intermediateIndex >= 0) {
|
|
// 更新已有的中间状态消息
|
|
messages[intermediateIndex] = messages[intermediateIndex].copyWith(
|
|
content: isVoiceRecognitionActive.value ? text : finalResult + text,
|
|
);
|
|
messages.refresh();
|
|
} else {
|
|
// 添加新的中间状态消息
|
|
var intermediateMessage = Message.intermediate('user');
|
|
intermediateMessage = intermediateMessage.copyWith(content: text);
|
|
messages.add(intermediateMessage);
|
|
messages.refresh();
|
|
}
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_scrollToBottom();
|
|
});
|
|
}
|
|
|
|
// 处理语音识别最终结果
|
|
void _handleFinalResult(String text) {
|
|
if (text.isEmpty) return;
|
|
|
|
Logger.info('处理语音识别最终结果: $text');
|
|
|
|
// 更新活动时间
|
|
_updateActivityTime();
|
|
|
|
if (isVoiceRecognitionActive.value) {
|
|
// 一直语音说话
|
|
|
|
recognizedText.value = text;
|
|
// 查找并移除用户的中间状态消息
|
|
final intermediateIndex =
|
|
messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
|
|
if (intermediateIndex >= 0) {
|
|
messages.removeAt(intermediateIndex);
|
|
}
|
|
|
|
// 发送识别到的文本
|
|
sendMessage(text);
|
|
// 重置识别文本,但不关闭语音识别框
|
|
recognizedText.value = '';
|
|
// 重置语音检测状态
|
|
isSpeechDetected.value = false;
|
|
} else {
|
|
// 按住说话
|
|
|
|
finalResult += text;
|
|
recognizedText.value = finalResult;
|
|
|
|
final intermediateIndex =
|
|
messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
|
|
|
|
if (intermediateIndex >= 0) {
|
|
// 更新已有的中间状态消息
|
|
messages[intermediateIndex] = messages[intermediateIndex].copyWith(
|
|
content: finalResult,
|
|
);
|
|
messages.refresh();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 处理语音识别事件错误
|
|
void _handleRecognitionEventError(String? error) {
|
|
Logger.error('语音识别错误: ${error ?? "未知错误"}');
|
|
|
|
_cleanupRecognition();
|
|
}
|
|
|
|
// 处理语音识别流错误
|
|
void _handleRecognitionError(dynamic error) {
|
|
Logger.error('语音识别错误', error);
|
|
|
|
_cleanupRecognition();
|
|
}
|
|
|
|
// 清理语音识别资源
|
|
void _cleanupRecognition() {
|
|
// 查找并移除用户的中间状态消息
|
|
final intermediateIndex =
|
|
messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
|
|
if (intermediateIndex >= 0) {
|
|
if (messages[intermediateIndex].content.trim().isEmpty) {
|
|
messages.removeAt(intermediateIndex);
|
|
} else {
|
|
// 如果中间状态消息有内容,将其转换为正式消息
|
|
messages[intermediateIndex] = messages[intermediateIndex].copyWith(
|
|
isIntermediate: false,
|
|
);
|
|
}
|
|
messages.refresh();
|
|
}
|
|
|
|
// 停止语音识别
|
|
isVoiceRecognitionActive.value = false;
|
|
isListening.value = false;
|
|
isSpeechDetected.value = false;
|
|
_asrService.stopContinuousRecognition();
|
|
}
|
|
|
|
// 停止语音识别
|
|
Future<void> stopVoiceRecognition() async {
|
|
if (!isVoiceRecognitionActive.value) return;
|
|
|
|
try {
|
|
await _asrService.stopContinuousRecognition();
|
|
_recognitionSubscription?.cancel();
|
|
_recognitionSubscription = null;
|
|
|
|
isVoiceRecognitionActive.value = false;
|
|
isListening.value = false;
|
|
recognizedText.value = '';
|
|
isSpeechDetected.value = false;
|
|
|
|
// 查找并处理用户的中间状态消息
|
|
final intermediateIndex =
|
|
messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
|
|
if (intermediateIndex >= 0) {
|
|
if (messages[intermediateIndex].content.trim().isEmpty) {
|
|
// 如果没有内容则移除
|
|
messages.removeAt(intermediateIndex);
|
|
} else {
|
|
// 有内容则转为正式消息
|
|
messages[intermediateIndex] = messages[intermediateIndex].copyWith(
|
|
isIntermediate: false,
|
|
);
|
|
}
|
|
messages.refresh();
|
|
}
|
|
} catch (e) {
|
|
Logger.error('停止语音识别失败', e);
|
|
}
|
|
}
|
|
|
|
// 清空聊天记录
|
|
void clearChat() {
|
|
messages.clear();
|
|
_ttsService.stop();
|
|
// 添加欢迎消息
|
|
if (agent.welcomeMessage.isNotEmpty) {
|
|
messages.add(Message(
|
|
role: 'assistant',
|
|
content: agent.welcomeMessage,
|
|
timestamp: DateTime.now(),
|
|
));
|
|
}
|
|
// 删除数据库中的历史记录
|
|
ChatStorage.deleteMessages(sessionId: agent.id).catchError((e) {
|
|
Logger.error('清空数据库聊天记录失败: $e');
|
|
});
|
|
}
|
|
|
|
// 滚动监听,用于触发加载更多历史记录
|
|
void _scrollListener() {
|
|
// 当滚动到顶部并且有更多历史记录且不在加载中时,加载更多
|
|
if (scrollController.position.pixels ==
|
|
scrollController.position.minScrollExtent &&
|
|
_hasMoreHistory.value &&
|
|
!_isLoadingMore.value) {
|
|
_loadMoreHistory();
|
|
}
|
|
}
|
|
|
|
// 加载聊天历史 (首次加载或刷新)
|
|
Future<void> _loadChatHistory() async {
|
|
isLoading.value = true;
|
|
_currentPage.value = 1; // 重置为第一页
|
|
_hasMoreHistory.value = true; // 假设有更多
|
|
|
|
try {
|
|
final pagedResult = await ChatStorage.getMessages(
|
|
sessionId: agent.id,
|
|
page: _currentPage.value,
|
|
pageSize: _pageSize,
|
|
);
|
|
|
|
_totalPages.value = pagedResult.totalPages;
|
|
_hasMoreHistory.value = _currentPage.value < _totalPages.value;
|
|
|
|
final historyMessages = pagedResult.messages.map((chatMsg) {
|
|
return Message(
|
|
role: chatMsg.sender,
|
|
content: chatMsg.message,
|
|
timestamp: DateTime.parse(chatMsg.timestamp),
|
|
meta: chatMsg.metadata != null ? jsonDecode(chatMsg.metadata!) : null,
|
|
);
|
|
}).toList();
|
|
|
|
messages.assignAll(historyMessages.reversed); // 最新的在底部,所以要反转
|
|
|
|
// 滚动到底部 (如果是首次加载)
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (scrollController.hasClients) {
|
|
scrollController.jumpTo(scrollController.position.maxScrollExtent);
|
|
}
|
|
});
|
|
} catch (e) {
|
|
Logger.error('加载聊天历史失败', e);
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
}
|
|
|
|
// 加载更多历史记录
|
|
Future<void> _loadMoreHistory() async {
|
|
if (_isLoadingMore.value || !_hasMoreHistory.value) return;
|
|
|
|
_isLoadingMore.value = true;
|
|
_currentPage.value++;
|
|
|
|
try {
|
|
final pagedResult = await ChatStorage.getMessages(
|
|
sessionId: agent.id,
|
|
page: _currentPage.value,
|
|
pageSize: _pageSize,
|
|
);
|
|
|
|
_totalPages.value = pagedResult.totalPages;
|
|
_hasMoreHistory.value = _currentPage.value < _totalPages.value;
|
|
|
|
final olderMessages = pagedResult.messages.map((chatMsg) {
|
|
return Message(
|
|
role: chatMsg.sender,
|
|
content: chatMsg.message,
|
|
timestamp: DateTime.parse(chatMsg.timestamp),
|
|
meta: chatMsg.metadata != null ? jsonDecode(chatMsg.metadata!) : null,
|
|
);
|
|
}).toList();
|
|
|
|
// 将旧消息插入到列表顶部
|
|
messages.insertAll(0, olderMessages.reversed);
|
|
} catch (e) {
|
|
Logger.error('加载更多聊天历史失败', e);
|
|
_currentPage.value--; // 加载失败,恢复页码
|
|
} finally {
|
|
_isLoadingMore.value = false;
|
|
}
|
|
}
|
|
|
|
// 保存单条聊天消息到数据库
|
|
Future<void> _saveMessageToDb(Message message) async {
|
|
if (message.isIntermediate) return; // 不保存中间状态的消息
|
|
try {
|
|
await ChatStorage.saveMessage(
|
|
sessionId: agent.id,
|
|
message: message.content,
|
|
sender: message.role,
|
|
metadata: jsonEncode(message.meta),
|
|
);
|
|
} catch (e) {
|
|
Logger.error('保存消息到数据库失败', e);
|
|
}
|
|
}
|
|
|
|
// 添加消息,供外部调用 (如用户发送消息、AI回复)
|
|
void addMessage(Message message) {
|
|
messages.add(message);
|
|
_saveMessageToDb(message); // 保存到数据库
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_scrollToBottom();
|
|
});
|
|
}
|
|
|
|
void _stopVoiceRecognitionAutomatically() {
|
|
final int timeoutSeconds = _isVoiceInteractionMode ? 8 : 15;
|
|
Logger.info('${timeoutSeconds}秒无活动,自动停止语音识别');
|
|
stopVoiceRecognition();
|
|
}
|
|
|
|
/// 检查是否为重复消息
|
|
bool _isDuplicateMessage(Message message) {
|
|
return messages.any((m) =>
|
|
m.role == message.role &&
|
|
m.content == message.content &&
|
|
(m.timestamp.millisecondsSinceEpoch -
|
|
message.timestamp.millisecondsSinceEpoch)
|
|
.abs() <
|
|
5000);
|
|
}
|
|
}
|
|
|