|
|
|
@ -1,8 +1,10 @@ |
|
|
|
import 'dart:async'; |
|
|
|
import 'dart:convert'; |
|
|
|
import 'package:deep_voice/data/models/appconfig_model.dart'; |
|
|
|
import 'package:flutter/services.dart'; |
|
|
|
import 'package:get/get.dart'; |
|
|
|
import 'package:flutter_dotenv/flutter_dotenv.dart'; |
|
|
|
import 'package:get_storage/get_storage.dart'; |
|
|
|
import '../models/events/voice_interaction_event.dart'; |
|
|
|
import '../../core/utils/logger.dart'; |
|
|
|
import '../../modules/chat/models/message_model.dart'; |
|
|
|
@ -14,29 +16,33 @@ import 'chat_history_service.dart'; |
|
|
|
/// 管理与平台原生语音交互服务的通信,提供统一的接口供应用使用 |
|
|
|
class VoiceInteractionService extends GetxService { |
|
|
|
static VoiceInteractionService get to => Get.find<VoiceInteractionService>(); |
|
|
|
|
|
|
|
final GetStorage _storage = GetStorage(); |
|
|
|
// 方法通道 |
|
|
|
static const MethodChannel _channel = MethodChannel('com.deep_voice.voice_interaction'); |
|
|
|
|
|
|
|
static const MethodChannel _channel = |
|
|
|
MethodChannel('com.deep_voice.voice_interaction'); |
|
|
|
|
|
|
|
// 事件通道 |
|
|
|
static const EventChannel _eventChannel = EventChannel('com.deep_voice.voice_interaction_events'); |
|
|
|
|
|
|
|
static const EventChannel _eventChannel = |
|
|
|
EventChannel('com.deep_voice.voice_interaction_events'); |
|
|
|
|
|
|
|
// 服务状态 |
|
|
|
final _isServiceRunning = false.obs; |
|
|
|
bool get isServiceRunning => _isServiceRunning.value; |
|
|
|
|
|
|
|
|
|
|
|
// 流控制器 |
|
|
|
final _eventStreamController = StreamController<VoiceInteractionEvent>.broadcast(); |
|
|
|
|
|
|
|
final _eventStreamController = |
|
|
|
StreamController<VoiceInteractionEvent>.broadcast(); |
|
|
|
|
|
|
|
// 事件流 |
|
|
|
Stream<VoiceInteractionEvent> get eventStream => _eventStreamController.stream; |
|
|
|
|
|
|
|
Stream<VoiceInteractionEvent> get eventStream => |
|
|
|
_eventStreamController.stream; |
|
|
|
|
|
|
|
// 事件通道订阅 |
|
|
|
StreamSubscription? _eventSubscription; |
|
|
|
|
|
|
|
|
|
|
|
// 标记是否初始化 |
|
|
|
bool _isInitialized = false; |
|
|
|
|
|
|
|
|
|
|
|
// 配置信息 |
|
|
|
late String _azureSpeechKey; |
|
|
|
late String _azureSpeechRegion; |
|
|
|
@ -47,61 +53,73 @@ class VoiceInteractionService extends GetxService { |
|
|
|
late String _volcanoSpeechAppToken; |
|
|
|
// MCP配置信息 |
|
|
|
String? _mcpConfig; |
|
|
|
|
|
|
|
|
|
|
|
// 聊天历史服务 |
|
|
|
late final ChatHistoryService _chatHistoryService; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// 设置事件通道 |
|
|
|
void _setupEventChannel() { |
|
|
|
_eventSubscription = _eventChannel |
|
|
|
.receiveBroadcastStream() |
|
|
|
.listen(_handleVoiceInteractionEvent, onError: (error) { |
|
|
|
Logger.error('语音交互事件通道错误: $error'); |
|
|
|
}); |
|
|
|
Logger.error('语音交互事件通道错误: $error'); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 从环境变量加载配置 |
|
|
|
void _loadConfig() { |
|
|
|
_azureSpeechKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; |
|
|
|
_azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; |
|
|
|
_openaiApiKey = dotenv.env['OPENAI_API_KEY'] ?? ''; |
|
|
|
_openaiBaseUrl = dotenv.env['OPENAI_BASE_URL'] ?? ''; |
|
|
|
_openaiModel = dotenv.env['OPENAI_MODEL'] ?? ''; |
|
|
|
_volcanoSpeechAppId = dotenv.env['VOLCANO_SPEECH_APP_ID'] ?? ''; |
|
|
|
_volcanoSpeechAppToken = dotenv.env['VOLCANO_SPEECH_APP_TOKEN'] ?? ''; |
|
|
|
|
|
|
|
// _azureSpeechKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; |
|
|
|
// _azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; |
|
|
|
// _openaiApiKey = dotenv.env['OPENAI_API_KEY'] ?? ''; |
|
|
|
// _openaiBaseUrl = dotenv.env['OPENAI_BASE_URL'] ?? ''; |
|
|
|
// _openaiModel = dotenv.env['OPENAI_MODEL'] ?? ''; |
|
|
|
// _volcanoSpeechAppId = dotenv.env['VOLCANO_SPEECH_APP_ID'] ?? ''; |
|
|
|
// _volcanoSpeechAppToken = dotenv.env['VOLCANO_SPEECH_APP_TOKEN'] ?? ''; |
|
|
|
final _env = _storage.read("ENV") as Map<String, String>; |
|
|
|
_azureSpeechKey = _env['AZURE_SPEECH_KEY'] ?? ''; |
|
|
|
_azureSpeechRegion = _env['AZURE_SPEECH_REGION'] ?? ''; |
|
|
|
_openaiApiKey = _env['OPENAI_API_KEY'] ?? ''; |
|
|
|
_openaiBaseUrl = _env['OPENAI_BASE_URL'] ?? ''; |
|
|
|
_openaiModel = _env['OPENAI_MODEL'] ?? ''; |
|
|
|
_volcanoSpeechAppId = _env['VOLCANO_SPEECH_APP_ID'] ?? ''; |
|
|
|
_volcanoSpeechAppToken = _env['VOLCANO_SPEECH_APP_TOKEN'] ?? ''; |
|
|
|
|
|
|
|
if (_azureSpeechKey.isEmpty || _azureSpeechRegion.isEmpty) { |
|
|
|
Logger.warning('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); |
|
|
|
Logger.warning( |
|
|
|
'未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); |
|
|
|
} |
|
|
|
|
|
|
|
// 加载MCP配置 |
|
|
|
_loadMcpConfig(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 加载MCP配置 |
|
|
|
void _loadMcpConfig() { |
|
|
|
try { |
|
|
|
rootBundle.loadString('.mcp.json').then((config) { |
|
|
|
_mcpConfig = config; |
|
|
|
Logger.info('成功加载MCP配置, 配置内容: $_mcpConfig'); |
|
|
|
}); |
|
|
|
final mcps = _storage.read("MCPS") as Map<String, DBMCPServer>; |
|
|
|
String config = jsonEncode(mcps); |
|
|
|
_mcpConfig = config; |
|
|
|
Logger.info('成功加载MCP配置, 配置内容: $_mcpConfig'); |
|
|
|
// rootBundle.loadString('.mcp.json').then((config) { |
|
|
|
// _mcpConfig = config; |
|
|
|
// Logger.info('成功加载MCP配置, 配置内容: $_mcpConfig'); |
|
|
|
// }); |
|
|
|
} catch (e) { |
|
|
|
Logger.warning('加载MCP配置失败: $e'); |
|
|
|
_mcpConfig = null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 处理来自原生层的事件 |
|
|
|
void _handleVoiceInteractionEvent(dynamic event) { |
|
|
|
if (event is! Map) return; |
|
|
|
|
|
|
|
|
|
|
|
final eventMap = event as Map<dynamic, dynamic>; |
|
|
|
final String eventType = eventMap['type'] as String? ?? ''; |
|
|
|
// final int timestamp = eventMap['timestamp'] as int? ?? DateTime.now().millisecondsSinceEpoch; |
|
|
|
|
|
|
|
|
|
|
|
Logger.info('收到语音交互事件: $eventType'); |
|
|
|
|
|
|
|
|
|
|
|
switch (eventType) { |
|
|
|
case 'recognition_started': |
|
|
|
// 语音识别开始事件 |
|
|
|
@ -110,13 +128,14 @@ class VoiceInteractionService extends GetxService { |
|
|
|
); |
|
|
|
_eventStreamController.add(recognitionEvent); |
|
|
|
break; |
|
|
|
|
|
|
|
|
|
|
|
case 'chat_history_updated': |
|
|
|
// 聊天历史更新事件 |
|
|
|
final String agentId = eventMap['agentId'] as String? ?? ''; |
|
|
|
final String userMessage = eventMap['userMessage'] as String? ?? ''; |
|
|
|
final String assistantMessage = eventMap['assistantMessage'] as String? ?? ''; |
|
|
|
|
|
|
|
final String assistantMessage = |
|
|
|
eventMap['assistantMessage'] as String? ?? ''; |
|
|
|
|
|
|
|
final chatHistoryEvent = ChatHistoryEvent( |
|
|
|
agentId: agentId, |
|
|
|
userMessage: userMessage, |
|
|
|
@ -124,72 +143,74 @@ class VoiceInteractionService extends GetxService { |
|
|
|
timestamp: DateTime.now().millisecondsSinceEpoch, |
|
|
|
); |
|
|
|
_eventStreamController.add(chatHistoryEvent); |
|
|
|
|
|
|
|
|
|
|
|
// 保存聊天历史到ChatHistoryService |
|
|
|
_saveChatHistory(agentId, userMessage, assistantMessage); |
|
|
|
break; |
|
|
|
|
|
|
|
|
|
|
|
case 'enter_translation_mode': |
|
|
|
// 进入翻译模式事件 |
|
|
|
Logger.info('收到进入翻译模式事件,正在导航到翻译界面'); |
|
|
|
_navigateToTranslation(); |
|
|
|
|
|
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 导航到翻译界面 |
|
|
|
void _navigateToTranslation() { |
|
|
|
try { |
|
|
|
// 使用GetX导航到翻译页面 |
|
|
|
Get.offNamedUntil(Routes.translation, (route) => route.settings.name == Routes.home); |
|
|
|
Get.offNamedUntil( |
|
|
|
Routes.translation, (route) => route.settings.name == Routes.home); |
|
|
|
Logger.info('已导航到翻译界面'); |
|
|
|
} catch (e) { |
|
|
|
Logger.error('导航到翻译界面失败: $e'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 保存聊天历史 |
|
|
|
Future<void> _saveChatHistory(String agentId, String userMessage, String assistantMessage) async { |
|
|
|
Future<void> _saveChatHistory( |
|
|
|
String agentId, String userMessage, String assistantMessage) async { |
|
|
|
try { |
|
|
|
final timestamp = DateTime.now().millisecondsSinceEpoch; |
|
|
|
|
|
|
|
|
|
|
|
// 创建用户消息和助手消息 |
|
|
|
final userMsg = Message( |
|
|
|
role: 'user', |
|
|
|
content: userMessage, |
|
|
|
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp), |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
final assistantMsg = Message( |
|
|
|
role: 'assistant', |
|
|
|
content: assistantMessage, |
|
|
|
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp + 1), // 确保助手消息时间戳晚于用户消息 |
|
|
|
timestamp: DateTime.fromMillisecondsSinceEpoch( |
|
|
|
timestamp + 1), // 确保助手消息时间戳晚于用户消息 |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
// 加载现有历史记录 |
|
|
|
final existingMessages = await _chatHistoryService.loadHistory(agentId); |
|
|
|
|
|
|
|
|
|
|
|
// 添加新消息 |
|
|
|
existingMessages.addAll([userMsg, assistantMsg]); |
|
|
|
|
|
|
|
|
|
|
|
// 保存更新后的历史记录 |
|
|
|
await _chatHistoryService.saveHistory(agentId, existingMessages); |
|
|
|
|
|
|
|
|
|
|
|
Logger.info('已保存聊天历史: agentId=$agentId'); |
|
|
|
} catch (e) { |
|
|
|
Logger.error('保存聊天历史失败: $e'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 初始化服务 |
|
|
|
Future<bool> initialize() async { |
|
|
|
if (_isInitialized) return true; |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
Logger.info('正在初始化语音交互服务...'); |
|
|
|
|
|
|
|
|
|
|
|
// 获取聊天历史服务 |
|
|
|
try { |
|
|
|
_chatHistoryService = Get.find<ChatHistoryService>(); |
|
|
|
@ -197,16 +218,16 @@ class VoiceInteractionService extends GetxService { |
|
|
|
Logger.warning('获取ChatHistoryService失败,将创建新实例'); |
|
|
|
_chatHistoryService = Get.put(ChatHistoryService()); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 设置事件通道 |
|
|
|
_setupEventChannel(); |
|
|
|
|
|
|
|
|
|
|
|
// 加载配置 |
|
|
|
_loadConfig(); |
|
|
|
|
|
|
|
// 检查服务是否已在运行 |
|
|
|
final bool running = await checkServiceStatus(); |
|
|
|
|
|
|
|
|
|
|
|
// 如果服务未运行,启动服务 |
|
|
|
if (!running) { |
|
|
|
// 启动语音交互服务 |
|
|
|
@ -216,7 +237,7 @@ class VoiceInteractionService extends GetxService { |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
_isInitialized = true; |
|
|
|
Logger.info('语音交互服务初始化完成'); |
|
|
|
return true; |
|
|
|
@ -225,12 +246,12 @@ class VoiceInteractionService extends GetxService { |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 启动语音交互服务 |
|
|
|
Future<bool> startService() async { |
|
|
|
try { |
|
|
|
Logger.info('启动语音交互服务...'); |
|
|
|
|
|
|
|
|
|
|
|
// 构建参数 |
|
|
|
final Map<String, dynamic> params = { |
|
|
|
'azure_speech_key': _azureSpeechKey, |
|
|
|
@ -241,53 +262,55 @@ class VoiceInteractionService extends GetxService { |
|
|
|
'volcano_speech_app_id': _volcanoSpeechAppId, |
|
|
|
'volcano_speech_app_token': _volcanoSpeechAppToken, |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// 如果有MCP配置,添加到参数中 |
|
|
|
if (_mcpConfig != null) { |
|
|
|
params['mcp_config'] = _mcpConfig; |
|
|
|
} |
|
|
|
|
|
|
|
final result = await _channel.invokeMethod<bool>('startService', params) ?? false; |
|
|
|
|
|
|
|
|
|
|
|
final result = |
|
|
|
await _channel.invokeMethod<bool>('startService', params) ?? false; |
|
|
|
|
|
|
|
if (result) { |
|
|
|
_isServiceRunning.value = true; |
|
|
|
Logger.info('语音交互服务已启动'); |
|
|
|
} else { |
|
|
|
Logger.error('启动语音交互服务失败'); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return result; |
|
|
|
} catch (e) { |
|
|
|
Logger.error('启动语音交互服务时发生错误: $e'); |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 停止语音交互服务 |
|
|
|
Future<bool> stopService() async { |
|
|
|
try { |
|
|
|
Logger.info('停止语音交互服务...'); |
|
|
|
|
|
|
|
|
|
|
|
final result = await _channel.invokeMethod<bool>('stopService') ?? false; |
|
|
|
|
|
|
|
|
|
|
|
if (result) { |
|
|
|
_isServiceRunning.value = false; |
|
|
|
Logger.info('语音交互服务已停止'); |
|
|
|
} else { |
|
|
|
Logger.error('停止语音交互服务失败'); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return result; |
|
|
|
} catch (e) { |
|
|
|
Logger.error('停止语音交互服务时发生错误: $e'); |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 检查服务是否运行 |
|
|
|
Future<bool> checkServiceStatus() async { |
|
|
|
try { |
|
|
|
final bool result = await _channel.invokeMethod<bool>('isServiceRunning') ?? false; |
|
|
|
final bool result = |
|
|
|
await _channel.invokeMethod<bool>('isServiceRunning') ?? false; |
|
|
|
_isServiceRunning.value = result; |
|
|
|
return result; |
|
|
|
} catch (e) { |
|
|
|
@ -295,27 +318,28 @@ class VoiceInteractionService extends GetxService { |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 暂停语音交互 |
|
|
|
Future<bool> pauseVoiceInteraction() async { |
|
|
|
try { |
|
|
|
Logger.info('暂停后台语音交互...'); |
|
|
|
|
|
|
|
final result = await _channel.invokeMethod<bool>('pauseVoiceInteraction') ?? false; |
|
|
|
|
|
|
|
|
|
|
|
final result = |
|
|
|
await _channel.invokeMethod<bool>('pauseVoiceInteraction') ?? false; |
|
|
|
|
|
|
|
if (result) { |
|
|
|
Logger.info('语音交互已暂停'); |
|
|
|
} else { |
|
|
|
Logger.error('暂停语音交互失败'); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return result; |
|
|
|
} catch (e) { |
|
|
|
Logger.error('暂停语音交互时发生错误: $e'); |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 资源释放 |
|
|
|
@override |
|
|
|
void onClose() { |
|
|
|
@ -323,4 +347,4 @@ class VoiceInteractionService extends GetxService { |
|
|
|
_eventStreamController.close(); |
|
|
|
super.onClose(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|