Browse Source
fix(pairing): 修复配对页面跳过绑定按钮逻辑 style: 统一pubspec.yaml中的引号格式 perf(music): 优化音乐播放列表的响应式布局 chore: 更新应用版本号至1.0.0+6weicu
11 changed files with 212 additions and 1244 deletions
@ -1,94 +0,0 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|||
import '../../modules/chat/models/message_model.dart'; |
|||
import '../theme/app_colors.dart'; |
|||
|
|||
class MessageBubble extends StatelessWidget { |
|||
final Message message; |
|||
final bool isUser; |
|||
final String? userAvatar; |
|||
final String? agentAvatar; |
|||
|
|||
const MessageBubble({ |
|||
Key? key, |
|||
required this.message, |
|||
required this.isUser, |
|||
this.userAvatar, |
|||
this.agentAvatar, |
|||
}) : super(key: key); |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Padding( |
|||
padding: EdgeInsets.only(bottom: 16.h), |
|||
child: Row( |
|||
mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start, |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Flexible( |
|||
child: Container( |
|||
padding: EdgeInsets.all(12.r), |
|||
decoration: BoxDecoration( |
|||
color: isUser ? AppColors.primary : Colors.grey[200], |
|||
borderRadius: BorderRadius.circular(16.r), |
|||
), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
// 消息内容 |
|||
if (message.content.isNotEmpty) |
|||
SelectableText( |
|||
message.content, |
|||
style: TextStyle( |
|||
color: isUser ? Colors.white : Colors.black, |
|||
fontSize: 16.sp, |
|||
height: 1.5, |
|||
letterSpacing: 0.2, |
|||
), |
|||
textAlign: TextAlign.left, |
|||
maxLines: null, |
|||
enableInteractiveSelection: true, |
|||
textWidthBasis: TextWidthBasis.longestLine, |
|||
), |
|||
|
|||
// 加载指示器 |
|||
if (message.isIntermediate) |
|||
Padding( |
|||
padding: EdgeInsets.only(top: message.content.isNotEmpty ? 8.h : 0), |
|||
child: _buildLoadingIndicator(), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildLoadingIndicator() { |
|||
return Row( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
SizedBox( |
|||
width: 12.w, |
|||
height: 12.h, |
|||
child: CircularProgressIndicator( |
|||
strokeWidth: 2, |
|||
valueColor: AlwaysStoppedAnimation<Color>( |
|||
isUser ? Colors.white : AppColors.primary, |
|||
), |
|||
), |
|||
), |
|||
SizedBox(width: 8.w), |
|||
Text( |
|||
'思考中...', |
|||
style: TextStyle( |
|||
color: isUser ? Colors.white : Colors.black, |
|||
fontSize: 12.sp, |
|||
), |
|||
), |
|||
], |
|||
); |
|||
} |
|||
} |
|||
@ -1,135 +0,0 @@ |
|||
import 'dart:convert'; |
|||
import 'package:get/get.dart'; |
|||
import '../../modules/chat/models/message_model.dart'; |
|||
import '../../core/utils/logger.dart'; |
|||
import 'package:chat_storage/chat_storage.dart'; |
|||
|
|||
/// 聊天记录管理服务,负责保存和加载聊天记录 |
|||
class ChatHistoryService extends GetxService { |
|||
// 最大历史记录数量 |
|||
static const int _maxHistoryCount = 200; // 每个对话最多保存的消息数 |
|||
|
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
Logger.info('ChatHistoryService 已初始化'); |
|||
} |
|||
|
|||
/// 保存聊天记录 |
|||
/// |
|||
/// [agentId] 代理ID |
|||
/// [messages] 消息列表 |
|||
Future<void> saveHistory(String agentId, List<Message> messages) async { |
|||
try { |
|||
if (messages.isEmpty) return; |
|||
|
|||
// 获取最新的消息 |
|||
final latestMessage = messages.last; |
|||
|
|||
// 如果是加载中的消息则跳过 |
|||
if (latestMessage.isIntermediate) return; |
|||
|
|||
// 直接插入最新的消息到数据库 |
|||
await ChatStorage.saveMessage( |
|||
sessionId: agentId, |
|||
message: latestMessage.content, |
|||
sender: latestMessage.role, |
|||
); |
|||
|
|||
// 检查并清理过多的历史记录 |
|||
await _cleanupOldMessages(agentId); |
|||
} catch (e) { |
|||
Logger.error('保存聊天记录失败', e.toString()); |
|||
} |
|||
} |
|||
|
|||
/// 清理旧消息,保持每个会话的消息数量不超过最大限制 |
|||
Future<void> _cleanupOldMessages(String agentId) async { |
|||
try { |
|||
final pagedResult = await ChatStorage.getMessages( |
|||
sessionId: agentId, |
|||
page: 1, |
|||
pageSize: 1, // 只获取总数信息 |
|||
); |
|||
|
|||
// 如果消息总数超过最大限制,删除最早的消息 |
|||
if (pagedResult.totalCount > _maxHistoryCount) { |
|||
final messagesToDelete = pagedResult.totalCount - _maxHistoryCount; |
|||
// 获取最早的N条消息ID |
|||
final oldestMessages = await ChatStorage.getMessages( |
|||
sessionId: agentId, |
|||
page: 1, |
|||
pageSize: messagesToDelete, |
|||
); |
|||
|
|||
// 构建要删除的消息ID列表 |
|||
final List<int> messageIds = oldestMessages.messages |
|||
.where((m) => m.id != null) |
|||
.map((m) => m.id!) |
|||
.toList(); |
|||
|
|||
// 删除这些消息 |
|||
if (messageIds.isNotEmpty) { |
|||
await ChatStorage.deleteMessages(messageIds: messageIds); |
|||
} |
|||
} |
|||
} catch (e) { |
|||
Logger.error('清理旧消息失败', e); |
|||
} |
|||
} |
|||
|
|||
/// 加载聊天记录 |
|||
/// |
|||
/// [agentId] 代理ID |
|||
/// 返回消息列表,如果没有找到或解析错误则返回空列表 |
|||
Future<List<Message>> loadHistory(String agentId) async { |
|||
try { |
|||
final pagedResult = await ChatStorage.getMessages( |
|||
sessionId: agentId, |
|||
page: 1, |
|||
pageSize: _maxHistoryCount, |
|||
); |
|||
|
|||
if (pagedResult.messages.isEmpty) { |
|||
return []; |
|||
} |
|||
|
|||
// 将ChatStorage.ChatMessage转换为Message |
|||
final messages = pagedResult.messages.map((chatMessage) { |
|||
return Message( |
|||
role: chatMessage.sender, |
|||
content: chatMessage.message, |
|||
timestamp: DateTime.parse( |
|||
chatMessage.timestamp), // 直接使用chatMessage的timestamp |
|||
// isLoading 默认false |
|||
// isLoading: metadata['isLoading'] ?? false, |
|||
); |
|||
}).toList(); |
|||
|
|||
return messages; |
|||
} catch (e) { |
|||
Logger.error('加载聊天记录失败', e); |
|||
return []; |
|||
} |
|||
} |
|||
|
|||
/// 清除特定代理的聊天记录 |
|||
Future<void> clearHistory(String agentId) async { |
|||
try { |
|||
await ChatStorage.deleteMessages(sessionId: agentId); |
|||
Logger.info('已清除聊天记录,代理: $agentId'); |
|||
} catch (e) { |
|||
Logger.error('清除聊天记录失败', e); |
|||
} |
|||
} |
|||
|
|||
/// 清除所有聊天记录 |
|||
Future<void> clearAllHistory() async { |
|||
try { |
|||
await ChatStorage.clearDatabase(); |
|||
Logger.info('已清除所有聊天记录'); |
|||
} catch (e) { |
|||
Logger.error('清除所有聊天记录失败', e); |
|||
} |
|||
} |
|||
} |
|||
@ -1,343 +0,0 @@ |
|||
import 'dart:async'; |
|||
import 'package:flutter/services.dart'; |
|||
import 'package:get/get.dart'; |
|||
import '../../../core/utils/logger.dart'; |
|||
import 'package:flutter_dotenv/flutter_dotenv.dart'; |
|||
import '../../../modules/chat/models/message_model.dart'; |
|||
import '../../models/events/voice_interaction_event.dart'; |
|||
import '../chat_history_service.dart'; |
|||
|
|||
/// Android语音交互服务 |
|||
/// |
|||
/// 该服务提供了与Android端的VoiceInteractionService.kt通信的接口, |
|||
/// 用于管理后台语音交互服务的生命周期和接收语音交互事件 |
|||
class AndroidVoiceInteractionService extends GetxService { |
|||
static AndroidVoiceInteractionService get to => Get.find(); |
|||
// 方法通道和事件通道 |
|||
static const MethodChannel _channel = MethodChannel('com.deep_voice.voice_interaction'); |
|||
static const EventChannel _eventChannel = EventChannel('com.deep_voice.voice_interaction_events'); |
|||
|
|||
// 服务状态 |
|||
final _isServiceRunning = false.obs; |
|||
bool get isServiceRunning => _isServiceRunning.value; |
|||
|
|||
// 事件流控制器 |
|||
StreamController<VoiceInteractionEvent>? _eventStreamController; |
|||
Stream<VoiceInteractionEvent>? _eventStream; |
|||
@override |
|||
Stream<VoiceInteractionEvent> get eventStream => _eventStream ?? Stream.empty(); |
|||
|
|||
// 事件通道状态 |
|||
StreamSubscription? _eventSubscription; |
|||
|
|||
// 配置信息 |
|||
late String _azureSpeechKey; |
|||
late String _azureSpeechRegion; |
|||
late String _volcanoAiApiKey; |
|||
|
|||
// 初始化状态标志 |
|||
static bool _isInitialized = false; |
|||
|
|||
// 当前会话历史 |
|||
final List<Map<String, String>> _messageHistory = []; |
|||
|
|||
// 服务ID |
|||
static const String _serviceId = 'android_assistant'; |
|||
|
|||
/// 构造函数 |
|||
AndroidVoiceInteractionService() { |
|||
|
|||
} |
|||
|
|||
/// 从环境变量加载配置 |
|||
void _loadConfig() { |
|||
_azureSpeechKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; |
|||
_azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; |
|||
_volcanoAiApiKey = dotenv.env['VOLCANO_AI_API_KEY'] ?? ''; |
|||
|
|||
if (_azureSpeechKey.isEmpty || _azureSpeechRegion.isEmpty) { |
|||
Logger.warning('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); |
|||
} |
|||
|
|||
if (_volcanoAiApiKey.isEmpty) { |
|||
Logger.warning('未找到火山 AI API 密钥。请在 .env 文件中设置 VOLCANO_AI_API_KEY'); |
|||
} |
|||
} |
|||
|
|||
/// 设置事件通道 |
|||
void _setupEventChannel() { |
|||
_eventSubscription = _eventChannel |
|||
.receiveBroadcastStream() |
|||
.listen((event) { |
|||
if (event is Map) { |
|||
final String eventType = event['type'] as String? ?? ''; |
|||
|
|||
// 直接处理事件,不再等待channelReady |
|||
_handleVoiceInteractionEvent(event); |
|||
} |
|||
}, onError: (error) { |
|||
Logger.error('语音交互事件通道错误: ${error.toString()}'); |
|||
}); |
|||
} |
|||
|
|||
/// 创建事件流 |
|||
void _createEventStream() { |
|||
_eventStreamController = StreamController<VoiceInteractionEvent>.broadcast(); |
|||
_eventStream = _eventStreamController?.stream; |
|||
} |
|||
|
|||
/// 初始化服务 |
|||
@override |
|||
Future<AndroidVoiceInteractionService> initialize() async { |
|||
try { |
|||
if (_isInitialized) { |
|||
return this; |
|||
} |
|||
_isInitialized = true; |
|||
|
|||
_loadConfig(); |
|||
_setupEventChannel(); |
|||
_createEventStream(); |
|||
// 检查服务是否正在运行 |
|||
await _checkServiceStatus(); |
|||
|
|||
await _startService(); |
|||
|
|||
Logger.info('语音交互服务初始化完成'); |
|||
return this; |
|||
} catch (e) { |
|||
Logger.error('语音交互服务初始化失败: ${e.toString()}'); |
|||
return this; |
|||
} |
|||
} |
|||
|
|||
@override |
|||
void onWakeup() { |
|||
Logger.info('语音交互服务唤醒'); |
|||
} |
|||
|
|||
/// 检查服务状态 |
|||
Future<void> _checkServiceStatus() async { |
|||
try { |
|||
final bool isRunning = await _channel.invokeMethod('isVoiceInteractionServiceRunning') ?? false; |
|||
_isServiceRunning.value = isRunning; |
|||
Logger.info('语音交互服务状态: ${isRunning ? "运行中" : "未运行"}'); |
|||
} catch (e) { |
|||
Logger.error('检查语音交互服务状态失败: ${e.toString()}'); |
|||
_isServiceRunning.value = false; |
|||
} |
|||
} |
|||
|
|||
/// 启动语音交互服务 |
|||
Future<bool> _startService() async { |
|||
if (_isServiceRunning.value) { |
|||
Logger.info('语音交互服务已经在运行'); |
|||
return true; |
|||
} |
|||
|
|||
try { |
|||
final bool result = await _channel.invokeMethod('startVoiceInteractionService', { |
|||
'azure_speech_key': _azureSpeechKey, |
|||
'azure_speech_region': _azureSpeechRegion, |
|||
'volcano_ai_api_key': _volcanoAiApiKey, |
|||
}) ?? false; |
|||
|
|||
if (result) { |
|||
_isServiceRunning.value = true; |
|||
Logger.info('语音交互服务启动成功'); |
|||
} else { |
|||
Logger.error('语音交互服务启动失败'); |
|||
} |
|||
|
|||
return result; |
|||
} catch (e) { |
|||
Logger.error('启动语音交互服务失败: ${e.toString()}'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// 停止语音交互服务 |
|||
Future<bool> _stopService() async { |
|||
if (!_isServiceRunning.value) { |
|||
Logger.info('语音交互服务未运行'); |
|||
return true; |
|||
} |
|||
|
|||
try { |
|||
final bool result = await _channel.invokeMethod('stopVoiceInteractionService') ?? false; |
|||
|
|||
if (result) { |
|||
_isServiceRunning.value = false; |
|||
Logger.info('语音交互服务停止成功'); |
|||
} else { |
|||
Logger.error('语音交互服务停止失败'); |
|||
} |
|||
|
|||
return result; |
|||
} catch (e) { |
|||
Logger.error('停止语音交互服务失败: ${e.toString()}'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// 暂停语音交互(停止TTS和语音识别,但保持服务运行) |
|||
@override |
|||
Future<bool> pauseVoiceInteraction() async { |
|||
if (!_isServiceRunning.value) { |
|||
Logger.info('语音交互服务未运行,无法暂停'); |
|||
return false; |
|||
} |
|||
|
|||
try { |
|||
final bool result = await _channel.invokeMethod('pauseVoiceInteraction') ?? false; |
|||
|
|||
if (result) { |
|||
Logger.info('语音交互暂停成功'); |
|||
} else { |
|||
Logger.error('语音交互暂停失败'); |
|||
} |
|||
|
|||
return result; |
|||
} catch (e) { |
|||
Logger.error('暂停语音交互失败: ${e.toString()}'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// 处理来自原生端的语音交互事件 |
|||
void _handleVoiceInteractionEvent(dynamic event) { |
|||
if (event is! Map || _eventStreamController == null) return; |
|||
|
|||
final Map<dynamic, dynamic> eventMap = event; |
|||
final String eventType = eventMap['type'] as String? ?? ''; |
|||
final int timestamp = eventMap['timestamp'] as int? ?? 0; |
|||
|
|||
// 添加时间戳日志,帮助调试 |
|||
Logger.info('收到原生端事件: ${eventType.toString()}, 时间戳: $timestamp, 当前时间: ${DateTime.now().millisecondsSinceEpoch}'); |
|||
|
|||
switch (eventType) { |
|||
case 'chatHistory': |
|||
final String agentId = eventMap['agentId'] as String? ?? ''; |
|||
final String userMessage = eventMap['userMessage'] as String? ?? ''; |
|||
final String assistantMessage = eventMap['assistantMessage'] as String? ?? ''; |
|||
|
|||
// 添加到消息历史 |
|||
if (userMessage.isNotEmpty) { |
|||
_messageHistory.add({'role': 'user', 'content': userMessage}); |
|||
_messageHistory.add({'role': 'assistant', 'content': assistantMessage}); |
|||
|
|||
// 保持历史记录在一定长度 |
|||
while (_messageHistory.length > 10) { |
|||
_messageHistory.removeAt(0); |
|||
} |
|||
} |
|||
|
|||
_eventStreamController?.add(ChatHistoryEvent( |
|||
agentId: agentId, |
|||
userMessage: userMessage, |
|||
assistantMessage: assistantMessage, |
|||
timestamp: timestamp, |
|||
)); |
|||
|
|||
// 保存聊天记录 |
|||
_saveChatHistory(agentId, userMessage, assistantMessage, timestamp); |
|||
|
|||
Logger.info('收到聊天历史事件: agentId=$agentId'); |
|||
break; |
|||
|
|||
case 'recognitionStarted': |
|||
_eventStreamController?.add(RecognitionStartedEvent( |
|||
timestamp: timestamp, |
|||
)); |
|||
break; |
|||
|
|||
default: |
|||
Logger.warning('收到未知类型的语音交互事件: ${eventType.toString()}'); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
@override |
|||
void onClose() { |
|||
// 清理资源 |
|||
_stopService(); |
|||
_eventSubscription?.cancel(); |
|||
_eventStreamController?.close(); |
|||
super.onClose(); |
|||
} |
|||
|
|||
/// 保存聊天记录 |
|||
Future<void> _saveChatHistory(String agentId, String userMessage, String assistantMessage, int timestamp) async { |
|||
try { |
|||
if (userMessage.isEmpty) { |
|||
return; |
|||
} |
|||
|
|||
final userMsg = Message( |
|||
role: 'user', |
|||
content: userMessage, |
|||
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp), |
|||
); |
|||
|
|||
final assistantMsg = Message( |
|||
role: 'assistant', |
|||
content: assistantMessage, |
|||
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp + 1), // 确保助手消息时间戳晚于用户消息 |
|||
); |
|||
|
|||
// 获取ChatHistoryService实例 |
|||
final chatHistoryService = Get.find<ChatHistoryService>(); |
|||
|
|||
// 加载现有历史记录 |
|||
final existingMessages = await chatHistoryService.loadHistory(agentId); |
|||
|
|||
// 添加新消息 |
|||
existingMessages.addAll([userMsg, assistantMsg]); |
|||
|
|||
// 保存更新后的历史记录 |
|||
await chatHistoryService.saveHistory(agentId, existingMessages); |
|||
} catch (e) { |
|||
Logger.error('保存聊天记录失败: ${e.toString()}'); |
|||
} |
|||
} |
|||
|
|||
/// 是否处于活跃状态 |
|||
@override |
|||
bool isActive() { |
|||
return _isServiceRunning.value; |
|||
} |
|||
|
|||
/// 获取当前对话历史 |
|||
@override |
|||
List<Map<String, String>> getMessageHistory() { |
|||
return List<Map<String, String>>.from(_messageHistory); |
|||
} |
|||
|
|||
/// 获取当前服务ID |
|||
@override |
|||
String getServiceId() { |
|||
return _serviceId; |
|||
} |
|||
} |
|||
|
|||
|
|||
/// 聊天历史事件 |
|||
class ChatHistoryEvent extends VoiceInteractionEvent { |
|||
final String agentId; |
|||
final String userMessage; |
|||
final String assistantMessage; |
|||
|
|||
ChatHistoryEvent({ |
|||
required this.agentId, |
|||
required this.userMessage, |
|||
required this.assistantMessage, |
|||
required int timestamp, |
|||
}) : super(timestamp: timestamp); |
|||
} |
|||
|
|||
/// 语音识别开始事件 |
|||
class RecognitionStartedEvent extends VoiceInteractionEvent { |
|||
RecognitionStartedEvent({ |
|||
required int timestamp, |
|||
}) : super(timestamp: timestamp); |
|||
} |
|||
@ -1,466 +0,0 @@ |
|||
import 'dart:async'; |
|||
import 'package:get/get.dart'; |
|||
import '../asr_service.dart'; |
|||
import '../tts_service.dart'; |
|||
import '../volcano_ai_service.dart'; |
|||
import '../chat_history_service.dart'; |
|||
import '../../../modules/chat/models/message_model.dart'; |
|||
import '../../models/events/voice_interaction_event.dart'; |
|||
import '../../../core/utils/logger.dart'; |
|||
|
|||
enum VoiceInteractionState { |
|||
idle, // 等待唤醒 |
|||
active, // 活跃状态 - 可以同时识别和播放 |
|||
} |
|||
|
|||
class IosVoiceInteractionService extends GetxService { |
|||
final AsrService _asrService = Get.find<AsrService>(); |
|||
final TtsService _ttsService = Get.find<TtsService>(); |
|||
final VolcanoAIService _aiService = Get.find<VolcanoAIService>(); |
|||
final ChatHistoryService _chatHistoryService = Get.find<ChatHistoryService>(); |
|||
|
|||
// 当前状态 |
|||
final Rx<VoiceInteractionState> state = VoiceInteractionState.idle.obs; |
|||
|
|||
// 是否识别到用户语音 |
|||
final RxBool isSpeechDetected = false.obs; |
|||
|
|||
// 是否正在处理AI响应 |
|||
final RxBool isProcessingAI = false.obs; |
|||
|
|||
// 是否正在播放TTS |
|||
final RxBool isSpeaking = false.obs; |
|||
|
|||
// 对话历史记录 |
|||
final List<Map<String, String>> _messageHistory = []; |
|||
|
|||
// 最大历史记录数 |
|||
static const int _maxHistorySize = 10; |
|||
|
|||
// 超时计时器 |
|||
Timer? _inactivityTimer; |
|||
|
|||
// 流订阅 |
|||
StreamSubscription? _recognitionSubscription; |
|||
|
|||
// 当前识别的文本 |
|||
String _currentRecognizedText = ''; |
|||
// 音频输入源 |
|||
bool _audioSourceType = false; |
|||
// 事件流控制器 |
|||
final StreamController<VoiceInteractionEvent> _eventStreamController = |
|||
StreamController<VoiceInteractionEvent>.broadcast(); |
|||
|
|||
// 获取事件流 |
|||
@override |
|||
Stream<VoiceInteractionEvent> get eventStream => |
|||
_eventStreamController.stream; |
|||
|
|||
// 标识符,用于存储聊天历史 |
|||
final String _agentId = 'ios_assistant'; |
|||
|
|||
/// 初始化服务 |
|||
@override |
|||
Future<IosVoiceInteractionService> initialize() async { |
|||
try { |
|||
// 保持idle状态,不自动启动语音识别 |
|||
Logger.info('iOS语音交互服务初始化完成,处于idle状态'); |
|||
} catch (e) { |
|||
Logger.error('iOS语音交互服务初始化失败: ${e.toString()}'); |
|||
} |
|||
|
|||
return this; |
|||
} |
|||
|
|||
// 处理唤醒事件 |
|||
void onWakeup() { |
|||
_ttsService.speakOnce('我在呢!'); |
|||
isSpeaking.value = true; |
|||
|
|||
if (state.value == VoiceInteractionState.idle) { |
|||
_activateInteraction(); |
|||
} |
|||
} |
|||
|
|||
// 激活交互 |
|||
void _activateInteraction() { |
|||
// 设置状态为活跃 |
|||
state.value = VoiceInteractionState.active; |
|||
|
|||
// 开始连续监听 |
|||
_startContinuousListening(); |
|||
|
|||
// 启动不活动计时器 |
|||
_startInactivityTimer(); |
|||
|
|||
// 发送识别开始事件 |
|||
_sendRecognitionStartedEvent(); |
|||
} |
|||
|
|||
// 开始连续监听 |
|||
Future<void> _startContinuousListening() async { |
|||
// 停止之前的监听 |
|||
_recognitionSubscription?.cancel(); |
|||
|
|||
try { |
|||
// 开始连续识别 |
|||
final recognitionStream = |
|||
await _asrService.startContinuousRecognition(_audioSourceType); |
|||
|
|||
// 监听识别结果 |
|||
_recognitionSubscription = |
|||
recognitionStream.listen(_handleRecognitionEvent, onError: (error) { |
|||
Logger.error('语音识别错误: ${error.toString()}'); |
|||
_startContinuousListening(); // 尝试重新启动 |
|||
}); |
|||
|
|||
Logger.info('开始连续语音识别'); |
|||
} catch (e) { |
|||
Logger.error('启动语音识别失败: ${e.toString()}'); |
|||
} |
|||
} |
|||
|
|||
// 处理识别事件 |
|||
void _handleRecognitionEvent(RecognitionEvent event) { |
|||
// 更新活动时间 |
|||
_updateActivityTime(); |
|||
|
|||
switch (event.type) { |
|||
case RecognitionEventType.intermediateResult: |
|||
// 检测用户是否开始说话 |
|||
if (event.text.isNotEmpty && !isSpeechDetected.value) { |
|||
isSpeechDetected.value = true; |
|||
|
|||
// 用户开始讲话时,立即中断当前响应 |
|||
_interruptCurrentResponse("检测到用户开始讲话,中断当前响应"); |
|||
} |
|||
break; |
|||
|
|||
case RecognitionEventType.finalResult: |
|||
if (event.text.isNotEmpty) { |
|||
// 最终结果,处理用户输入 |
|||
_processUserInput(event.text); |
|||
} |
|||
// 重置语音检测状态 |
|||
isSpeechDetected.value = false; |
|||
break; |
|||
|
|||
case RecognitionEventType.sessionStarted: |
|||
Logger.info('语音识别会话开始'); |
|||
break; |
|||
|
|||
case RecognitionEventType.sessionStopped: |
|||
Logger.info('语音识别会话结束'); |
|||
|
|||
break; |
|||
|
|||
case RecognitionEventType.error: |
|||
case RecognitionEventType.canceled: |
|||
Logger.error('语音识别错误: ${event.error}'); |
|||
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
// 中断当前响应 |
|||
void _interruptCurrentResponse(String reason) { |
|||
Logger.info(reason); |
|||
|
|||
// 如果正在活跃状态,需要中断当前操作 |
|||
if (state.value == VoiceInteractionState.active) { |
|||
// 停止TTS播放 |
|||
_ttsService.stop(); |
|||
isSpeaking.value = false; |
|||
isProcessingAI.value = false; |
|||
} |
|||
} |
|||
|
|||
// 处理用户输入 |
|||
Future<void> _processUserInput(String text) async { |
|||
Logger.info('处理用户输入: $text'); |
|||
|
|||
// 设置为处理状态 |
|||
isProcessingAI.value = true; |
|||
|
|||
// 添加用户消息到历史记录 |
|||
final userMessage = {'role': 'user', 'content': text}; |
|||
_addToHistory(userMessage); |
|||
|
|||
// 创建用户消息对象 |
|||
final userMsg = Message( |
|||
role: 'user', |
|||
content: text, |
|||
timestamp: DateTime.now(), |
|||
); |
|||
|
|||
// AI响应处理标志 |
|||
bool isProcessingCancelled = false; |
|||
|
|||
try { |
|||
// 构建消息历史 |
|||
final messageHistory = _buildMessageHistory(); |
|||
|
|||
// 调用AI服务获取响应 |
|||
final responseStream = _aiService.sendMessageStream( |
|||
messages: messageHistory, |
|||
systemPrompt: "你是一个智能助手,请简明扼要地回答问题。", |
|||
); |
|||
|
|||
String aiResponse = ''; |
|||
|
|||
// 等待AI响应 |
|||
await for (final chunk in responseStream) { |
|||
// 检查是否被用户打断 |
|||
if (isSpeechDetected.value) { |
|||
// 用户开始说话,标记处理被取消 |
|||
isProcessingCancelled = true; |
|||
Logger.info('AI响应生成过程中被用户打断'); |
|||
break; |
|||
} |
|||
|
|||
// 如果状态已改变(可能由其他原因导致),停止处理 |
|||
if (state.value != VoiceInteractionState.active) { |
|||
isProcessingCancelled = true; |
|||
break; |
|||
} |
|||
|
|||
aiResponse += chunk; |
|||
|
|||
// 立即播放当前文本块,实现边生成边播放 |
|||
_speakStreamResponse(chunk); |
|||
} |
|||
|
|||
// 如果处理被取消,不继续后续操作 |
|||
if (isProcessingCancelled) { |
|||
isProcessingAI.value = false; |
|||
return; |
|||
} |
|||
|
|||
// 确保完整响应被处理 |
|||
if (aiResponse.isNotEmpty) { |
|||
// 将AI响应添加到历史记录 |
|||
final assistantMessage = {'role': 'assistant', 'content': aiResponse}; |
|||
_addToHistory(assistantMessage); |
|||
|
|||
// 创建助手消息对象 |
|||
final assistantMsg = Message( |
|||
role: 'assistant', |
|||
content: aiResponse, |
|||
timestamp: DateTime.now(), |
|||
); |
|||
|
|||
// 保存聊天历史 |
|||
await _saveChatHistory(userMsg, assistantMsg); |
|||
|
|||
// 发送聊天历史事件 |
|||
_sendChatHistoryEvent(userMsg.content, assistantMsg.content); |
|||
|
|||
// 刷新TTS流,确保所有文本都被播放 |
|||
await _ttsService.flushStream(); |
|||
isSpeaking.value = false; |
|||
} |
|||
|
|||
// 处理完成 |
|||
isProcessingAI.value = false; |
|||
} catch (e) { |
|||
Logger.error('AI响应处理失败: ${e.toString()}'); |
|||
// 错误恢复 |
|||
isProcessingAI.value = false; |
|||
} |
|||
} |
|||
|
|||
// 构建用于AI服务的消息历史 |
|||
List<Map<String, String>> _buildMessageHistory() { |
|||
return List<Map<String, String>>.from(_messageHistory); |
|||
} |
|||
|
|||
// 添加消息到历史记录 |
|||
void _addToHistory(Map<String, String> message) { |
|||
_messageHistory.add(message); |
|||
|
|||
// 限制历史记录大小 |
|||
while (_messageHistory.length > _maxHistorySize) { |
|||
_messageHistory.removeAt(0); |
|||
} |
|||
} |
|||
|
|||
// 播放流式响应 |
|||
void _speakStreamResponse(String chunk) { |
|||
if (chunk.isEmpty) return; |
|||
|
|||
// 更新播放状态 |
|||
isSpeaking.value = true; |
|||
|
|||
// 使用流式TTS播放 |
|||
_ttsService.speakStream(chunk); |
|||
|
|||
// 更新活动时间 |
|||
_updateActivityTime(); |
|||
} |
|||
|
|||
// 停止TTS |
|||
void _stopTts() { |
|||
_ttsService.stop(); |
|||
isSpeaking.value = false; |
|||
} |
|||
|
|||
// 更新最后活动时间 |
|||
DateTime _lastActivityTime = DateTime.now(); |
|||
void _updateActivityTime() { |
|||
_lastActivityTime = DateTime.now(); |
|||
} |
|||
|
|||
// 启动不活动计时器(长时间无交互会切换到空闲状态) |
|||
void _startInactivityTimer() { |
|||
_cancelInactivityTimer(); |
|||
|
|||
_inactivityTimer = Timer.periodic(Duration(seconds: 5), (timer) { |
|||
// 计算空闲时间 |
|||
final idleTime = DateTime.now().difference(_lastActivityTime).inSeconds; |
|||
|
|||
// 如果空闲超过30秒,且不在播放或检测到语音,切换到空闲 |
|||
if (idleTime > 8 && |
|||
!isSpeaking.value && |
|||
!isProcessingAI.value && |
|||
!isSpeechDetected.value) { |
|||
_resetToIdle(); |
|||
timer.cancel(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
// 取消不活动计时器 |
|||
void _cancelInactivityTimer() { |
|||
_inactivityTimer?.cancel(); |
|||
_inactivityTimer = null; |
|||
} |
|||
|
|||
// 重置到空闲状态 |
|||
void _resetToIdle() { |
|||
_ttsService.speakOnce('没有听到声音,暂停对话,双击耳机唤醒!'); |
|||
_cancelInactivityTimer(); |
|||
|
|||
// 停止TTS |
|||
_stopTts(); |
|||
|
|||
// 停止语音识别 |
|||
_recognitionSubscription?.cancel(); |
|||
_asrService.stopContinuousRecognition(); |
|||
|
|||
// 重置状态 |
|||
state.value = VoiceInteractionState.idle; |
|||
isSpeechDetected.value = false; |
|||
isProcessingAI.value = false; |
|||
isSpeaking.value = false; |
|||
|
|||
// 清空历史记录 |
|||
_messageHistory.clear(); |
|||
|
|||
Logger.info('语音交互已重置为空闲状态'); |
|||
} |
|||
|
|||
// 暂停语音交互 |
|||
@override |
|||
void pauseVoiceInteraction() { |
|||
if (state.value != VoiceInteractionState.idle) { |
|||
_stopTts(); |
|||
_recognitionSubscription?.cancel(); |
|||
_asrService.stopContinuousRecognition(); |
|||
|
|||
// 重置所有状态 |
|||
state.value = VoiceInteractionState.idle; |
|||
isSpeechDetected.value = false; |
|||
isProcessingAI.value = false; |
|||
isSpeaking.value = false; |
|||
|
|||
// 清空历史记录 |
|||
_messageHistory.clear(); |
|||
} |
|||
} |
|||
|
|||
// 恢复语音交互 |
|||
void resumeVoiceInteraction() { |
|||
if (state.value == VoiceInteractionState.idle) { |
|||
_activateInteraction(); |
|||
} |
|||
} |
|||
|
|||
// 手动停止交互 |
|||
void stopInteraction() { |
|||
_resetToIdle(); |
|||
} |
|||
|
|||
// 获取当前状态 |
|||
VoiceInteractionState getCurrentState() { |
|||
return state.value; |
|||
} |
|||
|
|||
// 是否处于活跃状态 |
|||
@override |
|||
bool isActive() { |
|||
return state.value != VoiceInteractionState.idle; |
|||
} |
|||
|
|||
// 获取当前对话历史 |
|||
@override |
|||
List<Map<String, String>> getMessageHistory() { |
|||
return List<Map<String, String>>.from(_messageHistory); |
|||
} |
|||
|
|||
// 获取当前服务ID |
|||
@override |
|||
String getServiceId() { |
|||
return _agentId; |
|||
} |
|||
|
|||
// 释放资源 |
|||
@override |
|||
void onClose() { |
|||
_cancelInactivityTimer(); |
|||
_recognitionSubscription?.cancel(); |
|||
_resetToIdle(); |
|||
_eventStreamController.close(); |
|||
super.onClose(); |
|||
} |
|||
|
|||
// 保存聊天历史 |
|||
Future<void> _saveChatHistory(Message userMsg, Message assistantMsg) async { |
|||
try { |
|||
// 加载现有历史记录 |
|||
final existingMessages = await _chatHistoryService.loadHistory(_agentId); |
|||
|
|||
// 添加新消息 |
|||
existingMessages.addAll([userMsg, assistantMsg]); |
|||
|
|||
// 保存更新后的历史记录 |
|||
await _chatHistoryService.saveHistory(_agentId, existingMessages); |
|||
|
|||
Logger.info('保存聊天历史记录成功'); |
|||
} catch (e) { |
|||
Logger.error('保存聊天历史记录失败: ${e.toString()}'); |
|||
} |
|||
} |
|||
|
|||
// 发送聊天历史事件 |
|||
void _sendChatHistoryEvent(String userMessage, String assistantMessage) { |
|||
final event = ChatHistoryEvent( |
|||
agentId: _agentId, |
|||
userMessage: userMessage, |
|||
assistantMessage: assistantMessage, |
|||
timestamp: DateTime.now().millisecondsSinceEpoch, |
|||
); |
|||
|
|||
_eventStreamController.add(event); |
|||
Logger.info('已发送聊天历史事件'); |
|||
} |
|||
|
|||
// 发送识别开始事件 |
|||
void _sendRecognitionStartedEvent() { |
|||
final event = RecognitionStartedEvent( |
|||
timestamp: DateTime.now().millisecondsSinceEpoch, |
|||
); |
|||
|
|||
_eventStreamController.add(event); |
|||
Logger.info('已发送识别开始事件'); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue