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