18 changed files with 1288 additions and 532 deletions
@ -0,0 +1,191 @@ |
|||
import Foundation |
|||
import AVFoundation |
|||
|
|||
/// 统一音频会话管理类 |
|||
/// 用于统一管理所有组件的音频会话配置 |
|||
@objc class AudioSessionManager: NSObject { |
|||
// MARK: - 单例实现 |
|||
|
|||
@objc static let shared = AudioSessionManager() |
|||
|
|||
// 音频会话 |
|||
private let audioSession = AVAudioSession.sharedInstance() |
|||
|
|||
// 是否已经配置过音频会话 |
|||
private var isConfigured = false |
|||
|
|||
// 当前配置类型 |
|||
private var currentCategory: AVAudioSession.Category = .playAndRecord |
|||
private var currentMode: AVAudioSession.Mode = .default |
|||
private var currentOptions: AVAudioSession.CategoryOptions = [.allowBluetooth, .allowBluetoothA2DP] |
|||
|
|||
// 私有初始化方法,确保只能通过shared访问 |
|||
private override init() { |
|||
super.init() |
|||
NSLog("[AudioSessionManager] 音频会话管理器已初始化") |
|||
} |
|||
|
|||
// MARK: - 音频会话配置 |
|||
|
|||
/// 在应用启动时调用一次性配置方法 |
|||
/// - Parameters: |
|||
/// - category: 音频会话类别 |
|||
/// - mode: 音频会话模式 |
|||
/// - options: 音频会话选项 |
|||
/// - Returns: 是否配置成功 |
|||
@objc func initialConfiguration(category: AVAudioSession.Category = .playAndRecord, |
|||
mode: AVAudioSession.Mode = .default, |
|||
options: AVAudioSession.CategoryOptions = [.allowBluetooth, .allowBluetoothA2DP]) -> Bool { |
|||
if isConfigured { |
|||
NSLog("[AudioSessionManager] 音频会话已经配置过,忽略初始化请求") |
|||
return true |
|||
} |
|||
|
|||
do { |
|||
NSLog("[AudioSessionManager] 执行一次性音频会话配置: \(category)") |
|||
try audioSession.setCategory(category, mode: mode, options: options) |
|||
try audioSession.setActive(true) |
|||
|
|||
// 记录当前配置 |
|||
currentCategory = category |
|||
currentMode = mode |
|||
currentOptions = options |
|||
isConfigured = true |
|||
|
|||
return true |
|||
} catch { |
|||
NSLog("[AudioSessionManager] 一次性配置音频会话失败: \(error.localizedDescription)") |
|||
return false |
|||
} |
|||
} |
|||
|
|||
/// 配置音频会话,适用于语音识别和合成 |
|||
@objc func configureForVoiceProcessing() throws { |
|||
if !applyConfigurationIfNeeded(category: .playAndRecord, |
|||
mode: .spokenAudio, |
|||
options: [.allowBluetooth, .allowBluetoothA2DP, .duckOthers]) { |
|||
NSLog("[AudioSessionManager] 使用已存在的音频会话配置,跳过语音处理模式配置") |
|||
} |
|||
} |
|||
|
|||
/// 配置音频会话,适用于蓝牙耳机连接和监听 |
|||
@objc func configureForBluetoothHeadset() throws { |
|||
if !applyConfigurationIfNeeded(category: .playAndRecord, |
|||
mode: .default, |
|||
options: [.allowBluetooth, .allowBluetoothA2DP]) { |
|||
NSLog("[AudioSessionManager] 使用已存在的音频会话配置,跳过蓝牙耳机模式配置") |
|||
} |
|||
} |
|||
|
|||
/// 配置音频会话,适用于媒体播放和蓝牙媒体按钮 |
|||
@objc func configureForMediaPlayback() throws { |
|||
if !applyConfigurationIfNeeded(category: .playback, |
|||
mode: .default, |
|||
options: [.allowBluetooth, .allowBluetoothA2DP]) { |
|||
NSLog("[AudioSessionManager] 使用已存在的音频会话配置,跳过媒体播放模式配置") |
|||
} |
|||
} |
|||
|
|||
/// 配置自定义音频会话 |
|||
@objc func configure(category: AVAudioSession.Category, |
|||
mode: AVAudioSession.Mode, |
|||
options: AVAudioSession.CategoryOptions) throws { |
|||
if !applyConfigurationIfNeeded(category: category, mode: mode, options: options) { |
|||
NSLog("[AudioSessionManager] 使用已存在的音频会话配置,跳过自定义模式配置") |
|||
} |
|||
} |
|||
|
|||
/// 内部方法:如果尚未配置,应用新的配置 |
|||
private func applyConfigurationIfNeeded(category: AVAudioSession.Category, |
|||
mode: AVAudioSession.Mode, |
|||
options: AVAudioSession.CategoryOptions) -> Bool { |
|||
// 如果已经配置过,不再重复配置 |
|||
if isConfigured { |
|||
return false |
|||
} |
|||
|
|||
do { |
|||
NSLog("[AudioSessionManager] 应用音频会话配置: \(category)") |
|||
try audioSession.setCategory(category, mode: mode, options: options) |
|||
try audioSession.setActive(true) |
|||
|
|||
// 记录当前配置 |
|||
currentCategory = category |
|||
currentMode = mode |
|||
currentOptions = options |
|||
isConfigured = true |
|||
|
|||
return true |
|||
} catch { |
|||
NSLog("[AudioSessionManager] 配置音频会话失败: \(error.localizedDescription)") |
|||
return false |
|||
} |
|||
} |
|||
|
|||
/// 获取当前音频会话的配置信息 |
|||
@objc func getCurrentConfiguration() -> [String: Any] { |
|||
return [ |
|||
"isConfigured": isConfigured, |
|||
"category": "\(currentCategory)", |
|||
"mode": "\(currentMode)" |
|||
] |
|||
} |
|||
|
|||
/// 仅在特殊情况下使用:强制重新配置音频会话 |
|||
@objc func forceReconfigure() -> Bool { |
|||
isConfigured = false |
|||
|
|||
do { |
|||
NSLog("[AudioSessionManager] 强制重新配置音频会话") |
|||
try audioSession.setCategory(currentCategory, mode: currentMode, options: currentOptions) |
|||
try audioSession.setActive(true) |
|||
isConfigured = true |
|||
return true |
|||
} catch { |
|||
NSLog("[AudioSessionManager] 强制重新配置失败: \(error.localizedDescription)") |
|||
return false |
|||
} |
|||
} |
|||
|
|||
/// 激活音频会话(如果已配置) |
|||
@objc func activateSession() throws { |
|||
if isConfigured { |
|||
try audioSession.setActive(true) |
|||
} else { |
|||
NSLog("[AudioSessionManager] 警告: 尝试激活未配置的音频会话") |
|||
// 应用默认配置并激活 |
|||
try applyConfigurationIfNeeded(category: .playAndRecord, |
|||
mode: .default, |
|||
options: [.allowBluetooth, .allowBluetoothA2DP]) |
|||
} |
|||
} |
|||
|
|||
/// 停用音频会话(保留配置) |
|||
@objc func deactivateSession() throws { |
|||
try audioSession.setActive(false, options: .notifyOthersOnDeactivation) |
|||
} |
|||
|
|||
/// 获取当前音频会话路由描述 |
|||
@objc func getCurrentRoute() -> AVAudioSessionRouteDescription { |
|||
return audioSession.currentRoute |
|||
} |
|||
|
|||
/// 添加路由变化监听器 |
|||
@objc func addRouteChangeListener(_ observer: Any, selector: Selector) { |
|||
NotificationCenter.default.addObserver( |
|||
observer, |
|||
selector: selector, |
|||
name: AVAudioSession.routeChangeNotification, |
|||
object: nil |
|||
) |
|||
} |
|||
|
|||
/// 移除路由变化监听器 |
|||
@objc func removeRouteChangeListener(_ observer: Any) { |
|||
NotificationCenter.default.removeObserver( |
|||
observer, |
|||
name: AVAudioSession.routeChangeNotification, |
|||
object: nil |
|||
) |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
import 'package:meta/meta.dart'; |
|||
|
|||
/// 语音交互事件基类 |
|||
abstract class VoiceInteractionEvent { |
|||
final int timestamp; |
|||
|
|||
VoiceInteractionEvent({required this.timestamp}); |
|||
} |
|||
|
|||
/// 聊天历史事件 |
|||
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); |
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
class Message { |
|||
final String role; // 'user' or 'assistant' |
|||
final String content; |
|||
final DateTime timestamp; |
|||
final bool isLoading; |
|||
|
|||
Message({ |
|||
required this.role, |
|||
required this.content, |
|||
required this.timestamp, |
|||
this.isLoading = false, |
|||
}); |
|||
|
|||
// 从JSON构造函数 |
|||
factory Message.fromJson(Map<String, dynamic> json) { |
|||
return Message( |
|||
role: json['role'] as String, |
|||
content: json['content'] as String, |
|||
timestamp: DateTime.parse(json['timestamp'] as String), |
|||
isLoading: json['isLoading'] as bool? ?? false, |
|||
); |
|||
} |
|||
|
|||
// 转换为JSON |
|||
Map<String, dynamic> toJson() { |
|||
return { |
|||
'role': role, |
|||
'content': content, |
|||
'timestamp': timestamp.toIso8601String(), |
|||
'isLoading': isLoading, |
|||
}; |
|||
} |
|||
|
|||
// 创建一个加载中的消息 |
|||
factory Message.loading() { |
|||
return Message( |
|||
role: 'assistant', |
|||
content: '', |
|||
timestamp: DateTime.now(), |
|||
isLoading: true, |
|||
); |
|||
} |
|||
|
|||
// 复制并修改 |
|||
Message copyWith({ |
|||
String? role, |
|||
String? content, |
|||
DateTime? timestamp, |
|||
bool? isLoading, |
|||
}) { |
|||
return Message( |
|||
role: role ?? this.role, |
|||
content: content ?? this.content, |
|||
timestamp: timestamp ?? this.timestamp, |
|||
isLoading: isLoading ?? this.isLoading, |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,341 @@ |
|||
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'; |
|||
import '../voice_interaction_service.dart'; |
|||
|
|||
/// Android语音交互服务 |
|||
/// |
|||
/// 该服务提供了与Android端的VoiceInteractionService.kt通信的接口, |
|||
/// 用于管理后台语音交互服务的生命周期和接收语音交互事件 |
|||
class AndroidVoiceInteractionService extends GetxService implements VoiceInteractionService { |
|||
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'); |
|||
}); |
|||
} |
|||
|
|||
/// 创建事件流 |
|||
void _createEventStream() { |
|||
_eventStreamController = StreamController<VoiceInteractionEvent>.broadcast(); |
|||
_eventStream = _eventStreamController?.stream; |
|||
} |
|||
|
|||
/// 初始化服务 |
|||
@override |
|||
Future<VoiceInteractionService> 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'); |
|||
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'); |
|||
_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'); |
|||
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'); |
|||
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'); |
|||
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, 时间戳: $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'); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
@override |
|||
void onClose() { |
|||
// 清理资源 |
|||
_stopService(); |
|||
_eventSubscription?.cancel(); |
|||
_eventStreamController?.close(); |
|||
super.onClose(); |
|||
} |
|||
|
|||
/// 保存聊天记录 |
|||
void _saveChatHistory(String agentId, String userMessage, String assistantMessage, int timestamp) { |
|||
try { |
|||
// 获取ChatHistoryService实例 |
|||
final chatHistoryService = Get.find<ChatHistoryService>(); |
|||
|
|||
// 创建用户消息和助手消息 |
|||
final userMsg = Message( |
|||
role: 'user', |
|||
content: userMessage, |
|||
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp), |
|||
); |
|||
|
|||
final assistantMsg = Message( |
|||
role: 'assistant', |
|||
content: assistantMessage, |
|||
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp + 1), // 确保助手消息时间戳晚于用户消息 |
|||
); |
|||
|
|||
// 加载现有历史记录 |
|||
final existingMessages = chatHistoryService.loadHistory(agentId); |
|||
|
|||
// 添加新消息 |
|||
existingMessages.addAll([userMsg, assistantMsg]); |
|||
|
|||
// 保存更新后的历史记录 |
|||
chatHistoryService.saveHistory(agentId, existingMessages); |
|||
} catch (e) { |
|||
Logger.error('保存聊天记录失败: $e'); |
|||
} |
|||
} |
|||
|
|||
/// 是否处于活跃状态 |
|||
@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); |
|||
} |
|||
@ -0,0 +1,468 @@ |
|||
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'; |
|||
import '../voice_interaction_service.dart'; |
|||
|
|||
enum VoiceInteractionState { |
|||
idle, // 等待唤醒 |
|||
active, // 活跃状态 - 可以同时识别和播放 |
|||
} |
|||
|
|||
class IosVoiceInteractionService extends GetxService implements VoiceInteractionService { |
|||
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 = ''; |
|||
|
|||
// 事件流控制器 |
|||
final StreamController<VoiceInteractionEvent> _eventStreamController = |
|||
StreamController<VoiceInteractionEvent>.broadcast(); |
|||
|
|||
// 获取事件流 |
|||
@override |
|||
Stream<VoiceInteractionEvent> get eventStream => _eventStreamController.stream; |
|||
|
|||
// 标识符,用于存储聊天历史 |
|||
final String _agentId = 'ios_assistant'; |
|||
|
|||
/// 初始化服务 |
|||
@override |
|||
Future<VoiceInteractionService> initialize() async { |
|||
try { |
|||
// 保持idle状态,不自动启动语音识别 |
|||
Logger.info('iOS语音交互服务初始化完成,处于idle状态'); |
|||
} catch (e) { |
|||
Logger.error('iOS语音交互服务初始化失败: $e'); |
|||
} |
|||
|
|||
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(); |
|||
|
|||
// 监听识别结果 |
|||
_recognitionSubscription = recognitionStream.listen( |
|||
_handleRecognitionEvent, |
|||
onError: (error) { |
|||
Logger.error('语音识别错误: $error'); |
|||
_startContinuousListening(); // 尝试重新启动 |
|||
} |
|||
); |
|||
|
|||
Logger.info('开始连续语音识别'); |
|||
} catch (e) { |
|||
Logger.error('启动语音识别失败: $e'); |
|||
|
|||
} |
|||
} |
|||
|
|||
// 处理识别事件 |
|||
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(), |
|||
); |
|||
|
|||
// 保存聊天历史 |
|||
_saveChatHistory(userMsg, assistantMsg); |
|||
|
|||
// 发送聊天历史事件 |
|||
_sendChatHistoryEvent(userMsg.content, assistantMsg.content); |
|||
|
|||
// 刷新TTS流,确保所有文本都被播放 |
|||
await _ttsService.flushStream(); |
|||
isSpeaking.value = false; |
|||
} |
|||
|
|||
// 处理完成 |
|||
isProcessingAI.value = false; |
|||
} catch (e) { |
|||
Logger.error('AI响应处理失败: $e'); |
|||
// 错误恢复 |
|||
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(); |
|||
} |
|||
|
|||
// 保存聊天历史 |
|||
void _saveChatHistory(Message userMsg, Message assistantMsg) { |
|||
try { |
|||
// 加载现有历史记录 |
|||
final existingMessages = _chatHistoryService.loadHistory(_agentId); |
|||
|
|||
// 添加新消息 |
|||
existingMessages.addAll([userMsg, assistantMsg]); |
|||
|
|||
// 保存更新后的历史记录 |
|||
_chatHistoryService.saveHistory(_agentId, existingMessages); |
|||
|
|||
Logger.info('保存聊天历史记录成功'); |
|||
} catch (e) { |
|||
Logger.error('保存聊天历史记录失败: $e'); |
|||
} |
|||
} |
|||
|
|||
// 发送聊天历史事件 |
|||
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('已发送识别开始事件'); |
|||
} |
|||
} |
|||
|
|||
@ -1,334 +1,33 @@ |
|||
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 'chat_history_service.dart'; |
|||
import 'speech_impl/azure_asr_service.dart'; |
|||
import 'speech_impl/azure_tts_service.dart'; |
|||
import 'asr_service.dart'; |
|||
import 'tts_service.dart'; |
|||
import '../models/events/voice_interaction_event.dart'; |
|||
|
|||
/// 语音交互服务 |
|||
/// 语音交互服务抽象基类 |
|||
/// |
|||
/// 该服务提供了与Android端的VoiceInteractionService.kt通信的接口, |
|||
/// 用于管理后台语音交互服务的生命周期和接收语音交互事件 |
|||
class VoiceInteractionService extends GetxService { |
|||
static VoiceInteractionService 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; |
|||
Stream<VoiceInteractionEvent>? get eventStream => _eventStream; |
|||
|
|||
// 事件通道状态 |
|||
bool _isEventChannelReady = false; |
|||
Completer<void>? _eventChannelReadyCompleter; |
|||
StreamSubscription? _eventSubscription; |
|||
|
|||
// 配置信息 |
|||
late String _azureSpeechKey; |
|||
late String _azureSpeechRegion; |
|||
late String _volcanoAiApiKey; |
|||
|
|||
// 初始化状态标志 |
|||
static bool _isInitialized = false; |
|||
|
|||
/// 构造函数 |
|||
VoiceInteractionService() { |
|||
|
|||
} |
|||
|
|||
/// 从环境变量加载配置 |
|||
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() { |
|||
_eventChannelReadyCompleter = Completer<void>(); |
|||
|
|||
_eventSubscription = _eventChannel |
|||
.receiveBroadcastStream() |
|||
.listen((event) { |
|||
if (event is Map) { |
|||
final String eventType = event['type'] as String? ?? ''; |
|||
|
|||
// 处理通道准备好的事件 |
|||
if (eventType == 'channelReady') { |
|||
_isEventChannelReady = true; |
|||
if (!_eventChannelReadyCompleter!.isCompleted) { |
|||
_eventChannelReadyCompleter!.complete(); |
|||
} |
|||
return; |
|||
} |
|||
|
|||
_handleVoiceInteractionEvent(event); |
|||
} |
|||
}, onError: (error) { |
|||
Logger.error('语音交互事件通道错误: $error'); |
|||
}); |
|||
} |
|||
|
|||
/// 创建事件流 |
|||
void _createEventStream() { |
|||
_eventStreamController = StreamController<VoiceInteractionEvent>.broadcast(); |
|||
_eventStream = _eventStreamController?.stream; |
|||
} |
|||
|
|||
/// 等待事件通道准备好 |
|||
Future<bool> _waitForEventChannel({Duration timeout = const Duration(seconds: 5)}) async { |
|||
if (_isEventChannelReady) return true; |
|||
|
|||
try { |
|||
await _eventChannelReadyCompleter!.future.timeout(timeout); |
|||
return true; |
|||
} on TimeoutException { |
|||
Logger.error('等待语音交互事件通道准备好超时'); |
|||
return false; |
|||
} |
|||
} |
|||
/// 定义了iOS和Android平台共用的语音交互服务接口 |
|||
abstract class VoiceInteractionService { |
|||
/// 事件流,用于发布语音交互相关事件 |
|||
Stream<VoiceInteractionEvent> get eventStream; |
|||
|
|||
/// 初始化服务 |
|||
Future<VoiceInteractionService> initialize() async { |
|||
return this; |
|||
try { |
|||
if (_isInitialized) { |
|||
return this; |
|||
} |
|||
_isInitialized = true; |
|||
/// |
|||
/// 返回服务实例自身,以支持链式调用 |
|||
Future<VoiceInteractionService> initialize(); |
|||
|
|||
/// 暂停语音交互 |
|||
/// |
|||
/// 停止正在进行的语音识别和TTS播放 |
|||
void pauseVoiceInteraction(); |
|||
|
|||
_loadConfig(); |
|||
_setupEventChannel(); |
|||
_createEventStream(); |
|||
// 检查服务是否正在运行 |
|||
await _checkServiceStatus(); |
|||
|
|||
// 等待事件通道准备好 |
|||
await _waitForEventChannel(); |
|||
|
|||
Logger.info('语音交互服务初始化完成'); |
|||
return this; |
|||
} catch (e) { |
|||
Logger.error('语音交互服务初始化失败: $e'); |
|||
return this; |
|||
} |
|||
} |
|||
|
|||
/// 检查服务状态 |
|||
Future<void> _checkServiceStatus() async { |
|||
try { |
|||
final bool isRunning = await _channel.invokeMethod('isVoiceInteractionServiceRunning') ?? false; |
|||
_isServiceRunning.value = isRunning; |
|||
Logger.info('语音交互服务状态: ${isRunning ? "运行中" : "未运行"}'); |
|||
} catch (e) { |
|||
Logger.error('检查语音交互服务状态失败: $e'); |
|||
_isServiceRunning.value = false; |
|||
} |
|||
} |
|||
|
|||
/// 启动语音交互服务 |
|||
Future<bool> startService() async { |
|||
return true; |
|||
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'); |
|||
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'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// 暂停语音交互(停止TTS和语音识别,但保持服务运行) |
|||
Future<bool> pauseVoiceInteraction() async { |
|||
return true; |
|||
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'); |
|||
return false; |
|||
} |
|||
} |
|||
/// 唤醒语音交互 |
|||
void onWakeup(); |
|||
|
|||
/// 处理来自原生端的语音交互事件 |
|||
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, 时间戳: $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? ?? ''; |
|||
|
|||
_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'); |
|||
break; |
|||
} |
|||
} |
|||
/// 是否处于活跃状态 |
|||
bool isActive(); |
|||
|
|||
@override |
|||
void onClose() { |
|||
// 清理资源 |
|||
_eventSubscription?.cancel(); |
|||
_eventStreamController?.close(); |
|||
super.onClose(); |
|||
} |
|||
/// 获取当前对话历史 |
|||
List<Map<String, String>> getMessageHistory(); |
|||
|
|||
/// 保存聊天记录 |
|||
void _saveChatHistory(String agentId, String userMessage, String assistantMessage, int timestamp) { |
|||
try { |
|||
// 获取ChatHistoryService实例 |
|||
final chatHistoryService = Get.find<ChatHistoryService>(); |
|||
|
|||
// 创建用户消息和助手消息 |
|||
final userMsg = Message( |
|||
role: 'user', |
|||
content: userMessage, |
|||
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp), |
|||
); |
|||
|
|||
final assistantMsg = Message( |
|||
role: 'assistant', |
|||
content: assistantMessage, |
|||
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp + 1), // 确保助手消息时间戳晚于用户消息 |
|||
); |
|||
|
|||
// 加载现有历史记录 |
|||
final existingMessages = chatHistoryService.loadHistory(agentId); |
|||
|
|||
// 添加新消息 |
|||
existingMessages.addAll([userMsg, assistantMsg]); |
|||
|
|||
// 保存更新后的历史记录 |
|||
chatHistoryService.saveHistory(agentId, existingMessages); |
|||
} catch (e) { |
|||
Logger.error('保存聊天记录失败: $e'); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// 语音交互事件基类 |
|||
abstract class VoiceInteractionEvent { |
|||
final int timestamp; |
|||
|
|||
VoiceInteractionEvent({required this.timestamp}); |
|||
} |
|||
|
|||
/// 聊天历史事件 |
|||
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); |
|||
/// 获取当前服务ID |
|||
String getServiceId(); |
|||
} |
|||
Loading…
Reference in new issue