diff --git a/lib/data/models/appconfig_model.dart b/lib/data/models/appconfig_model.dart new file mode 100644 index 000000000..9ecb31714 --- /dev/null +++ b/lib/data/models/appconfig_model.dart @@ -0,0 +1,30 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'appconfig_model.g.dart'; // 生成的文件名 + +@JsonSerializable() +class UserGetAppConfigResp { + final Map env; + final Map mcps; + UserGetAppConfigResp({required this.env, required this.mcps}); + + factory UserGetAppConfigResp.fromJson(Map json) => + _$UserGetAppConfigRespFromJson(json); + Map toJson() => _$UserGetAppConfigRespToJson(this); +} + +@JsonSerializable() +class DBMCPServer { + final String servername; + final String url; + final List? tools; + DBMCPServer({ + required this.servername, + required this.url, + required this.tools, + }); + + factory DBMCPServer.fromJson(Map json) => + _$DBMCPServerFromJson(json); + Map toJson() => _$DBMCPServerToJson(this); +} diff --git a/lib/data/models/appconfig_model.g.dart b/lib/data/models/appconfig_model.g.dart new file mode 100644 index 000000000..89da944bf --- /dev/null +++ b/lib/data/models/appconfig_model.g.dart @@ -0,0 +1,37 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'appconfig_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +UserGetAppConfigResp _$UserGetAppConfigRespFromJson( + Map json) => + UserGetAppConfigResp( + env: Map.from(json['env'] as Map), + mcps: (json['mcps'] as Map).map( + (k, e) => MapEntry(k, DBMCPServer.fromJson(e as Map)), + ), + ); + +Map _$UserGetAppConfigRespToJson( + UserGetAppConfigResp instance) => + { + 'env': instance.env, + 'mcps': instance.mcps, + }; + +DBMCPServer _$DBMCPServerFromJson(Map json) => DBMCPServer( + servername: json['servername'] as String, + url: json['url'] as String, + tools: + (json['tools'] as List?)?.map((e) => e as String).toList(), + ); + +Map _$DBMCPServerToJson(DBMCPServer instance) => + { + 'servername': instance.servername, + 'url': instance.url, + 'tools': instance.tools, + }; diff --git a/lib/data/services/network/api.dart b/lib/data/services/network/api.dart index c290e2ce5..5318d8d15 100644 --- a/lib/data/services/network/api.dart +++ b/lib/data/services/network/api.dart @@ -64,4 +64,13 @@ class Api { params: params, ); } + + //app配置 + static getappconfig([params]) { + return DioManager().request( + NWMethod.post, + '/api/home/user_getappconfig', + params: params, + ); + } } diff --git a/lib/data/services/open_ai_service_adapter.dart b/lib/data/services/open_ai_service_adapter.dart index 83fcc1f79..2aeb2af85 100644 --- a/lib/data/services/open_ai_service_adapter.dart +++ b/lib/data/services/open_ai_service_adapter.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'dart:io'; import 'dart:convert'; +import 'package:deep_voice/data/models/appconfig_model.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:get_storage/get_storage.dart'; import 'package:open_ai_service/open_ai_service.dart'; import 'package:get/get.dart'; import 'ai_service.dart'; @@ -26,6 +28,7 @@ class OpenAIServiceAdapter implements AiService { StreamSubscription? _eventSubscription; final StreamController _tokenStreamController = StreamController.broadcast(); + final GetStorage _storage = GetStorage(); /// 构造函数 OpenAIServiceAdapter() { @@ -97,9 +100,13 @@ class OpenAIServiceAdapter implements AiService { Future initialize() async { try { // 从.env文件中读取配置 - final apiKey = dotenv.env['OPENAI_API_KEY'] ?? ''; - final baseUrl = dotenv.env['OPENAI_BASE_URL'] ?? ''; - final model = dotenv.env['OPENAI_MODEL'] ?? ''; + // final apiKey = dotenv.env['OPENAI_API_KEY'] ?? ''; + // final baseUrl = dotenv.env['OPENAI_BASE_URL'] ?? ''; + // final model = dotenv.env['OPENAI_MODEL'] ?? ''; + final _env = _storage.read("ENV") as Map; + final apiKey = _env['OPENAI_API_KEY'] ?? ''; + final baseUrl = _env['OPENAI_BASE_URL'] ?? ''; + final model = _env['OPENAI_MODEL'] ?? ''; printInfo(info: '从.env读取OpenAI配置'); printInfo(info: '基础URL: $baseUrl'); @@ -109,24 +116,24 @@ class OpenAIServiceAdapter implements AiService { printError(info: '错误: OpenAI API密钥未配置,请在.env文件中设置OPENAI_API_KEY'); return false; } - + final _mcps = _storage.read("MCPS") as Map; // 读取.mcp.json文件 - String mcpConfig = ''; - try { - // 从Flutter资源包中加载.mcp.json - mcpConfig = await rootBundle.loadString('.mcp.json'); - printInfo(info: '成功从资源包加载.mcp.json配置文件'); + String mcpConfig = jsonEncode(_mcps); + // try { + // // 从Flutter资源包中加载.mcp.json + // mcpConfig = await rootBundle.loadString('.mcp.json'); + // printInfo(info: '成功从资源包加载.mcp.json配置文件'); - // 验证JSON格式 - final jsonData = jsonDecode(mcpConfig); - if (jsonData is Map && jsonData.containsKey('mcpServers')) { - printInfo(info: '解析到有效的mcpServers配置'); - } else { - printInfo(info: '.mcp.json内容格式不正确,期望包含mcpServers字段'); - } - } catch (e) { - printError(info: '加载.mcp.json资源文件时出错: $e'); - } + // // 验证JSON格式 + // final jsonData = jsonDecode(mcpConfig); + // if (jsonData is Map && jsonData.containsKey('mcpServers')) { + // printInfo(info: '解析到有效的mcpServers配置'); + // } else { + // printInfo(info: '.mcp.json内容格式不正确,期望包含mcpServers字段'); + // } + // } catch (e) { + // printError(info: '加载.mcp.json资源文件时出错: $e'); + // } // 初始化OpenAI服务 final result = await _openAIService.initialize( diff --git a/lib/data/services/speech_impl/azure_asr_service.dart b/lib/data/services/speech_impl/azure_asr_service.dart index e09defb66..dee08ed5e 100644 --- a/lib/data/services/speech_impl/azure_asr_service.dart +++ b/lib/data/services/speech_impl/azure_asr_service.dart @@ -1,76 +1,79 @@ import 'dart:async'; import 'package:flutter/services.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:get_storage/get_storage.dart'; import '../../../core/utils/logger.dart'; import 'package:get/get.dart'; import '../asr_service.dart'; /// Azure 语音识别服务 -/// +/// /// 该服务提供了通过平台通道与 Android 上的 Microsoft Speech SDK 交互的接口 class AzureAsrService extends GetxService implements AsrService { static final AzureAsrService to = Get.put(AzureAsrService()); static const MethodChannel _channel = MethodChannel('azure_speech/asr'); - static const EventChannel _eventChannel = EventChannel('azure_speech/asr_events'); - + static const EventChannel _eventChannel = + EventChannel('azure_speech/asr_events'); + final GetStorage _storage = GetStorage(); bool _isInitialized = false; late final String _subscriptionKey; late final String _serviceRegion; - + final List _defaultSupportedLanguages = ['zh-CN']; @override List get supportedLanguages => _defaultSupportedLanguages; - + // 连续识别相关 bool _isContinuousRecognitionActive = false; StreamController? _eventStreamController; StreamSubscription? _eventSubscription; - + // 最新的识别结果 String _latestRecognizedText = ''; String get latestRecognizedText => _latestRecognizedText; - + // 最新检测到的语言 String _latestDetectedLanguage = ''; String get latestDetectedLanguage => _latestDetectedLanguage; - + AzureAsrService() { _loadConfig(); } - + /// 从环境变量加载配置 void _loadConfig() { - _subscriptionKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; - _serviceRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; - + final _env = _storage.read("ENV") as Map; + _subscriptionKey = _env['AZURE_SPEECH_KEY'] ?? ''; + _serviceRegion = _env['AZURE_SPEECH_REGION'] ?? ''; + if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) { - throw Exception('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); + throw Exception( + '未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); } } - + /// 设置事件通道 void _setupEventChannel() { - _eventSubscription = _eventChannel - .receiveBroadcastStream() - .listen((event) { - if (event is Map) { - _handleRecognitionEvent(event); - } - }, onError: _handleRecognitionError); + _eventSubscription = _eventChannel.receiveBroadcastStream().listen((event) { + if (event is Map) { + _handleRecognitionEvent(event); + } + }, onError: _handleRecognitionError); } - + @override Future initialize({ List? supportedLanguages, }) async { try { - final List languages = supportedLanguages ?? _defaultSupportedLanguages; - + final List languages = + supportedLanguages ?? _defaultSupportedLanguages; + // 检查是否需要重新初始化 if (_isInitialized) { await dispose(); } - + final bool result = await _channel.invokeMethod('initialize', { 'subscriptionKey': _subscriptionKey, 'region': _serviceRegion, @@ -87,22 +90,23 @@ class AzureAsrService extends GetxService implements AsrService { rethrow; } } - + @override Future recognizeOnce() async { if (!_isInitialized) { await initialize(); } - + try { final result = await _channel.invokeMethod('recognizeOnce'); - + if (result is Map) { final String text = result['text'] as String? ?? ''; - final String detectedLanguage = result['detectedLanguage'] as String? ?? ''; + final String detectedLanguage = + result['detectedLanguage'] as String? ?? ''; _latestRecognizedText = text; _latestDetectedLanguage = detectedLanguage; - + return RecognitionEvent.finalResult( text: text, detectedLanguage: detectedLanguage, @@ -112,35 +116,36 @@ class AzureAsrService extends GetxService implements AsrService { _latestRecognizedText = result; return RecognitionEvent.finalResult(text: result); } - + throw Exception('无效的识别结果格式'); } catch (e) { Logger.error('语音识别失败: $e'); return RecognitionEvent.error(e.toString()); } } - + @override Future> startContinuousRecognition() async { if (!_isInitialized) { await initialize(); } - + if (_isContinuousRecognitionActive) { await stopContinuousRecognition(); } - + try { _eventStreamController = StreamController.broadcast(); - + // 开始连续识别 - final bool result = await _channel.invokeMethod('startContinuousRecognition'); - + final bool result = + await _channel.invokeMethod('startContinuousRecognition'); + if (!result) { _cleanupEventStream(); throw Exception('启动连续识别失败'); } - + _isContinuousRecognitionActive = true; return _eventStreamController!.stream; } catch (e) { @@ -149,15 +154,16 @@ class AzureAsrService extends GetxService implements AsrService { rethrow; } } - + @override Future stopContinuousRecognition() async { if (!_isInitialized || !_isContinuousRecognitionActive) { return true; } - + try { - final bool result = await _channel.invokeMethod('stopContinuousRecognition'); + final bool result = + await _channel.invokeMethod('stopContinuousRecognition'); _cleanupEventStream(); return result; } catch (e) { @@ -166,26 +172,27 @@ class AzureAsrService extends GetxService implements AsrService { rethrow; } } - + @override bool isContinuousRecognitionActive() { return _isContinuousRecognitionActive; } - + /// 处理来自原生端的识别事件 void _handleRecognitionEvent(dynamic event) { if (event is! Map || _eventStreamController == null) return; - + final Map eventMap = event; final String eventType = eventMap['type'] as String? ?? ''; - + // 添加日志帮助调试 Logger.debug('收到语音识别事件: $eventType, 数据: $eventMap'); - + switch (eventType) { case 'result': final String text = eventMap['text'] as String? ?? ''; - final String detectedLanguage = eventMap['detectedLanguage'] as String? ?? ''; + final String detectedLanguage = + eventMap['detectedLanguage'] as String? ?? ''; _latestRecognizedText = text; _latestDetectedLanguage = detectedLanguage; _eventStreamController?.add(RecognitionEvent( @@ -194,45 +201,46 @@ class AzureAsrService extends GetxService implements AsrService { detectedLanguage: detectedLanguage, )); break; - + case 'recognizing': final String text = eventMap['text'] as String? ?? ''; - final String detectedLanguage = eventMap['detectedLanguage'] as String? ?? ''; + final String detectedLanguage = + eventMap['detectedLanguage'] as String? ?? ''; _eventStreamController?.add(RecognitionEvent( type: RecognitionEventType.intermediateResult, text: text, detectedLanguage: detectedLanguage, )); break; - + case 'sessionStarted': _eventStreamController?.add(RecognitionEvent( type: RecognitionEventType.sessionStarted, )); break; - + case 'sessionStopped': _isContinuousRecognitionActive = false; _eventStreamController?.add(RecognitionEvent( type: RecognitionEventType.sessionStopped, )); break; - + case 'canceled': _isContinuousRecognitionActive = false; final String reason = eventMap['reason'] as String? ?? ''; final String errorDetails = eventMap['errorDetails'] as String? ?? ''; - + if (reason.isNotEmpty || errorDetails.isNotEmpty) { Logger.error('识别取消: $reason - $errorDetails'); } - + _eventStreamController?.add(RecognitionEvent( type: RecognitionEventType.canceled, error: '$reason: $errorDetails', )); break; - + case 'error': final String error = eventMap['message'] as String? ?? ''; Logger.error('识别错误: $error'); @@ -243,36 +251,36 @@ class AzureAsrService extends GetxService implements AsrService { break; } } - + /// 处理识别事件流错误 void _handleRecognitionError(Object error) { Logger.error('识别事件流错误: $error'); _eventStreamController?.addError(error); _cleanupEventStream(); } - + /// 清理事件流资源 void _cleanupEventStream() { _eventStreamController?.close(); _eventStreamController = null; _isContinuousRecognitionActive = false; } - + @override Future dispose() async { try { if (_isContinuousRecognitionActive) { await stopContinuousRecognition(); } - + // 取消事件订阅 await _eventSubscription?.cancel(); _eventSubscription = null; - + // 通知原生端释放资源 await _channel.invokeMethod('dispose'); _isInitialized = false; - + Logger.info('Azure 语音识别资源已释放'); } catch (e) { Logger.error('释放语音识别资源失败: $e'); @@ -280,4 +288,4 @@ class AzureAsrService extends GetxService implements AsrService { _isInitialized = false; } } -} \ No newline at end of file +} diff --git a/lib/data/services/speech_impl/azure_tts_service.dart b/lib/data/services/speech_impl/azure_tts_service.dart index 1cdb62507..66bd501bc 100644 --- a/lib/data/services/speech_impl/azure_tts_service.dart +++ b/lib/data/services/speech_impl/azure_tts_service.dart @@ -3,27 +3,28 @@ import 'dart:io'; 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 '../../../core/utils/logger.dart'; import '../tts_service.dart'; /// 微软 Text-to-Speech 服务 -/// +/// /// 该服务通过平台通道与原生 Microsoft Speech SDK 交互, /// 提供文本转语音功能。 class AzureTtsService extends GetxService implements TtsService { static final AzureTtsService to = Get.put(AzureTtsService()); static const MethodChannel _channel = MethodChannel('azure_speech/tts'); - + final GetStorage _storage = GetStorage(); bool _isInitialized = false; late final String _subscriptionKey; late final String _serviceRegion; - + // 当前设置 String _currentVoice = 'zh-CN-XiaoxiaoNeural'; int _currentRate = 0; int _currentPitch = 0; int _currentVolume = 100; - + // 支持的语音列表 final List _supportedVoices = [ 'zh-CN-XiaoxiaoNeural', @@ -32,103 +33,98 @@ class AzureTtsService extends GetxService implements TtsService { 'en-US-JennyNeural', 'en-US-GuyNeural' ]; - + // 语音合成队列 final List<_SpeechItem> _textQueue = []; bool _isProcessingQueue = false; - + // 可观察状态 final isEnabled = true.obs; - + // 流式文本缓冲区 String _streamBuffer = ''; - + /// 缓存的播放状态 bool _speakingCache = false; DateTime _lastSpeakingCheck = DateTime.now(); - + @override String get currentVoice => _currentVoice; - + @override bool get isSpeaking { // 如果未初始化,直接返回false if (!_isInitialized) return false; - + // 如果距离上次检查不到50ms,则使用缓存值 if (DateTime.now().difference(_lastSpeakingCheck).inMilliseconds < 50) { return _speakingCache; } - + // 否则异步更新缓存(但仍返回当前缓存值) _checkSpeakingStatus(); return _speakingCache; } - + @override Stream get onEvent => const Stream.empty(); - + @override List get supportedVoices => _supportedVoices; - - + AzureTtsService() { _loadConfig(); } - + /// 从环境变量加载配置 void _loadConfig() { - _subscriptionKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; - _serviceRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; + final _env = _storage.read("ENV") as Map; + _subscriptionKey = _env['AZURE_SPEECH_KEY'] ?? ''; + _serviceRegion = _env['AZURE_SPEECH_REGION'] ?? ''; } @override Future initialize({ List? supportedLanguages, }) async { - // 检查是否需要重新初始化 - if (_isInitialized) { - await dispose(); - } - + // 检查是否需要重新初始化 + if (_isInitialized) { + await dispose(); + } + try { if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) { Logger.error('未找到Azure语音服务配置'); return false; } - - // 更新支持的语音列表(如果提供) final String language = supportedLanguages?.first ?? 'zh-CN'; - - final result = await _channel.invokeMethod('initialize', { - 'subscriptionKey': _subscriptionKey, - 'region': _serviceRegion, - 'language': language, - }); - - - _isInitialized = true; - return result; + final result = await _channel.invokeMethod('initialize', { + 'subscriptionKey': _subscriptionKey, + 'region': _serviceRegion, + 'language': language, + }); + _isInitialized = true; + return result; } catch (e) { Logger.error('初始化失败: $e'); return false; } } - + @override Future setVoice(String voiceName) async { if (!_isInitialized) await initialize(); if (voiceName == _currentVoice) return true; - + try { final result = await _channel.invokeMethod('setVoice', { 'voiceName': voiceName, }); - + if (result) _currentVoice = voiceName; return result; } catch (e) { @@ -136,45 +132,46 @@ class AzureTtsService extends GetxService implements TtsService { return false; } } - + /// 设置语音参数 - Future setSpeechParams({int rate = 0, int pitch = 0, int volume = 100}) async { + Future setSpeechParams( + {int rate = 0, int pitch = 0, int volume = 100}) async { if (!_isInitialized) await initialize(); - + try { final result = await _channel.invokeMethod('setSpeechParams', { 'rate': rate, 'pitch': pitch, 'volume': volume, }); - + if (result) { _currentRate = rate; _currentPitch = pitch; _currentVolume = volume; } - + return result; } catch (e) { Logger.error('设置语音参数失败: $e'); return false; } } - + @override Future speakOnce(String text) async { if (!_isInitialized) await initialize(); if (!isEnabled.value || text.isEmpty) return false; - + try { // 主动设置缓存状态为true _speakingCache = true; - + final result = await _channel.invokeMethod('speakText', {'text': text}); - + // 调用完成后主动检查状态 _checkSpeakingStatus(); - + return result == "OK"; } catch (e) { // 发生错误时重置状态 @@ -183,21 +180,21 @@ class AzureTtsService extends GetxService implements TtsService { return false; } } - + /// 播放SSML Future speakSsmlOnce(String ssml) async { if (!_isInitialized) await initialize(); if (!isEnabled.value || ssml.isEmpty) return false; - + try { // 主动设置缓存状态为true _speakingCache = true; - + final result = await _channel.invokeMethod('speakSsml', {'ssml': ssml}); - + // 调用完成后主动检查状态 _checkSpeakingStatus(); - + return result == "OK"; } catch (e) { // 发生错误时重置状态 @@ -206,15 +203,16 @@ class AzureTtsService extends GetxService implements TtsService { return false; } } - + /// 等待语音播放完成 - Future waitForSpeechCompletion({Duration timeout = const Duration(seconds: 30)}) async { + Future waitForSpeechCompletion( + {Duration timeout = const Duration(seconds: 30)}) async { // 首先检查当前状态 _checkSpeakingStatus(); if (!_speakingCache) return; - + final stopwatch = Stopwatch()..start(); - + while (_speakingCache && stopwatch.elapsed < timeout) { // 短暂等待后再次检查 await Future.delayed(const Duration(milliseconds: 100)); @@ -227,31 +225,30 @@ class AzureTtsService extends GetxService implements TtsService { }); } } - + @override Future speakStream(String text) async { if (!isEnabled.value || text.isEmpty) return false; - + try { // 添加文本到缓冲区 _streamBuffer += text; - + // 如果缓冲区为空,直接返回 if (_streamBuffer.isEmpty) return true; - + // 使用正则表达式匹配句子,包括结束符号 // 匹配任意字符,直到遇到句子结束符号 final sentenceRegex = RegExp( - r'([^。.!!??;;:\n\r]+[。.!!??;;::\n\r]|[^。.!!??;;:\n\r]+(?:\.{3,}|…)|[^。.!!??;;:\n\r]+["」』])' - ); - + r'([^。.!!??;;:\n\r]+[。.!!??;;::\n\r]|[^。.!!??;;:\n\r]+(?:\.{3,}|…)|[^。.!!??;;:\n\r]+["」』])'); + bool hasProcessed = false; - + // 查找所有完整句子 final matches = sentenceRegex.allMatches(_streamBuffer); final List sentences = []; int lastMatchEnd = 0; - + for (final match in matches) { // 提取完整句子(包含结束符号) final sentence = match.group(1)?.trim(); @@ -260,7 +257,7 @@ class AzureTtsService extends GetxService implements TtsService { lastMatchEnd = match.end; } } - + // 处理找到的句子 for (final sentence in sentences) { _textQueue.add(_SpeechItem( @@ -271,32 +268,32 @@ class AzureTtsService extends GetxService implements TtsService { )); hasProcessed = true; } - + // 更新缓冲区,只保留未完成的部分 if (lastMatchEnd > 0) { _streamBuffer = _streamBuffer.substring(lastMatchEnd); } - + // 如果处理了文本并且队列未在处理中,开始处理队列 if (hasProcessed && !_isProcessingQueue) { _processQueue(); } - + return true; } catch (e) { Logger.error('处理流式文本失败: $e'); return false; } } - + @override Future flushStream() async { if (_textQueue.isEmpty && _streamBuffer.isEmpty) return true; - + try { // 处理缓冲区中可能的完整句子 await speakStream(''); - + // 如果缓冲区仍有剩余文本,将其添加到播放队列 if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) { _textQueue.add(_SpeechItem( @@ -305,25 +302,25 @@ class AzureTtsService extends GetxService implements TtsService { pitch: _currentPitch, volume: _currentVolume, )); - + // 清空缓冲区 _streamBuffer = ''; - + // 如果队列未在处理中,开始处理队列 if (!_isProcessingQueue) { _processQueue(); } } - + Logger.info('等待TTS队列播放完成,剩余${_textQueue.length}条'); - + // 等待队列处理完成 await _checkSpeakingStatus(); while (_isProcessingQueue || _speakingCache) { await Future.delayed(const Duration(milliseconds: 100)); await _checkSpeakingStatus(); } - + Logger.info('TTS队列播放完成'); return true; } catch (e) { @@ -331,33 +328,35 @@ class AzureTtsService extends GetxService implements TtsService { return false; } } - + /// 处理语音合成队列 Future _processQueue() async { if (_textQueue.isEmpty || _isProcessingQueue) return; - + _isProcessingQueue = true; - + try { while (_textQueue.isNotEmpty) { if (!isEnabled.value) { _textQueue.clear(); break; } - + final item = _textQueue.removeAt(0); - - if (item.rate != _currentRate || item.pitch != _currentPitch || item.volume != _currentVolume) { + + if (item.rate != _currentRate || + item.pitch != _currentPitch || + item.volume != _currentVolume) { await setSpeechParams( rate: item.rate, pitch: item.pitch, volume: item.volume, ); } - + // 使用speakOnce播放当前项 await speakOnce(item.text); - + // 等待播放完成 await waitForSpeechCompletion(); } @@ -367,26 +366,26 @@ class AzureTtsService extends GetxService implements TtsService { _isProcessingQueue = false; } } - + @override Future stop() async { try { _textQueue.clear(); - + if (_speakingCache) { final result = await _channel.invokeMethod('stopSpeaking'); // 重置缓存状态 _speakingCache = false; return result; } - + return true; } catch (e) { Logger.error('停止语音合成出错: $e'); return false; } } - + /// 生成SSML文本 String generateSsml({ required String text, @@ -399,11 +398,15 @@ class AzureTtsService extends GetxService implements TtsService { final rateValue = (rate ?? _currentRate).clamp(-100, 100); final pitchValue = (pitch ?? _currentPitch).clamp(-100, 100); final volumeValue = (volume ?? _currentVolume).clamp(0, 100); - - final rateStr = rateValue == 0 ? '0%' : rateValue < 0 ? '${(rateValue * 0.9).round()}%' : '$rateValue%'; + + final rateStr = rateValue == 0 + ? '0%' + : rateValue < 0 + ? '${(rateValue * 0.9).round()}%' + : '$rateValue%'; final pitchStr = pitchValue == 0 ? '0%' : '${(pitchValue * 0.5).round()}%'; final volumeStr = "$volumeValue%"; - + return ''' @@ -414,17 +417,17 @@ class AzureTtsService extends GetxService implements TtsService { '''; } - + /// 切换启用状态 void toggleEnabled() { isEnabled.toggle(); if (!isEnabled.value) stop(); } - + @override Future dispose() async { if (!_isInitialized) return; - + try { await stop(); await _channel.invokeMethod('dispose'); @@ -448,7 +451,6 @@ class AzureTtsService extends GetxService implements TtsService { return _speakingCache; } } - } /// 语音合成项目 @@ -457,11 +459,11 @@ class _SpeechItem { final int rate; final int pitch; final int volume; - + _SpeechItem({ required this.text, this.rate = 0, this.pitch = 0, this.volume = 100, }); -} \ No newline at end of file +} diff --git a/lib/data/services/voice_interaction_service.dart b/lib/data/services/voice_interaction_service.dart index 1c6e330dd..5a55f0219 100644 --- a/lib/data/services/voice_interaction_service.dart +++ b/lib/data/services/voice_interaction_service.dart @@ -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(); - + 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.broadcast(); - + final _eventStreamController = + StreamController.broadcast(); + // 事件流 - Stream get eventStream => _eventStreamController.stream; - + Stream 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; + _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 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; 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 _saveChatHistory(String agentId, String userMessage, String assistantMessage) async { + Future _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 initialize() async { if (_isInitialized) return true; - + try { Logger.info('正在初始化语音交互服务...'); - + // 获取聊天历史服务 try { _chatHistoryService = Get.find(); @@ -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 startService() async { try { Logger.info('启动语音交互服务...'); - + // 构建参数 final Map 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('startService', params) ?? false; - + + final result = + await _channel.invokeMethod('startService', params) ?? false; + if (result) { _isServiceRunning.value = true; Logger.info('语音交互服务已启动'); } else { Logger.error('启动语音交互服务失败'); } - + return result; } catch (e) { Logger.error('启动语音交互服务时发生错误: $e'); return false; } } - + /// 停止语音交互服务 Future stopService() async { try { Logger.info('停止语音交互服务...'); - + final result = await _channel.invokeMethod('stopService') ?? false; - + if (result) { _isServiceRunning.value = false; Logger.info('语音交互服务已停止'); } else { Logger.error('停止语音交互服务失败'); } - + return result; } catch (e) { Logger.error('停止语音交互服务时发生错误: $e'); return false; } } - + /// 检查服务是否运行 Future checkServiceStatus() async { try { - final bool result = await _channel.invokeMethod('isServiceRunning') ?? false; + final bool result = + await _channel.invokeMethod('isServiceRunning') ?? false; _isServiceRunning.value = result; return result; } catch (e) { @@ -295,27 +318,28 @@ class VoiceInteractionService extends GetxService { return false; } } - + /// 暂停语音交互 Future pauseVoiceInteraction() async { try { Logger.info('暂停后台语音交互...'); - - final result = await _channel.invokeMethod('pauseVoiceInteraction') ?? false; - + + final result = + await _channel.invokeMethod('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(); } -} \ No newline at end of file +} diff --git a/lib/modules/login/controllers/login_controller.dart b/lib/modules/login/controllers/login_controller.dart index 945889b00..e98b009ca 100644 --- a/lib/modules/login/controllers/login_controller.dart +++ b/lib/modules/login/controllers/login_controller.dart @@ -1,3 +1,4 @@ +import 'package:deep_voice/data/models/appconfig_model.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:logger/web.dart'; @@ -692,7 +693,8 @@ class LoginController extends GetxController { //final prefs = await GetStorage(); await _storage.write( 'user', jsonEncode(user.toJson())); // 使用 user.toJson() - + //获取app的配置信息 + await getAppConfig(); Get.offAllNamed(Routes.pairing); // } else { // throw FormatException('API 响应缺少 user 字段'); @@ -722,4 +724,24 @@ class LoginController extends GetxController { colorText: Colors.blue, ); } + + Future getAppConfig() async { + try { + print('getAppConfig:进来了'); + //if (User.getname() != "") { + final response = await Api.getappconfig(); + print('getAppConfig: $response'); + if (response == null) return false; + print('getAppConfig:进来了'); + // 根据API设计调整 + final resp = UserGetAppConfigResp.fromJson(response); + // 存储用户信息(示例) + _storage.write('ENV', resp.env); + _storage.write('MCPS', resp.mcps); + return true; + } catch (e) { + print('getAppConfig 取失败: $e'); + return false; + } + } } diff --git a/lib/modules/splash/controllers/splash_controller.dart b/lib/modules/splash/controllers/splash_controller.dart index 70f2cfc81..cec9fbff7 100644 --- a/lib/modules/splash/controllers/splash_controller.dart +++ b/lib/modules/splash/controllers/splash_controller.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:amap_flutter/amap_flutter.dart'; +import 'package:deep_voice/data/models/appconfig_model.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; @@ -88,6 +89,7 @@ class SplashController extends GetxController { isLoggedIn = await getLoginToken(); } if (isLoggedIn) { + await getAppConfig(); Get.offAllNamed(Routes.pairing); } else { Get.offAllNamed(Routes.login); // 跳转到登录页 @@ -118,4 +120,25 @@ class SplashController extends GetxController { return false; } } + + Future getAppConfig() async { + try { + print('getAppConfig:进来了'); + //if (User.getname() != "") { + final response = await Api.getappconfig(); + print('getAppConfig: $response'); + if (response == null) return false; + // 根据API设计调整 + final resp = UserGetAppConfigResp.fromJson(response); + print('getAppConfig:进来了'); + // 存储用户信息(示例) + _storage.write('ENV', resp.env); + _storage.write('MCPS', resp.mcps); + + return true; + } catch (e) { + print('getAppConfig 取失败: $e'); + return false; + } + } } diff --git a/local_plugins/open_ai_service/android/build.gradle.kts b/local_plugins/open_ai_service/android/build.gradle.kts index 58c28775b..ab7c03ecf 100644 --- a/local_plugins/open_ai_service/android/build.gradle.kts +++ b/local_plugins/open_ai_service/android/build.gradle.kts @@ -4,6 +4,7 @@ plugins { // Kotlin Android 插件 id("org.jetbrains.kotlin.android") id("com.github.johnrengelman.shadow") version "8.1.1" + kotlin("plugin.serialization") version "1.9.24" } android { @@ -58,4 +59,5 @@ dependencies { implementation("io.ktor:ktor-client-okhttp:3.1.2") implementation("io.ktor:ktor-client-logging:3.1.2") implementation("com.google.code.gson:gson:2.10.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") } \ No newline at end of file diff --git a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt index 45b5a8810..681489d2f 100644 --- a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt +++ b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt @@ -34,7 +34,17 @@ import io.ktor.client.request.* import io.ktor.http.* import com.yunqiinnovation.open_ai_service.mcp.CustomSseClientTransport import io.ktor.util.valuesOf +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +@Serializable +data class ServerConfig( + val servername: String, + val url: String, + val tools: List? = null +) /** * 工具类型枚举 */ @@ -484,15 +494,17 @@ class MCPClient(private val context: Context? = null) : AutoCloseable { if (mcpConfigJson.isNotEmpty()) { val mapper = ObjectMapper() try { - val config = mapper.readValue(mcpConfigJson, Map::class.java) + Log.d(TAG, "传入JSON:${mcpConfigJson}") + val configMap: Map = Json.decodeFromString(mcpConfigJson) +// val config = mapper.readValue(mcpConfigJson, Map::class.java) // 处理mcpServers格式 @Suppress("UNCHECKED_CAST") - val mcpServers = config["mcpServers"] as? Map> - if (mcpServers != null && mcpServers.isNotEmpty()) { +// val mcpServers = config["mcpServers"] as? Map> + if (configMap.isNotEmpty()) { // 从服务器配置中提取URL - mcpServers.forEach { (serverId, serverConfig) -> - val url = serverConfig["url"] + configMap.forEach { (serverId, serverConfig) -> + val url = serverConfig.url if (!url.isNullOrEmpty()) { serverMap[serverId] = url Log.d(TAG, "从配置中添加服务器 [$serverId]: $url") diff --git a/pubspec.yaml b/pubspec.yaml index 47da8b194..75af864a0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: sdk: flutter cupertino_icons: ^1.0.6 get: ^4.6.6 + json_annotation: ^4.9.0 get_storage: ^2.1.1 http: ^1.3.0 flutter_screenutil: ^5.9.0 @@ -80,6 +81,8 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 + build_runner: ^2.3.3 # 用于生成代码 flutter pub run build_runner build + json_serializable: ^6.6.0 # 用于自动生成代码 flutter: uses-material-design: true