You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
350 lines
10 KiB
350 lines
10 KiB
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';
|
|
import '../../routes/app_routes.dart';
|
|
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 EventChannel _eventChannel =
|
|
EventChannel('com.deep_voice.voice_interaction_events');
|
|
|
|
// 服务状态
|
|
final _isServiceRunning = false.obs;
|
|
bool get isServiceRunning => _isServiceRunning.value;
|
|
|
|
// 流控制器
|
|
final _eventStreamController =
|
|
StreamController<VoiceInteractionEvent>.broadcast();
|
|
|
|
// 事件流
|
|
Stream<VoiceInteractionEvent> get eventStream =>
|
|
_eventStreamController.stream;
|
|
|
|
// 事件通道订阅
|
|
StreamSubscription? _eventSubscription;
|
|
|
|
// 标记是否初始化
|
|
bool _isInitialized = false;
|
|
|
|
// 配置信息
|
|
late String _azureSpeechKey;
|
|
late String _azureSpeechRegion;
|
|
late String _openaiApiKey;
|
|
late String _openaiBaseUrl;
|
|
late String _openaiModel;
|
|
late String _volcanoSpeechAppId;
|
|
late String _volcanoSpeechAppToken;
|
|
// MCP配置信息
|
|
String? _mcpConfig;
|
|
|
|
// 聊天历史服务
|
|
late final ChatHistoryService _chatHistoryService;
|
|
|
|
/// 设置事件通道
|
|
void _setupEventChannel() {
|
|
_eventSubscription = _eventChannel
|
|
.receiveBroadcastStream()
|
|
.listen(_handleVoiceInteractionEvent, onError: (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'] ?? '';
|
|
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');
|
|
}
|
|
|
|
// 加载MCP配置
|
|
_loadMcpConfig();
|
|
}
|
|
|
|
/// 加载MCP配置
|
|
void _loadMcpConfig() {
|
|
try {
|
|
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':
|
|
// 语音识别开始事件
|
|
final recognitionEvent = RecognitionStartedEvent(
|
|
timestamp: DateTime.now().millisecondsSinceEpoch,
|
|
);
|
|
_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 chatHistoryEvent = ChatHistoryEvent(
|
|
agentId: agentId,
|
|
userMessage: userMessage,
|
|
assistantMessage: assistantMessage,
|
|
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);
|
|
Logger.info('已导航到翻译界面');
|
|
} catch (e) {
|
|
Logger.error('导航到翻译界面失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 保存聊天历史
|
|
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), // 确保助手消息时间戳晚于用户消息
|
|
);
|
|
|
|
// 加载现有历史记录
|
|
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>();
|
|
} catch (e) {
|
|
Logger.warning('获取ChatHistoryService失败,将创建新实例');
|
|
_chatHistoryService = Get.put(ChatHistoryService());
|
|
}
|
|
|
|
// 设置事件通道
|
|
_setupEventChannel();
|
|
|
|
// 加载配置
|
|
_loadConfig();
|
|
|
|
// 检查服务是否已在运行
|
|
final bool running = await checkServiceStatus();
|
|
|
|
// 如果服务未运行,启动服务
|
|
if (!running) {
|
|
// 启动语音交互服务
|
|
final success = await startService();
|
|
if (!success) {
|
|
Logger.error('语音交互服务启动失败');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
_isInitialized = true;
|
|
Logger.info('语音交互服务初始化完成');
|
|
return true;
|
|
} catch (e) {
|
|
Logger.error('语音交互服务初始化失败: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 启动语音交互服务
|
|
Future<bool> startService() async {
|
|
try {
|
|
Logger.info('启动语音交互服务...');
|
|
|
|
// 构建参数
|
|
final Map<String, dynamic> params = {
|
|
'azure_speech_key': _azureSpeechKey,
|
|
'azure_speech_region': _azureSpeechRegion,
|
|
'openai_api_key': _openaiApiKey,
|
|
'openai_base_url': _openaiBaseUrl,
|
|
'openai_model': _openaiModel,
|
|
'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;
|
|
|
|
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;
|
|
_isServiceRunning.value = result;
|
|
return result;
|
|
} catch (e) {
|
|
Logger.error('检查服务状态时发生错误: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 暂停语音交互
|
|
Future<bool> pauseVoiceInteraction() async {
|
|
try {
|
|
Logger.info('暂停后台语音交互...');
|
|
|
|
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() {
|
|
_eventSubscription?.cancel();
|
|
_eventStreamController.close();
|
|
super.onClose();
|
|
}
|
|
}
|
|
|