From 676cabd4f08a41bf5247c9514ba80f9ef58ac5f8 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Tue, 22 Apr 2025 17:44:45 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=99=A8=E7=9A=84=E9=85=8D=E7=BD=AE=E4=BF=A1?= =?UTF-8?q?=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/models/appconfig_model.dart | 30 +++ lib/data/models/appconfig_model.g.dart | 37 ++++ lib/data/services/network/api.dart | 9 + .../services/open_ai_service_adapter.dart | 45 ++-- .../speech_impl/azure_asr_service.dart | 132 ++++++------ .../speech_impl/azure_tts_service.dart | 202 +++++++++--------- .../services/voice_interaction_service.dart | 184 +++++++++------- .../login/controllers/login_controller.dart | 24 ++- .../splash/controllers/splash_controller.dart | 23 ++ .../open_ai_service/android/build.gradle.kts | 2 + .../open_ai_service/MCPClient.kt | 22 +- pubspec.yaml | 3 + 12 files changed, 446 insertions(+), 267 deletions(-) create mode 100644 lib/data/models/appconfig_model.dart create mode 100644 lib/data/models/appconfig_model.g.dart 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 From eee4fd3bb92849fa116574ced49033be32e308e8 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Tue, 22 Apr 2025 19:26:07 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E8=A1=A5=E5=85=85=20mcp=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E8=BF=87=E6=BB=A4=E9=80=BB=E8=BE=91=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../open_ai_service/MCPClient.kt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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 681489d2f..de6725b5f 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 @@ -71,13 +71,13 @@ interface FunctionHandler { */ class MCPSubClient( private val serverId: String, - serverUrl: String, + private val config: ServerConfig, private val customClient: HttpClient? = null ) : AutoCloseable { private val TAG = "MCPSubClient[$serverId]" // 保存服务器URL - private val serverUrl: String = serverUrl + private val serverUrl: String = config.url init { Log.d(TAG, "MCPSubClient初始化,URL: $serverUrl") @@ -90,7 +90,6 @@ class MCPSubClient( version = "1.0.0" ) ) - // 服务器提供的工具列表 private var tools: List = emptyList() @@ -166,7 +165,8 @@ class MCPSubClient( * 处理工具列表 */ private fun processToolsList(toolsResult: ListToolsResult?) { - tools = toolsResult?.tools?.map { tool -> + tools = toolsResult?.tools?.filter { tool -> config.tools == null || config.tools.contains(tool.name) == true + }?.map { tool -> // 确保输入模式有效 val inputType = tool.inputSchema?.type ?: "object" val inputProperties = tool.inputSchema?.properties ?: emptyMap() @@ -486,7 +486,7 @@ class MCPClient(private val context: Context? = null) : AutoCloseable { // 清除现有连接 closeAllConnections() - val serverMap = mutableMapOf() + val serverMap = mutableMapOf() var configLoaded = false // 尝试解析传入的JSON配置 @@ -506,7 +506,7 @@ class MCPClient(private val context: Context? = null) : AutoCloseable { configMap.forEach { (serverId, serverConfig) -> val url = serverConfig.url if (!url.isNullOrEmpty()) { - serverMap[serverId] = url + serverMap[serverId] = serverConfig Log.d(TAG, "从配置中添加服务器 [$serverId]: $url") } } @@ -525,10 +525,10 @@ class MCPClient(private val context: Context? = null) : AutoCloseable { // 创建并连接所有子客户端 val connectionResults = mutableMapOf() - for ((serverId, serverUrl) in serverMap) { + for ((serverId, serverConfig) in serverMap) { try { - Log.d(TAG, "为服务器 [$serverId],[$serverUrl] 创建子客户端") - val subClient = MCPSubClient(serverId, serverUrl, sharedHttpClient) + Log.d(TAG, "为服务器 [$serverId],[${serverConfig.url}] 创建子客户端") + val subClient = MCPSubClient(serverId, serverConfig, sharedHttpClient) val connected = subClient.connect() if (connected) { From e85dcdb71adf9f5200fe687aace5cf92c24c315c Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Wed, 23 Apr 2025 10:18:51 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E5=8F=98=E9=87=8F=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/services/spotify_service.dart | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/data/services/spotify_service.dart b/lib/data/services/spotify_service.dart index 983868394..05d439e18 100644 --- a/lib/data/services/spotify_service.dart +++ b/lib/data/services/spotify_service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:convert'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:get_storage/get_storage.dart'; import 'package:spotify_sdk/spotify_sdk.dart'; import 'package:get/get.dart'; import 'ai_service.dart'; @@ -11,6 +12,10 @@ import 'package:path_provider/path_provider.dart'; /// OpenAI服务适配器 - 连接AiService接口与OpenAIService插件 class SpotifService { bool isInitialized = false; + final GetStorage _storage = GetStorage(); + + late String SPOTIFY_CLIENT_ID; + late String SPOTIFY_REDIRECT_URL; /// 初始化OpenAI服务 Future initialize() async { @@ -18,6 +23,9 @@ class SpotifService { await getAccessToken(); await connectToSpotifyRemote(); isInitialized = true; + final _env = _storage.read("ENV") as Map; + SPOTIFY_CLIENT_ID = _env['SPOTIFY_CLIENT_ID'] ?? ''; + SPOTIFY_REDIRECT_URL = _env['SPOTIFY_REDIRECT_URL'] ?? ''; printInfo(info: "SpotifService----------- initialize"); } @@ -28,8 +36,8 @@ class SpotifService { Future connectToSpotifyRemote() async { try { var result = await SpotifySdk.connectToSpotifyRemote( - clientId: dotenv.env['SPOTIFY_CLIENT_ID'].toString(), - redirectUrl: dotenv.env['SPOTIFY_REDIRECT_URL'].toString(), + clientId: SPOTIFY_CLIENT_ID, + redirectUrl: SPOTIFY_REDIRECT_URL, scope: 'user-read-private user-read-email streaming app-remote-control user-modify-playback-state', ); @@ -46,8 +54,8 @@ class SpotifService { Future getAccessToken() async { try { var authenticationToken = await SpotifySdk.getAccessToken( - clientId: dotenv.env['SPOTIFY_CLIENT_ID'].toString(), - redirectUrl: dotenv.env['SPOTIFY_REDIRECT_URL'].toString(), + clientId: SPOTIFY_CLIENT_ID, + redirectUrl: SPOTIFY_REDIRECT_URL, scope: 'app-remote-control, ' 'user-modify-playback-state, ' 'playlist-read-private, ' From 9633e85cf0c637a84cbc60dc020b5eeff9454afd Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Wed, 23 Apr 2025 14:14:40 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yunqiinnovation/deepsound/MainActivity.kt | 8 +- .../deepsound/VoiceInteractionHandler.kt | 4 +- .../deepsound/VoiceInteractionService.kt | 6 +- .../services/volcano_translation_service.dart | 133 ++++---- .../spotify_sdk/SpotifySdkService.kt | 311 ++++++++++++++++++ 5 files changed, 401 insertions(+), 61 deletions(-) create mode 100644 local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifySdkService.kt diff --git a/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt b/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt index ffc3d256a..bded96c8c 100644 --- a/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt +++ b/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt @@ -173,7 +173,9 @@ class MainActivity: FlutterActivity() { var volcanoSpeechAppId: String = "" var volcanoSpeechAppToken: String = "" var mcpServer: String = "" - + var spotifyClientId: String = "" + var spotifyRedirectUrl: String = "" + // 安全存储相关常量 private const val SECURE_PREFS_FILENAME = "deep_voice_secure_prefs" private const val KEY_AZURE_SPEECH_KEY = "azure_speech_key" @@ -186,6 +188,8 @@ class MainActivity: FlutterActivity() { private const val KEY_VOLCANO_SPEECH_APP_ID = "volcano_speech_app_id" private const val KEY_VOLCANO_SPEECH_APP_TOKEN = "volcano_speech_app_token" private const val KEY_MCP_SERVER = "mcp_server" + private const val KEY_SPOTIFY_CLIENT_ID = "spotify_client_id" + private const val KEY_SPOTIFY_REDIRECT_URL = "spotify_redirect_url" // 会话管理 private const val KEY_SESSION_ID = "session_id" @@ -227,6 +231,8 @@ class MainActivity: FlutterActivity() { .putString(KEY_VOLCANO_SPEECH_APP_ID, volcanoSpeechAppId) .putString(KEY_VOLCANO_SPEECH_APP_TOKEN, volcanoSpeechAppToken) .putString(KEY_MCP_SERVER, mcpServer) + .putString(KEY_SPOTIFY_CLIENT_ID, spotifyClientId) + .putString(KEY_SPOTIFY_REDIRECT_URL, spotifyRedirectUrl) .apply() FileLogger.d("MainActivity", "密钥已安全保存到加密存储中") diff --git a/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionHandler.kt b/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionHandler.kt index 4fd924a36..881b9dc84 100644 --- a/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionHandler.kt +++ b/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionHandler.kt @@ -26,7 +26,9 @@ class VoiceInteractionHandler( private val openaiModel: String = "", private val volcanoSpeechAppId: String, private val volcanoSpeechAppToken: String, - private val mcpServer: String + private val mcpServer: String, + private val spotifyclientid: String, + private val spotifyRedirectUrl: String ) { private val TAG = "VoiceInteractionHandler" diff --git a/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionService.kt b/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionService.kt index 0497458d2..2822ed511 100644 --- a/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionService.kt +++ b/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionService.kt @@ -148,6 +148,8 @@ class VoiceInteractionService : Service() { var volcanoSpeechAppId = MainActivity.volcanoSpeechAppId ?: "" var volcanoSpeechAppToken = MainActivity.volcanoSpeechAppToken ?: "" var mcpServer = MainActivity.mcpServer ?: "" + var spotifyclientid = MainActivity.spotifyClientId ?: "" + var spotifyRedirectUrl = MainActivity.spotifyRedirectUrl ?: "" // 如果静态变量中没有配置,尝试从加密存储中加载 if (subscriptionKey.isEmpty() || serviceRegion.isEmpty() || openaiKey.isEmpty()) { @@ -166,6 +168,8 @@ class VoiceInteractionService : Service() { volcanoSpeechAppId = MainActivity.volcanoSpeechAppId ?: "" volcanoSpeechAppToken = MainActivity.volcanoSpeechAppToken ?: "" mcpServer = MainActivity.mcpServer ?: "" + spotifyclientid = MainActivity.spotifyClientId ?: "" + spotifyRedirectUrl = MainActivity.spotifyRedirectUrl ?: "" FileLogger.d(TAG, "已从加密存储加载配置信息") } else { FileLogger.e(TAG, "无法从加密存储加载配置信息") @@ -177,7 +181,7 @@ class VoiceInteractionService : Service() { subscriptionKey, serviceRegion, openaiKey, openaiBaseUrl, openaiModel, volcanoSpeechAppId, volcanoSpeechAppToken, - mcpServer) + mcpServer,spotifyclientid,spotifyRedirectUrl) // 初始化回调 voiceInteractionHandler.setCallback(object : VoiceInteractionHandler.InteractionCallback { diff --git a/lib/data/services/volcano_translation_service.dart b/lib/data/services/volcano_translation_service.dart index 801619d38..11e19bb1c 100644 --- a/lib/data/services/volcano_translation_service.dart +++ b/lib/data/services/volcano_translation_service.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:math'; +import 'package:get_storage/get_storage.dart'; import 'package:http/http.dart' as http; import 'package:crypto/crypto.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; @@ -10,7 +11,7 @@ import 'language_manager.dart'; class VolcanoTranslationService extends GetxService { // API 配置 late String _baseUrl; - final String _endpoint = '/'; // 修改为根路径 + final String _endpoint = '/'; // 修改为根路径 late final String _accessKey; late final String _secretKey; late final String _region; @@ -21,7 +22,7 @@ class VolcanoTranslationService extends GetxService { // 语言管理器 final LanguageManager _languageManager = Get.find(); - + final GetStorage _storage = GetStorage(); // 构造函数 VolcanoTranslationService(); @@ -32,9 +33,10 @@ class VolcanoTranslationService extends GetxService { String? region, }) async { try { - _accessKey = accessKey ?? dotenv.env['VOLCANO_TRANSLATION_ACCESS_KEY'] ?? ''; - _secretKey = secretKey ?? dotenv.env['VOLCANO_TRANSLATION_SECRET_KEY'] ?? ''; - _region = region ?? dotenv.env['VOLCANO_TRANSLATION_REGION'] ?? 'cn-north-1'; + final _env = _storage.read("ENV") as Map; + _accessKey = accessKey ?? _env['VOLCANO_TRANSLATION_ACCESS_KEY'] ?? ''; + _secretKey = secretKey ?? _env['VOLCANO_TRANSLATION_SECRET_KEY'] ?? ''; + _region = region ?? _env['VOLCANO_TRANSLATION_REGION'] ?? 'cn-north-1'; _service = 'translate'; _baseUrl = 'https://translate.volcengineapi.com'; @@ -50,14 +52,21 @@ class VolcanoTranslationService extends GetxService { } // 生成签名 - Map _generateSignature(String method, Map requestBody, Map queryParams) { + Map _generateSignature(String method, + Map requestBody, Map queryParams) { // 1. 准备时间相关参数 final now = DateTime.now().toUtc(); final date = now.toIso8601String().split('T')[0].replaceAll('-', ''); - final timestamp = now.toIso8601String().replaceAll(':', '').replaceAll('-', '').split('.')[0] + 'Z'; + final timestamp = now + .toIso8601String() + .replaceAll(':', '') + .replaceAll('-', '') + .split('.')[0] + + 'Z'; // 2. 构建规范查询字符串 - 按照参数名ASCII排序 - final sortedParams = Map.fromEntries(queryParams.entries.toList()..sort((a, b) => a.key.compareTo(b.key))); + final sortedParams = Map.fromEntries( + queryParams.entries.toList()..sort((a, b) => a.key.compareTo(b.key))); final List queryParts = []; sortedParams.forEach((key, value) { // 使用RFC 3986编码,确保特殊字符被正确处理 @@ -66,48 +75,53 @@ class VolcanoTranslationService extends GetxService { queryParts.add('$encodedKey=$encodedValue'); }); final canonicalQueryString = queryParts.join('&'); - + // 3. 创建规范请求 final contentType = 'application/json'; - final payloadHash = sha256.convert(utf8.encode(json.encode(requestBody))).toString(); - + final payloadHash = + sha256.convert(utf8.encode(json.encode(requestBody))).toString(); + final host = 'translate.volcengineapi.com'; - + // 确保头部按照字典序排序,且键名小写 // 注意:只包含host和x-date头部,与示例保持一致 final canonicalHeaders = 'host:$host\nx-date:$timestamp\n'; final signedHeaders = 'host;x-date'; - + // 构建规范请求,格式必须与示例完全一致 - final canonicalRequest = '$method\n$_endpoint\n$canonicalQueryString\n$canonicalHeaders\n$signedHeaders\n$payloadHash'; - + final canonicalRequest = + '$method\n$_endpoint\n$canonicalQueryString\n$canonicalHeaders\n$signedHeaders\n$payloadHash'; + // 4. 创建待签字符串 final algorithm = 'HMAC-SHA256'; final credentialScope = '$date/$_region/$_service/request'; - final stringToSign = '$algorithm\n$timestamp\n$credentialScope\n${sha256.convert(utf8.encode(canonicalRequest)).toString()}'; - + final stringToSign = + '$algorithm\n$timestamp\n$credentialScope\n${sha256.convert(utf8.encode(canonicalRequest)).toString()}'; + // 5. 计算签名 - 按照文档正确派生密钥 // 注意:这里是关键修复点,确保密钥派生过程完全符合文档 - final kSecret = utf8.encode(_secretKey); // 直接使用原始密钥,不添加前缀 - + final kSecret = utf8.encode(_secretKey); // 直接使用原始密钥,不添加前缀 + // kDate = HMAC(kSecret, Date) var kDate = Hmac(sha256, kSecret).convert(utf8.encode(date)).bytes; - + // kRegion = HMAC(kDate, Region) var kRegion = Hmac(sha256, kDate).convert(utf8.encode(_region)).bytes; - + // kService = HMAC(kRegion, Service) var kService = Hmac(sha256, kRegion).convert(utf8.encode(_service)).bytes; - + // kSigning = HMAC(kService, "request") var kSigning = Hmac(sha256, kService).convert(utf8.encode('request')).bytes; - + // 使用派生的签名密钥计算最终签名 - final signature = Hmac(sha256, kSigning).convert(utf8.encode(stringToSign)).toString(); - + final signature = + Hmac(sha256, kSigning).convert(utf8.encode(stringToSign)).toString(); + // 6. 添加签名到请求头 - 格式必须与示例完全一致 - final authorization = '$algorithm Credential=$_accessKey/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature'; - + final authorization = + '$algorithm Credential=$_accessKey/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature'; + // 只返回必要的头部,与示例保持一致 return { 'Content-Type': contentType, @@ -131,27 +145,29 @@ class VolcanoTranslationService extends GetxService { // 获取源语言和目标语言的短代码 String sourceCode = ''; String targetCode = 'zh'; - + // 从 ASR 代码获取短代码 if (sourceLanguageCode.isNotEmpty) { - final sourceShortCode = _languageManager.getShortCodeByAsrCode(sourceLanguageCode); + final sourceShortCode = + _languageManager.getShortCodeByAsrCode(sourceLanguageCode); if (sourceShortCode != null) { sourceCode = sourceShortCode; } } - - final targetShortCode = _languageManager.getShortCodeByAsrCode(targetLanguageCode); + + final targetShortCode = + _languageManager.getShortCodeByAsrCode(targetLanguageCode); if (targetShortCode != null) { targetCode = targetShortCode; } - + // 构建请求体 final requestBody = { 'SourceLanguage': sourceCode, 'TargetLanguage': targetCode, 'TextList': [text], }; - + // 添加必要的查询参数 - 确保包含所有必需参数 final queryParams = { 'Action': 'TranslateText', @@ -159,60 +175,61 @@ class VolcanoTranslationService extends GetxService { 'Region': _region, 'Service': _service, }; - + // 生成包含查询参数的签名 final headers = _generateSignature('POST', requestBody, queryParams); - + // 构建带有查询参数的URL final queryString = Uri(queryParameters: queryParams).query; final url = '$_baseUrl/?$queryString'; - + final response = await http.post( Uri.parse(url), headers: headers, body: json.encode(requestBody), ); - + if (response.statusCode == 200) { final responseData = json.decode(response.body); - + // 尝试从不同的响应结构中提取翻译结果 String? translation; - + // 1. 标准响应结构 - if (responseData['TranslationList'] != null && - responseData['TranslationList'] is List && + if (responseData['TranslationList'] != null && + responseData['TranslationList'] is List && responseData['TranslationList'].isNotEmpty) { final translationItem = responseData['TranslationList'][0]; - if (translationItem is Map && translationItem.containsKey('Translation')) { + if (translationItem is Map && + translationItem.containsKey('Translation')) { translation = translationItem['Translation']; } } - + // 2. 检查是否有错误响应 - if (translation == null && - responseData['ResponseMetadata'] != null && - responseData['ResponseMetadata'] is Map && + if (translation == null && + responseData['ResponseMetadata'] != null && + responseData['ResponseMetadata'] is Map && responseData['ResponseMetadata']['Error'] != null) { return null; } - + // 3. 尝试其他可能的响应结构 if (translation == null) { if (responseData.containsKey('Translation')) { translation = responseData['Translation']; - } else if (responseData.containsKey('Result') && - responseData['Result'] is Map && - responseData['Result'].containsKey('Translation')) { + } else if (responseData.containsKey('Result') && + responseData['Result'] is Map && + responseData['Result'].containsKey('Translation')) { translation = responseData['Result']['Translation']; - } else if (responseData.containsKey('Data') && - responseData['Data'] is Map) { + } else if (responseData.containsKey('Data') && + responseData['Data'] is Map) { final data = responseData['Data']; if (data.containsKey('Translation')) { translation = data['Translation']; - } else if (data.containsKey('TranslationList') && - data['TranslationList'] is List && - data['TranslationList'].isNotEmpty) { + } else if (data.containsKey('TranslationList') && + data['TranslationList'] is List && + data['TranslationList'].isNotEmpty) { final item = data['TranslationList'][0]; if (item is Map && item.containsKey('Translation')) { translation = item['Translation']; @@ -220,14 +237,14 @@ class VolcanoTranslationService extends GetxService { } } } - + // 返回提取到的翻译结果 return translation; } - + return null; } catch (e) { return null; } } -} \ No newline at end of file +} diff --git a/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifySdkService.kt b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifySdkService.kt new file mode 100644 index 000000000..7449a4b03 --- /dev/null +++ b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifySdkService.kt @@ -0,0 +1,311 @@ +package de.minimalme.spotify_sdk + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.util.Log +import com.spotify.android.appremote.api.ConnectionParams +import com.spotify.android.appremote.api.Connector +import com.spotify.android.appremote.api.PlayerApi +import com.spotify.android.appremote.api.SpotifyAppRemote +import com.spotify.android.appremote.api.error.AuthenticationFailedException +import com.spotify.android.appremote.api.error.CouldNotFindSpotifyApp +import com.spotify.android.appremote.api.error.NotLoggedInException +import com.spotify.android.appremote.api.error.OfflineModeException +import com.spotify.android.appremote.api.error.SpotifyConnectionTerminatedException +import com.spotify.android.appremote.api.error.SpotifyDisconnectedException +import com.spotify.android.appremote.api.error.SpotifyRemoteServiceException +import com.spotify.android.appremote.api.error.UnsupportedFeatureVersionException +import com.spotify.android.appremote.api.error.UserNotAuthorizedException +import com.spotify.sdk.android.auth.AuthorizationClient +import com.spotify.sdk.android.auth.AuthorizationRequest +import com.spotify.sdk.android.auth.AuthorizationResponse +import de.minimalme.spotify_sdk.subscriptions.CapabilitiesChannel +import de.minimalme.spotify_sdk.subscriptions.ConnectionStatusChannel +import de.minimalme.spotify_sdk.subscriptions.PlayerContextChannel +import de.minimalme.spotify_sdk.subscriptions.PlayerStateChannel +import de.minimalme.spotify_sdk.subscriptions.UserStatusChannel +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.PluginRegistry +import kotlinx.event.SetEvent +import kotlinx.event.event +class SpotifySdkService(private val context: Context? = null) { + + // application context + private var applicationContext : Context? = null + private var applicationActivity : Activity? = null + // method channel + private lateinit var methodChannel : MethodChannel + private val channelName = "spotify_sdk" + private val loggingTag = "spotify_sdk" + + // event channels + private var playerContextChannel : EventChannel? = null + private var playerStateChannel : EventChannel? = null + private var capabilitiesChannel : EventChannel? = null + private var userStatusChannel : EventChannel? = null + private var connectionStatusChannel : EventChannel? = null + + private val playerContextSubscription = "player_context_subscription" + private val playerStateSubscription = "player_state_subscription" + private val capabilitiesSubscription = "capabilities_subscription" + private val userStatusSubscription = "user_status_subscription" + private val connectionStatusSubscription = "connection_status_subscription" + + //connecting + private val methodConnectToSpotify = "connectToSpotify" + private val methodGetAccessToken = "getAccessToken" + private val methodDisconnectFromSpotify = "disconnectFromSpotify" + + // connectApi + private val methodSwitchToLocalDevice = "switchToLocalDevice" + + //playerApi + private val methodGetCrossfadeState = "getCrossfadeState" + private val methodGetPlayerState = "getPlayerState" + private val methodPlay = "play" + private val methodPause = "pause" + private val methodQueueTrack = "queueTrack" + private val methodResume = "resume" + private val methodSeekToRelativePosition = "seekToRelativePosition" + private val methodSetPodcastPlaybackSpeed = "setPodcastPlaybackSpeed" + private val methodSkipNext = "skipNext" + private val methodSkipPrevious = "skipPrevious" + private val methodSkipToIndex = "skipToIndex" + private val methodSeekTo = "seekTo" + private val methodToggleRepeat = "toggleRepeat" + private val methodToggleShuffle = "toggleShuffle" + private val methodSetShuffle = "setShuffle" + private val methodSetRepeatMode = "setRepeatMode" + + //userApi + private val methodAddToLibrary = "addToLibrary" + private val methodRemoveFromLibrary = "removeFromLibrary" + private val methodGetCapabilities = "getCapabilities" + private val methodGetLibraryState = "getLibraryState" + + //imagesApi + private val methodGetImage = "getImage" + + private val paramClientId = "clientId" + private val paramRedirectUrl = "redirectUrl" + private val paramScope = "scope" + private val paramSpotifyUri = "spotifyUri" + private val paramImageUri = "imageUri" + private val paramImageDimension = "imageDimension" + private val paramPositionedMilliseconds = "positionedMilliseconds" + private val paramRelativeMilliseconds = "relativeMilliseconds" + private val paramPodcastPlaybackSpeed = "podcastPlaybackSpeed" + private val paramTrackIndex = "trackIndex" + private val paramRepeatMode = "repeatMode" + private val paramShuffle = "shuffle" + + private val errorConnecting = "errorConnecting" + private val errorDisconnecting = "errorDisconnecting" + private val errorConnection = "errorConnection" + private val errorAuthenticationToken = "authenticationTokenError" + + private val requestCodeAuthentication = 1337 + + private var spotifyAppRemote: SpotifyAppRemote? = null + private var playerApi: PlayerApi? = null + + //-- Method implementations + private fun connectToSpotify(clientId: String?, redirectUrl: String?, result: MethodChannel.Result) { + + if (clientId.isNullOrBlank() || redirectUrl.isNullOrBlank()) { + result.error(errorConnecting, "client id or redirectUrl are not set or have invalid format", "") + } else { + val connectionParams = ConnectionParams.Builder(clientId) + .setRedirectUri(redirectUrl) + .showAuthView(true) + .build() + SpotifyAppRemote.disconnect(spotifyAppRemote) + var initiallyConnected = false + SpotifyAppRemote.connect(applicationContext, connectionParams, + object : Connector.ConnectionListener { + override fun onConnected(spotifyAppRemoteValue: SpotifyAppRemote) { + spotifyAppRemote = spotifyAppRemoteValue + playerApi = spotifyAppRemote?.playerApi + + initiallyConnected = true + Log.i(loggingTag, "App Remote successfully connected") + result.success(true) + } + + override fun onFailure(throwable: Throwable) { + val errorDetails = throwable.toString() + // determine the error + val errorMessage: String + val errorCode: String + var connected = false + when (throwable) { + is SpotifyDisconnectedException, is SpotifyConnectionTerminatedException -> { + // The Spotify app was/is disconnected by the Spotify app. + // This indicates typically that the Spotify app was closed by the user or for other reasons. + // You need to reconnect to continue using Spotify App Remote. + errorMessage = + "The Spotify app was/is disconnected by the Spotify app.Reconnect necessary" + errorCode = "SpotifyDisconnectedException" + } + + is CouldNotFindSpotifyApp -> { + errorMessage = "The Spotify app is not installed on the device" + errorCode = "CouldNotFindSpotifyApp" + } + + is AuthenticationFailedException -> { + errorMessage = + "Partner app failed to authenticate with Spotify. Check client credentials and make sure your app is registered correctly at developer.spotify.com" + errorCode = "AuthenticationFailedException" + } + + is UserNotAuthorizedException -> { + errorMessage = + "Indicates the user did not authorize this client of App Remote to use Spotify on the users behalf." + errorCode = "UserNotAuthorizedException" + } + + is UnsupportedFeatureVersionException -> { + errorMessage = + "Spotify app can't support requested features. User should update Spotify app." + errorCode = "UnsupportedFeatureVersionException" + connected = true + } + + is OfflineModeException -> { + errorMessage = + "Spotify user has set their Spotify app to be in offline mode" + errorCode = "OfflineModeException" + connected = true + } + + is NotLoggedInException -> { + errorMessage = "User has logged out from Spotify." + errorCode = "NotLoggedInException" + } + + is SpotifyRemoteServiceException -> { + errorMessage = + "Encapsulates possible SecurityException and IllegalStateException errors." + errorCode = "SpotifyRemoteServiceException" + } + + else -> { + errorMessage = "Something went wrong connecting spotify remote" + errorCode = errorConnection + } + } + Log.e(loggingTag, errorMessage) + // notify plugin + if (initiallyConnected) { + // emit connection error event +// connStatusEventChannel( +// ConnectionStatusChannel.ConnectionEvent( +// connected, +// errorMessage, +// errorCode, +// errorDetails +// ) +// ) + } else { + // throw exception as the connect method + result.error(errorCode, errorMessage, errorDetails) + } + } + }) + } + } + + private fun getAccessToken(clientId: String?, redirectUrl: String?, scope: String?, result: MethodChannel.Result) { + if (applicationActivity == null) { + throw IllegalStateException("getAccessToken needs a foreground activity") + } + + if (clientId.isNullOrBlank() || redirectUrl.isNullOrBlank()) { + result.error(errorConnecting, "client id or redirectUrl are not set or have invalid format", "") + } else { + //Convert String? scope to Array. Delimiter set as comma "," + val scopeArray = scope?.split(",")?.toTypedArray() +// methodConnectToSpotify.checkAndSetPendingOperation(result) + + val builder = AuthorizationRequest.Builder( + clientId, + AuthorizationResponse.Type.TOKEN, + redirectUrl + ) + builder.setScopes(scopeArray) + val request = builder.build() + + AuthorizationClient.openLoginActivity( + applicationActivity, + requestCodeAuthentication, + request + ) + } + } + + private fun disconnectFromSpotify(result: MethodChannel.Result) { + if (spotifyAppRemote != null && spotifyAppRemote!!.isConnected) { + SpotifyAppRemote.disconnect(spotifyAppRemote) + + // emit connection terminated event +// connStatusEventChannel( +// ConnectionStatusChannel.ConnectionEvent( +// false, +// "Successfully disconnected from Spotify.", +// null, +// null +// ) +// ) + // method success + result.success(true) + } else if (!spotifyAppRemote!!.isConnected) { + result.error(errorDisconnecting, "could not disconnect spotify remote", "you are not connected, no need to disconnect") + } else { + result.error(errorDisconnecting, "could not disconnect spotify remote", "spotifyAppRemote is not set") + } + } + + private fun play(spotifyUri: String?){ +// if (playerApi != null && !spotifyUri.isNullOrBlank()) { +// playerApi.play(spotifyUri) +// .setResultCallback { result.success(true) } +// .setErrorCallback { throwable -> result.error(errorPlay, "error when playing uri: $spotifyUri", throwable.toString()) } +// } else if (spotifyUri.isNullOrBlank()) { +// result.error(errorPlay, "spotifyUri has invalid format or is not set", "") +// } else { +// spotifyRemoteAppNotSetError() +// } + if (playerApi != null && !spotifyUri.isNullOrBlank()) { + playerApi!!.play(spotifyUri) + } else if (spotifyUri.isNullOrBlank()) { + + } else { + + } + } + private fun pause(){ +// if (playerApi != null) { +// playerApi.pause() +// .setResultCallback { result.success(true) } +// .setErrorCallback { throwable -> result.error(errorPause, "error when pausing", throwable.toString()) } +// } else { +// spotifyRemoteAppNotSetError() +// } + if (playerApi != null) { + playerApi!!.pause() + .setResultCallback {} + .setErrorCallback { } + } else { + + } + } + + +} \ No newline at end of file