|
|
|
@ -34,8 +34,6 @@ class ChatController extends GetxController { |
|
|
|
// 用于存储当前正在流式生成的消息 |
|
|
|
final RxString currentStreamMessage = ''.obs; |
|
|
|
Message? _currentAssistantMessage; |
|
|
|
String _pendingTtsText = ''; |
|
|
|
static const int _minTtsLength = 20; |
|
|
|
bool _isDisposed = false; |
|
|
|
|
|
|
|
// TTS事件订阅 |
|
|
|
@ -71,18 +69,6 @@ class ChatController extends GetxController { |
|
|
|
isVoiceMode.value = !isVoiceMode.value; |
|
|
|
} |
|
|
|
|
|
|
|
// 辅助方法:管理TTS状态 |
|
|
|
void _manageTtsState(bool enable) { |
|
|
|
if (!enable && isTtsEnabled.value) { |
|
|
|
// 需要禁用TTS |
|
|
|
_ttsService.endSession(); // 先停止当前播放 |
|
|
|
isTtsEnabled.value = false; |
|
|
|
} else if (enable && !isTtsEnabled.value) { |
|
|
|
// 需要启用TTS |
|
|
|
isTtsEnabled.value = true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 开始语音输入 |
|
|
|
void startVoiceInput() { |
|
|
|
// 确保先停止任何可能正在进行的语音识别会话 |
|
|
|
@ -154,38 +140,38 @@ class ChatController extends GetxController { |
|
|
|
} |
|
|
|
|
|
|
|
void stopVoiceInput() { |
|
|
|
// 确保停止语音识别 |
|
|
|
// 停止语音识别 |
|
|
|
if (Get.isRegistered<VoiceInputController>()) { |
|
|
|
try { |
|
|
|
final voiceController = Get.find<VoiceInputController>(); |
|
|
|
voiceController.stopRecording(); |
|
|
|
Get.find<VoiceInputController>().stopRecording(); |
|
|
|
Get.delete<VoiceInputController>(); |
|
|
|
} catch (e) { |
|
|
|
debugPrint('Error stopping voice recording: $e'); |
|
|
|
debugPrint('停止语音输入失败: $e'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 重置状态 |
|
|
|
isVoiceInputVisible.value = false; |
|
|
|
isVoiceConnecting.value = true; |
|
|
|
isRecording.value = false; |
|
|
|
recordingText.value = ''; |
|
|
|
isRecognizing.value = false; |
|
|
|
|
|
|
|
// 不再需要恢复TTS状态,因为已启用回声消除 |
|
|
|
|
|
|
|
// 如果存在临时语音消息但没有实际内容,则移除它 |
|
|
|
// 清理临时语音消息 |
|
|
|
if (_currentVoiceMessage != null && (_currentVoiceMessage!.content == '🎤 ...' || _currentVoiceMessage!.content.startsWith('🎤 '))) { |
|
|
|
messages.remove(_currentVoiceMessage); |
|
|
|
} |
|
|
|
_currentVoiceMessage = null; |
|
|
|
|
|
|
|
// 当语音面板关闭时,确保ListView滚动回适当位置 |
|
|
|
// 滚动到底部 |
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) { |
|
|
|
if (!_isDisposed && scrollController.hasClients && messages.isNotEmpty) { |
|
|
|
// 滚动到底部,确保最新消息可见 |
|
|
|
if (!_isDisposed && messages.isNotEmpty) { |
|
|
|
_scrollToBottom(animate: true); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// 重置按住说话状态 |
|
|
|
|
|
|
|
// 删除语音输入控制器 |
|
|
|
try { |
|
|
|
if (Get.isRegistered<VoiceInputController>()) { |
|
|
|
@ -200,107 +186,106 @@ class ChatController extends GetxController { |
|
|
|
} |
|
|
|
|
|
|
|
void handleRecognizing(String text) { |
|
|
|
if (text.isNotEmpty) { |
|
|
|
// 更新识别中的文本 |
|
|
|
recordingText.value = text; |
|
|
|
if (text.isEmpty) return; |
|
|
|
|
|
|
|
// 更新识别中的文本 |
|
|
|
recordingText.value = text; |
|
|
|
|
|
|
|
// 如果还没有创建临时语音消息,则创建一个 |
|
|
|
if (_currentVoiceMessage == null) { |
|
|
|
_currentVoiceMessage = Message( |
|
|
|
role: 'user', |
|
|
|
content: '🎤 $text', |
|
|
|
timestamp: DateTime.now(), |
|
|
|
); |
|
|
|
messages.add(_currentVoiceMessage!); |
|
|
|
isRecognizing.value = true; |
|
|
|
|
|
|
|
// 滚动到底部 |
|
|
|
_scrollToBottom(animate: true); |
|
|
|
} else { |
|
|
|
// 更新现有的临时语音消息 |
|
|
|
final index = messages.indexWhere((msg) => |
|
|
|
msg.timestamp == _currentVoiceMessage!.timestamp && |
|
|
|
msg.role == _currentVoiceMessage!.role); |
|
|
|
|
|
|
|
// 如果还没有创建临时语音消息,则创建一个 |
|
|
|
if (_currentVoiceMessage == null) { |
|
|
|
_currentVoiceMessage = Message( |
|
|
|
if (index != -1) { |
|
|
|
messages[index] = Message( |
|
|
|
role: 'user', |
|
|
|
content: '🎤 ' + text, |
|
|
|
timestamp: DateTime.now(), |
|
|
|
content: '🎤 $text', |
|
|
|
timestamp: _currentVoiceMessage!.timestamp, |
|
|
|
); |
|
|
|
messages.add(_currentVoiceMessage!); |
|
|
|
isRecognizing.value = true; |
|
|
|
|
|
|
|
// 滚动到底部 |
|
|
|
_scrollToBottom(animate: true); |
|
|
|
} else { |
|
|
|
// 更新现有的临时语音消息 |
|
|
|
final index = messages.indexWhere((msg) => |
|
|
|
msg.timestamp == _currentVoiceMessage!.timestamp && |
|
|
|
msg.role == _currentVoiceMessage!.role); |
|
|
|
|
|
|
|
if (index != -1) { |
|
|
|
messages[index] = Message( |
|
|
|
role: 'user', |
|
|
|
content: '🎤 ' + text, |
|
|
|
timestamp: _currentVoiceMessage!.timestamp, |
|
|
|
); |
|
|
|
|
|
|
|
// 滚动到底部 |
|
|
|
_scrollToBottom(animate: false); |
|
|
|
} |
|
|
|
_scrollToBottom(animate: false); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
void handleVoiceResult(String text) { |
|
|
|
if (text.isNotEmpty) { |
|
|
|
// 如果有临时语音消息,更新它 |
|
|
|
if (_currentVoiceMessage != null) { |
|
|
|
final index = messages.indexWhere((msg) => |
|
|
|
msg.timestamp == _currentVoiceMessage!.timestamp && |
|
|
|
msg.role == _currentVoiceMessage!.role); |
|
|
|
|
|
|
|
if (index != -1) { |
|
|
|
// 更新消息,移除麦克风图标 |
|
|
|
messages[index] = Message( |
|
|
|
role: 'user', |
|
|
|
content: text, |
|
|
|
timestamp: _currentVoiceMessage!.timestamp, |
|
|
|
); |
|
|
|
|
|
|
|
// 保存聊天历史 |
|
|
|
_saveChatHistory(); |
|
|
|
|
|
|
|
// 滚动到底部 |
|
|
|
_scrollToBottom(animate: true); |
|
|
|
if (text.isEmpty) return; |
|
|
|
|
|
|
|
// 重新启用TTS,以便AI回复时可以播放 |
|
|
|
_manageTtsState(true); |
|
|
|
Message userMessage; |
|
|
|
|
|
|
|
// 发送给AI处理 |
|
|
|
_processAIResponse(messages[index]); |
|
|
|
// 处理现有的临时语音消息 |
|
|
|
if (_currentVoiceMessage != null) { |
|
|
|
final index = messages.indexWhere((msg) => |
|
|
|
msg.timestamp == _currentVoiceMessage!.timestamp && |
|
|
|
msg.role == _currentVoiceMessage!.role); |
|
|
|
|
|
|
|
// 重置临时语音消息 |
|
|
|
_currentVoiceMessage = null; |
|
|
|
isRecognizing.value = false; |
|
|
|
} |
|
|
|
if (index != -1) { |
|
|
|
// 更新消息,移除麦克风图标 |
|
|
|
userMessage = Message( |
|
|
|
role: 'user', |
|
|
|
content: text, |
|
|
|
timestamp: _currentVoiceMessage!.timestamp, |
|
|
|
); |
|
|
|
messages[index] = userMessage; |
|
|
|
} else { |
|
|
|
// 如果没有临时语音消息,创建一个新的用户消息 |
|
|
|
final userMessage = Message( |
|
|
|
// 创建新消息 |
|
|
|
userMessage = Message( |
|
|
|
role: 'user', |
|
|
|
content: text, |
|
|
|
timestamp: DateTime.now(), |
|
|
|
); |
|
|
|
|
|
|
|
// 添加到消息列表 |
|
|
|
messages.add(userMessage); |
|
|
|
} |
|
|
|
} else { |
|
|
|
// 创建新消息 |
|
|
|
userMessage = Message( |
|
|
|
role: 'user', |
|
|
|
content: text, |
|
|
|
timestamp: DateTime.now(), |
|
|
|
); |
|
|
|
messages.add(userMessage); |
|
|
|
} |
|
|
|
|
|
|
|
// 保存聊天历史 |
|
|
|
_saveChatHistory(); |
|
|
|
// 保存聊天历史 |
|
|
|
_saveChatHistory(); |
|
|
|
|
|
|
|
// 滚动到底部 |
|
|
|
_scrollToBottom(animate: true); |
|
|
|
// 滚动到底部 |
|
|
|
_scrollToBottom(animate: true); |
|
|
|
|
|
|
|
// 重新启用TTS,以便AI回复时可以播放 |
|
|
|
_manageTtsState(true); |
|
|
|
// 重置状态 |
|
|
|
_currentVoiceMessage = null; |
|
|
|
isRecognizing.value = false; |
|
|
|
|
|
|
|
// 发送给AI处理 |
|
|
|
_processAIResponse(userMessage); |
|
|
|
} |
|
|
|
} |
|
|
|
// 处理AI响应 |
|
|
|
_processAIResponse(userMessage); |
|
|
|
} |
|
|
|
|
|
|
|
// 处理AI响应 |
|
|
|
Future<void> _processAIResponse(Message userMessage) async { |
|
|
|
if (_isDisposed) return; |
|
|
|
|
|
|
|
// TTS会话状态 |
|
|
|
bool ttsSessionStarted = false; |
|
|
|
const speaker = 'zh_female_shuangkuaisisi_moon_bigtts'; |
|
|
|
|
|
|
|
try { |
|
|
|
isLoading.value = true; |
|
|
|
|
|
|
|
// 创建助手消息 |
|
|
|
currentStreamMessage.value = ''; |
|
|
|
_currentAssistantMessage = Message( |
|
|
|
role: 'assistant', |
|
|
|
@ -308,75 +293,60 @@ class ChatController extends GetxController { |
|
|
|
timestamp: DateTime.now(), |
|
|
|
); |
|
|
|
messages.add(_currentAssistantMessage!); |
|
|
|
_pendingTtsText = ''; |
|
|
|
|
|
|
|
// 确保在开始新的合成前停止当前播放 |
|
|
|
// 准备TTS会话 |
|
|
|
if (isTtsEnabled.value) { |
|
|
|
await _ttsService.endSession(); |
|
|
|
// 添加短暂延迟确保停止完成 |
|
|
|
await Future.delayed(const Duration(milliseconds: 200)); |
|
|
|
|
|
|
|
if (!_ttsService.isConnected.value) { |
|
|
|
await _ttsService.connect(); |
|
|
|
} |
|
|
|
|
|
|
|
ttsSessionStarted = await _ttsService.startSession(speaker); |
|
|
|
} |
|
|
|
|
|
|
|
// 处理AI流式响应 |
|
|
|
await for (final chunk in _aiService.sendMessageStream( |
|
|
|
messages: messages |
|
|
|
.map((m) => { |
|
|
|
'role': m.role, |
|
|
|
'content': m.content, |
|
|
|
}) |
|
|
|
.toList(), |
|
|
|
messages: messages.map((m) => {'role': m.role, 'content': m.content}).toList(), |
|
|
|
systemPrompt: systemPrompt, |
|
|
|
)) { |
|
|
|
if (_isDisposed) break; |
|
|
|
|
|
|
|
// 更新UI |
|
|
|
currentStreamMessage.value += chunk; |
|
|
|
_updateAssistantMessage(currentStreamMessage.value); |
|
|
|
|
|
|
|
// 累积文本并合成 |
|
|
|
_pendingTtsText += chunk; |
|
|
|
if (!_isDisposed && isTtsEnabled.value) { |
|
|
|
// 检查是否达到最小长度 |
|
|
|
if (_pendingTtsText.length >= _minTtsLength) { |
|
|
|
// 找到最后一个句子结束的位置 |
|
|
|
int lastSentenceEnd = _findLastSentenceEnd(_pendingTtsText); |
|
|
|
if (lastSentenceEnd > 0) { |
|
|
|
// 播放到最后一个句子结束的位置 |
|
|
|
String textToSpeak = |
|
|
|
_pendingTtsText.substring(0, lastSentenceEnd + 1); |
|
|
|
|
|
|
|
// 使用新方法替代直接调用speak |
|
|
|
_speakText(textToSpeak); |
|
|
|
|
|
|
|
// 保留剩余的文本 |
|
|
|
_pendingTtsText = _pendingTtsText.substring(lastSentenceEnd + 1); |
|
|
|
} |
|
|
|
} |
|
|
|
// 直接将文本片段发送到TTS接口 |
|
|
|
if (isTtsEnabled.value && ttsSessionStarted && chunk.isNotEmpty) { |
|
|
|
await _ttsService.speak(chunk, speaker: speaker); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 处理剩余的文本 |
|
|
|
if (!_isDisposed && |
|
|
|
isTtsEnabled.value && |
|
|
|
_pendingTtsText.isNotEmpty) { |
|
|
|
// 使用新方法替代直接调用speak |
|
|
|
_speakText(_pendingTtsText); |
|
|
|
// 结束TTS会话 |
|
|
|
if (isTtsEnabled.value && ttsSessionStarted) { |
|
|
|
await _ttsService.endSession(); |
|
|
|
} |
|
|
|
_pendingTtsText = ''; |
|
|
|
|
|
|
|
// 保存聊天历史 |
|
|
|
if (!_isDisposed) { |
|
|
|
await _saveChatHistory(); |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
// 错误处理 |
|
|
|
if (!_isDisposed) { |
|
|
|
if (_currentAssistantMessage != null) { |
|
|
|
messages.remove(_currentAssistantMessage); |
|
|
|
} |
|
|
|
Get.snackbar( |
|
|
|
'Error', |
|
|
|
'Failed to get response from AI: $e', |
|
|
|
snackPosition: SnackPosition.BOTTOM, |
|
|
|
); |
|
|
|
|
|
|
|
Get.snackbar('Error', 'Failed to get response from AI', snackPosition: SnackPosition.BOTTOM); |
|
|
|
|
|
|
|
// 结束TTS会话 |
|
|
|
if (isTtsEnabled.value && ttsSessionStarted) { |
|
|
|
await _ttsService.endSession(); |
|
|
|
} |
|
|
|
} |
|
|
|
} finally { |
|
|
|
// 重置状态 |
|
|
|
if (!_isDisposed) { |
|
|
|
_currentAssistantMessage = null; |
|
|
|
currentStreamMessage.value = ''; |
|
|
|
@ -385,48 +355,36 @@ class ChatController extends GetxController { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 播放文本并使用WebSocket流式合成 |
|
|
|
// 播放文本(用于独立播放场景) |
|
|
|
Future<void> _speakText(String text) async { |
|
|
|
if (text.isEmpty || !isTtsEnabled.value) return; |
|
|
|
|
|
|
|
try { |
|
|
|
// 如果当前有正在进行的TTS会话,先结束它 |
|
|
|
if (_isTtsSpeaking) { |
|
|
|
await _ttsService.endSession(); |
|
|
|
} |
|
|
|
|
|
|
|
// 确保连接已建立 |
|
|
|
if (!_ttsService.isConnected.value) { |
|
|
|
final connected = await _ttsService.connect(); |
|
|
|
if (!connected) { |
|
|
|
print('无法连接到TTS服务'); |
|
|
|
return; |
|
|
|
} |
|
|
|
await _ttsService.connect(); |
|
|
|
} |
|
|
|
|
|
|
|
// 开始合成 |
|
|
|
await _ttsService.synthesize(text, 'zh_female_1'); // 使用默认说话人 |
|
|
|
const speaker = 'zh_female_shuangkuaisisi_moon_bigtts'; |
|
|
|
|
|
|
|
await _ttsService.startSession(speaker); |
|
|
|
await _ttsService.speak(text, speaker: speaker); |
|
|
|
await _ttsService.endSession(); |
|
|
|
} catch (e) { |
|
|
|
print('TTS播放出错: $e'); |
|
|
|
debugPrint('TTS播放出错: $e'); |
|
|
|
await _ttsService.endSession(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 确保滚动到底部的方法,使用多种策略确保成功 |
|
|
|
// 确保滚动到底部 |
|
|
|
void _ensureScrollToBottom() { |
|
|
|
// 立即尝试滚动 |
|
|
|
_scrollToBottom(); |
|
|
|
|
|
|
|
// 延迟100ms后再次尝试滚动(等待视图构建) |
|
|
|
// 延迟滚动以确保视图已构建 |
|
|
|
Future.delayed(const Duration(milliseconds: 100), () { |
|
|
|
if (!_isDisposed) _scrollToBottom(animate: true); |
|
|
|
}); |
|
|
|
|
|
|
|
// 延迟500ms后再次尝试滚动(确保所有元素都已加载) |
|
|
|
Future.delayed(const Duration(milliseconds: 500), () { |
|
|
|
if (!_isDisposed) _scrollToBottom(animate: true); |
|
|
|
}); |
|
|
|
|
|
|
|
// 使用帧回调确保在渲染后滚动 |
|
|
|
// 帧回调确保在渲染后滚动 |
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) { |
|
|
|
if (!_isDisposed) _scrollToBottom(animate: true); |
|
|
|
}); |
|
|
|
@ -450,17 +408,17 @@ class ChatController extends GetxController { |
|
|
|
messageController = TextEditingController(); |
|
|
|
scrollController = ScrollController(); |
|
|
|
|
|
|
|
// 确保清理任何可能存在的语音输入控制器 |
|
|
|
// 清理可能存在的语音输入控制器 |
|
|
|
if (Get.isRegistered<VoiceInputController>()) { |
|
|
|
try { |
|
|
|
final voiceController = Get.find<VoiceInputController>(); |
|
|
|
voiceController.stopRecording(); |
|
|
|
Get.find<VoiceInputController>().stopRecording(); |
|
|
|
Get.delete<VoiceInputController>(); |
|
|
|
} catch (e) { |
|
|
|
debugPrint('Error cleaning up existing VoiceInputController: $e'); |
|
|
|
debugPrint('清理语音控制器失败: $e'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 初始化服务和数据 |
|
|
|
_initTtsService(); |
|
|
|
_initFromArguments(); |
|
|
|
_initializeController(); |
|
|
|
@ -468,18 +426,8 @@ class ChatController extends GetxController { |
|
|
|
// 添加滚动监听器 |
|
|
|
scrollController.addListener(_scrollListener); |
|
|
|
|
|
|
|
// 确保在页面加载完成后滚动到底部 |
|
|
|
// 确保滚动到底部 |
|
|
|
_ensureScrollToBottom(); |
|
|
|
|
|
|
|
// 添加帧回调,确保在页面完全渲染后滚动到底部 |
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) { |
|
|
|
// 延迟执行,确保页面已完全加载 |
|
|
|
Future.delayed(const Duration(milliseconds: 300), () { |
|
|
|
if (!_isDisposed) { |
|
|
|
_scrollToBottom(animate: true); |
|
|
|
} |
|
|
|
}); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
void _initFromArguments() { |
|
|
|
@ -527,21 +475,24 @@ class ChatController extends GetxController { |
|
|
|
void _handleTtsEvent(Map<String, dynamic> event) { |
|
|
|
if (_isDisposed) return; |
|
|
|
|
|
|
|
final String eventType = event['type'] as String? ?? 'unknown'; |
|
|
|
final String eventType = event['eventType'] as String? ?? 'unknown'; |
|
|
|
|
|
|
|
// 根据事件类型更新TTS状态 |
|
|
|
switch (eventType) { |
|
|
|
case 'ttsSentenceStart': |
|
|
|
case 'playbackStarted': |
|
|
|
case 'synthesisBegin': |
|
|
|
_isTtsSpeaking = true; |
|
|
|
break; |
|
|
|
case 'ttsSentenceEnd': |
|
|
|
case 'playbackCompleted': |
|
|
|
case 'synthesisEnd': |
|
|
|
case 'sessionFinished': |
|
|
|
case 'connectionFinished': |
|
|
|
_isTtsSpeaking = false; |
|
|
|
break; |
|
|
|
case 'connectionFailed': |
|
|
|
case 'sessionFailed': |
|
|
|
case 'playbackError': |
|
|
|
_isTtsSpeaking = false; |
|
|
|
print('TTS错误: ${event['message']}'); |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
@ -561,44 +512,40 @@ class ChatController extends GetxController { |
|
|
|
} |
|
|
|
|
|
|
|
Future<void> _showWelcomeMessages() async { |
|
|
|
if (!agentName.isNotEmpty || !playVoiceOnEnter) return; |
|
|
|
|
|
|
|
try { |
|
|
|
if (agentName.isNotEmpty && playVoiceOnEnter) { |
|
|
|
// 只有 playVoiceOnEnter 为 true 时才生成动态问候语 |
|
|
|
await _generatePersonalizedGreeting(); |
|
|
|
// 保存聊天记录并立即滚动到底部 |
|
|
|
await _saveChatHistory(); |
|
|
|
_scrollToBottom(animate: true); |
|
|
|
// 生成动态问候语 |
|
|
|
await _generatePersonalizedGreeting(); |
|
|
|
await _saveChatHistory(); |
|
|
|
_scrollToBottom(animate: true); |
|
|
|
|
|
|
|
// 添加一个系统消息,告知用户已启用连续语音对话 |
|
|
|
final systemMessage = Message( |
|
|
|
role: 'system', |
|
|
|
content: '已为您自动启用连续语音对话模式,您可以直接开始说话', |
|
|
|
timestamp: DateTime.now(), |
|
|
|
); |
|
|
|
messages.add(systemMessage); |
|
|
|
await _saveChatHistory(); |
|
|
|
// 添加系统消息 |
|
|
|
messages.add(Message( |
|
|
|
role: 'system', |
|
|
|
content: '已为您自动启用连续语音对话模式,您可以直接开始说话', |
|
|
|
timestamp: DateTime.now(), |
|
|
|
)); |
|
|
|
await _saveChatHistory(); |
|
|
|
|
|
|
|
// 延迟一小段时间后自动启动语音输入模式 |
|
|
|
Future.delayed(const Duration(milliseconds: 500), () { |
|
|
|
if (!_isDisposed) { |
|
|
|
startVoiceInput(); |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
// 启动语音输入 |
|
|
|
Future.delayed(const Duration(milliseconds: 300), () { |
|
|
|
if (!_isDisposed) { |
|
|
|
startVoiceInput(); |
|
|
|
} |
|
|
|
}); |
|
|
|
} catch (e) { |
|
|
|
print('显示欢迎消息失败: $e'); |
|
|
|
// 如果生成问候语失败,确保至少显示静态欢迎消息 |
|
|
|
debugPrint('显示欢迎消息失败: $e'); |
|
|
|
|
|
|
|
// 播放静态欢迎消息 |
|
|
|
if (isTtsEnabled.value) { |
|
|
|
// 使用新方法替代直接调用speak |
|
|
|
_speakText(welcomeMessage); |
|
|
|
} |
|
|
|
|
|
|
|
// 即使问候语生成失败,如果playVoiceOnEnter为true,仍然启动语音输入 |
|
|
|
if (playVoiceOnEnter) { |
|
|
|
Future.delayed(const Duration(milliseconds: 500), () { |
|
|
|
if (!_isDisposed) { |
|
|
|
startVoiceInput(); |
|
|
|
} |
|
|
|
// 仍然启动语音输入 |
|
|
|
if (!_isDisposed) { |
|
|
|
Future.delayed(const Duration(milliseconds: 300), () { |
|
|
|
startVoiceInput(); |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
@ -607,8 +554,8 @@ class ChatController extends GetxController { |
|
|
|
Future<void> _generatePersonalizedGreeting() async { |
|
|
|
if (_isDisposed) return; |
|
|
|
|
|
|
|
String fullGreeting = ''; |
|
|
|
try { |
|
|
|
// 创建助手消息 |
|
|
|
currentStreamMessage.value = ''; |
|
|
|
_currentAssistantMessage = Message( |
|
|
|
role: 'assistant', |
|
|
|
@ -617,15 +564,14 @@ class ChatController extends GetxController { |
|
|
|
); |
|
|
|
messages.add(_currentAssistantMessage!); |
|
|
|
|
|
|
|
// 构建用于生成个性化问候语的消息列表 |
|
|
|
// 构建问候语请求 |
|
|
|
final greetingMessages = [ |
|
|
|
{'role': 'system', 'content': systemPrompt}, |
|
|
|
{ |
|
|
|
'role': 'user', |
|
|
|
'content': '请根据你的身份和性格,用一句简短的话向我打个招呼,要体现你的特点,不要超过18个字。' |
|
|
|
}, |
|
|
|
{'role': 'user', 'content': '请用一句简短的话向我打个招呼,要体现你的特点,不要超过18个字。'}, |
|
|
|
]; |
|
|
|
|
|
|
|
// 生成问候语 |
|
|
|
String fullGreeting = ''; |
|
|
|
await for (final chunk in _aiService.sendMessageStream( |
|
|
|
messages: greetingMessages, |
|
|
|
systemPrompt: systemPrompt, |
|
|
|
@ -638,14 +584,12 @@ class ChatController extends GetxController { |
|
|
|
fullGreeting += chunk; |
|
|
|
} |
|
|
|
|
|
|
|
// 完整问候语生成完毕后,一次性播放 |
|
|
|
if (!_isDisposed && isTtsEnabled.value && fullGreeting.isNotEmpty) { |
|
|
|
// 使用新方法替代直接调用speak |
|
|
|
// 播放问候语 |
|
|
|
if (isTtsEnabled.value && fullGreeting.isNotEmpty) { |
|
|
|
await _speakText(fullGreeting); |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
print('生成问候语失败: $e'); |
|
|
|
// 移除未完成的消息 |
|
|
|
debugPrint('生成问候语失败: $e'); |
|
|
|
if (_currentAssistantMessage != null) { |
|
|
|
messages.remove(_currentAssistantMessage); |
|
|
|
} |
|
|
|
@ -661,38 +605,25 @@ class ChatController extends GetxController { |
|
|
|
void onClose() { |
|
|
|
_isDisposed = true; |
|
|
|
|
|
|
|
// 确保语音输入被停止 |
|
|
|
// 停止语音输入和TTS |
|
|
|
stopVoiceInput(); |
|
|
|
|
|
|
|
// 先停止所有正在进行的操作 |
|
|
|
_ttsService.endSession(); // 使用endSession替代stop |
|
|
|
|
|
|
|
// 取消TTS事件订阅 |
|
|
|
_ttsService.endSession(); |
|
|
|
_ttsEventSubscription?.cancel(); |
|
|
|
|
|
|
|
// 重置状态 |
|
|
|
currentStreamMessage.value = ''; |
|
|
|
_currentAssistantMessage = null; |
|
|
|
_pendingTtsText = ''; |
|
|
|
|
|
|
|
// 最后再处理控制器的销毁 |
|
|
|
try { |
|
|
|
messageController.dispose(); |
|
|
|
} catch (e) { |
|
|
|
debugPrint('Error disposing messageController: $e'); |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
scrollController.dispose(); |
|
|
|
} catch (e) { |
|
|
|
debugPrint('Error disposing scrollController: $e'); |
|
|
|
} |
|
|
|
// 释放控制器 |
|
|
|
messageController.dispose(); |
|
|
|
scrollController.dispose(); |
|
|
|
|
|
|
|
super.onClose(); |
|
|
|
} |
|
|
|
|
|
|
|
Future<void> _loadChatHistory() async { |
|
|
|
try { |
|
|
|
final historyJson = _storage.read('${_storagePrefix}${agentId}'); |
|
|
|
final historyJson = _storage.read('${_storagePrefix}$agentId'); |
|
|
|
if (historyJson != null) { |
|
|
|
final List<dynamic> historyList = json.decode(historyJson); |
|
|
|
messages.value = historyList.map((item) => Message.fromJson(item)).toList(); |
|
|
|
@ -716,8 +647,8 @@ class ChatController extends GetxController { |
|
|
|
Future<void> _saveChatHistory() async { |
|
|
|
try { |
|
|
|
final historyJson = |
|
|
|
json.encode(messages.map((msg) => msg!.toJson()).toList()); |
|
|
|
await _storage.write('${_storagePrefix}${agentId}', historyJson); |
|
|
|
json.encode(messages.map((msg) => msg.toJson()).toList()); |
|
|
|
await _storage.write('${_storagePrefix}$agentId', historyJson); |
|
|
|
} catch (e) { |
|
|
|
Get.snackbar( |
|
|
|
'Error', |
|
|
|
@ -728,42 +659,38 @@ class ChatController extends GetxController { |
|
|
|
} |
|
|
|
|
|
|
|
void _scrollToBottom({bool animate = false}) { |
|
|
|
if (scrollController.hasClients) { |
|
|
|
if (animate) { |
|
|
|
scrollController.animateTo( |
|
|
|
scrollController.position.maxScrollExtent, |
|
|
|
duration: const Duration(milliseconds: 300), |
|
|
|
curve: Curves.easeOut, |
|
|
|
); |
|
|
|
} else { |
|
|
|
scrollController.jumpTo(scrollController.position.maxScrollExtent); |
|
|
|
} |
|
|
|
if (!scrollController.hasClients) return; |
|
|
|
|
|
|
|
final position = scrollController.position.maxScrollExtent; |
|
|
|
if (animate) { |
|
|
|
scrollController.animateTo( |
|
|
|
position, |
|
|
|
duration: const Duration(milliseconds: 300), |
|
|
|
curve: Curves.easeOut, |
|
|
|
); |
|
|
|
} else { |
|
|
|
// 如果 scrollController 还没有附加到 ListView,延迟执行 |
|
|
|
Future.delayed(const Duration(milliseconds: 100), () { |
|
|
|
if (!_isDisposed) { |
|
|
|
_scrollToBottom(animate: animate); |
|
|
|
} |
|
|
|
}); |
|
|
|
scrollController.jumpTo(position); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
void _updateAssistantMessage(String content) { |
|
|
|
if (_currentAssistantMessage != null) { |
|
|
|
final index = messages.indexWhere((msg) => |
|
|
|
msg.timestamp == _currentAssistantMessage?.timestamp && |
|
|
|
msg.role == _currentAssistantMessage?.role); |
|
|
|
if (_currentAssistantMessage == null) return; |
|
|
|
|
|
|
|
if (index != -1) { |
|
|
|
messages[index] = Message( |
|
|
|
role: 'assistant', |
|
|
|
content: content, |
|
|
|
timestamp: _currentAssistantMessage!.timestamp, |
|
|
|
); |
|
|
|
// 只在非初始化阶段滚动 |
|
|
|
if (!playVoiceOnEnter || content.isNotEmpty) { |
|
|
|
_scrollToBottom(animate: false); |
|
|
|
} |
|
|
|
final index = messages.indexWhere((msg) => |
|
|
|
msg.timestamp == _currentAssistantMessage?.timestamp && |
|
|
|
msg.role == _currentAssistantMessage?.role); |
|
|
|
|
|
|
|
if (index != -1) { |
|
|
|
// 使用非空断言,因为我们已经在方法开始时检查了非空 |
|
|
|
final timestamp = _currentAssistantMessage!.timestamp; |
|
|
|
messages[index] = Message( |
|
|
|
role: 'assistant', |
|
|
|
content: content, |
|
|
|
timestamp: timestamp, |
|
|
|
); |
|
|
|
// 只在非初始化阶段滚动 |
|
|
|
if (!playVoiceOnEnter || content.isNotEmpty) { |
|
|
|
_scrollToBottom(animate: false); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
@ -780,112 +707,25 @@ class ChatController extends GetxController { |
|
|
|
final text = messageController.text.trim(); |
|
|
|
if (text.isEmpty) return; |
|
|
|
|
|
|
|
try { |
|
|
|
final userMessage = Message( |
|
|
|
role: 'user', |
|
|
|
content: text, |
|
|
|
timestamp: DateTime.now(), |
|
|
|
); |
|
|
|
messages.add(userMessage); |
|
|
|
clearMessage(); |
|
|
|
await _saveChatHistory(); |
|
|
|
_scrollToBottom(animate: true); |
|
|
|
|
|
|
|
isLoading.value = true; |
|
|
|
try { |
|
|
|
currentStreamMessage.value = ''; |
|
|
|
_currentAssistantMessage = Message( |
|
|
|
role: 'assistant', |
|
|
|
content: '', |
|
|
|
timestamp: DateTime.now(), |
|
|
|
); |
|
|
|
messages.add(_currentAssistantMessage!); |
|
|
|
_pendingTtsText = ''; |
|
|
|
|
|
|
|
await for (final chunk in _aiService.sendMessageStream( |
|
|
|
messages: messages |
|
|
|
.map((m) => { |
|
|
|
'role': m.role, |
|
|
|
'content': m.content, |
|
|
|
}) |
|
|
|
.toList(), |
|
|
|
systemPrompt: systemPrompt, |
|
|
|
)) { |
|
|
|
if (_isDisposed) break; |
|
|
|
|
|
|
|
currentStreamMessage.value += chunk; |
|
|
|
_updateAssistantMessage(currentStreamMessage.value); |
|
|
|
|
|
|
|
// 累积文本并合成 |
|
|
|
_pendingTtsText += chunk; |
|
|
|
if (!_isDisposed && isTtsEnabled.value) { |
|
|
|
// 检查是否达到最小长度 |
|
|
|
if (_pendingTtsText.length >= _minTtsLength) { |
|
|
|
// 找到最后一个句子结束的位置 |
|
|
|
int lastSentenceEnd = _findLastSentenceEnd(_pendingTtsText); |
|
|
|
if (lastSentenceEnd > 0) { |
|
|
|
// 播放到最后一个句子结束的位置 |
|
|
|
String textToSpeak = |
|
|
|
_pendingTtsText.substring(0, lastSentenceEnd + 1); |
|
|
|
// 使用新方法替代直接调用speak |
|
|
|
_speakText(textToSpeak); |
|
|
|
// 保留剩余的文本 |
|
|
|
_pendingTtsText = _pendingTtsText.substring(lastSentenceEnd + 1); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 处理剩余的文本 |
|
|
|
if (!_isDisposed && isTtsEnabled.value && _pendingTtsText.isNotEmpty) { |
|
|
|
// 使用新方法替代直接调用speak |
|
|
|
_speakText(_pendingTtsText); |
|
|
|
} |
|
|
|
_pendingTtsText = ''; |
|
|
|
// 创建用户消息 |
|
|
|
final userMessage = Message( |
|
|
|
role: 'user', |
|
|
|
content: text, |
|
|
|
timestamp: DateTime.now(), |
|
|
|
); |
|
|
|
messages.add(userMessage); |
|
|
|
clearMessage(); |
|
|
|
await _saveChatHistory(); |
|
|
|
_scrollToBottom(animate: true); |
|
|
|
|
|
|
|
if (!_isDisposed) { |
|
|
|
await _saveChatHistory(); |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
if (!_isDisposed) { |
|
|
|
if (_currentAssistantMessage != null) { |
|
|
|
messages.remove(_currentAssistantMessage); |
|
|
|
} |
|
|
|
Get.snackbar( |
|
|
|
'Error', |
|
|
|
'Failed to get response from AI: $e', |
|
|
|
snackPosition: SnackPosition.BOTTOM, |
|
|
|
); |
|
|
|
} |
|
|
|
} finally { |
|
|
|
if (!_isDisposed) { |
|
|
|
_currentAssistantMessage = null; |
|
|
|
currentStreamMessage.value = ''; |
|
|
|
isLoading.value = false; |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
print('发送消息失败: $e'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
int _findLastSentenceEnd(String text) { |
|
|
|
final sentenceEnds = [ |
|
|
|
text.lastIndexOf('。'), |
|
|
|
text.lastIndexOf('!'), |
|
|
|
text.lastIndexOf('?'), |
|
|
|
text.lastIndexOf('.'), |
|
|
|
text.lastIndexOf('!'), |
|
|
|
text.lastIndexOf('?'), |
|
|
|
]; |
|
|
|
|
|
|
|
return sentenceEnds.reduce((max, pos) => pos > max ? pos : max); |
|
|
|
// 处理AI响应 |
|
|
|
await _processAIResponse(userMessage); |
|
|
|
} |
|
|
|
|
|
|
|
Future<void> clearChat() async { |
|
|
|
try { |
|
|
|
messages.clear(); |
|
|
|
await _storage.remove('${_storagePrefix}${agentId}'); |
|
|
|
await _storage.remove('${_storagePrefix}$agentId'); |
|
|
|
await _showWelcomeMessages(); |
|
|
|
} catch (e) { |
|
|
|
Get.snackbar( |
|
|
|
@ -900,7 +740,7 @@ class ChatController extends GetxController { |
|
|
|
// 切换TTS启用状态 |
|
|
|
isTtsEnabled.toggle(); |
|
|
|
if (!isTtsEnabled.value) { |
|
|
|
_ttsService.endSession(); // 使用endSession替代stop |
|
|
|
_ttsService.endSession(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -909,6 +749,7 @@ class ChatController extends GetxController { |
|
|
|
if (_isDisposed) return; |
|
|
|
|
|
|
|
try { |
|
|
|
// 创建助手消息 |
|
|
|
currentStreamMessage.value = ''; |
|
|
|
_currentAssistantMessage = Message( |
|
|
|
role: 'assistant', |
|
|
|
@ -923,6 +764,7 @@ class ChatController extends GetxController { |
|
|
|
{'role': 'user', 'content': '请用一句简短的话向我打个招呼,要体现你的特点,不要超过12个字。'}, |
|
|
|
]; |
|
|
|
|
|
|
|
// 生成问候语 |
|
|
|
String fullGreeting = ''; |
|
|
|
await for (final chunk in _aiService.sendMessageStream( |
|
|
|
messages: greetingMessages, |
|
|
|
@ -936,13 +778,12 @@ class ChatController extends GetxController { |
|
|
|
fullGreeting += chunk; |
|
|
|
} |
|
|
|
|
|
|
|
// 完整问候语生成完毕后,一次性播放 |
|
|
|
if (!_isDisposed && isTtsEnabled.value && fullGreeting.isNotEmpty) { |
|
|
|
// 使用新方法替代直接调用speak |
|
|
|
// 播放问候语 |
|
|
|
if (isTtsEnabled.value && fullGreeting.isNotEmpty) { |
|
|
|
await _speakText(fullGreeting); |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
print('生成问候语失败: $e'); |
|
|
|
debugPrint('生成问候语失败: $e'); |
|
|
|
// 移除未完成的消息 |
|
|
|
if (_currentAssistantMessage != null) { |
|
|
|
messages.remove(_currentAssistantMessage); |
|
|
|
|