From e227e7a9304b74860daf178b8a7e7e0d4785aa42 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Wed, 16 Apr 2025 18:25:58 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E9=9F=B3=E4=B9=90=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .mcp.json | 8 +- .../services/open_ai_service_adapter.dart | 17 +- lib/data/services/spotify_service.dart | 176 ++++++++++++++++++ .../chat/controllers/chat_controller.dart | 26 ++- lib/modules/chat/views/chat_music_card.dart | 37 +--- lib/modules/chat/views/chat_view.dart | 6 +- .../open_ai_service/MCPClient.kt | 4 +- .../open_ai_service/OpenAIServicePlugin.kt | 38 ++-- .../open_ai_service/lib/open_ai_service.dart | 7 +- 9 files changed, 243 insertions(+), 76 deletions(-) create mode 100644 lib/data/services/spotify_service.dart diff --git a/.mcp.json b/.mcp.json index a344e9c9a..dddcc8d13 100644 --- a/.mcp.json +++ b/.mcp.json @@ -1,11 +1,7 @@ { "mcpServers": { - - "amap-amap-sse": { - "url": "https://mcp.amap.com/sse?key=e5fdc9605eabdeb5626f18f5721f343d" - }, - "web-search": { - "url": "http://mcp.ideapsound.com:8000/sse" + "deepsound-mcp": { + "url": "https://api.deapsound.com/sse" } } diff --git a/lib/data/services/open_ai_service_adapter.dart b/lib/data/services/open_ai_service_adapter.dart index 7052a7157..83fcc1f79 100644 --- a/lib/data/services/open_ai_service_adapter.dart +++ b/lib/data/services/open_ai_service_adapter.dart @@ -9,14 +9,15 @@ import 'package:flutter/services.dart'; import 'package:path_provider/path_provider.dart'; /// 定义流事件类型,用于区分不同类型的事件 -enum StreamEventType { token, complete, error } +enum StreamEventType { token, functionCall, complete, error } /// 流事件包装类 class StreamEvent { final StreamEventType type; final String? content; + final Map? meta; - StreamEvent(this.type, {this.content}); + StreamEvent(this.type, {this.content, this.meta}); } /// OpenAI服务适配器 - 连接AiService接口与OpenAIService插件 @@ -63,6 +64,9 @@ class OpenAIServiceAdapter implements AiService { printInfo( info: '接收到 functionCall content: ${event.content} meta: ${event.meta}'); + _tokenStreamController.add(StreamEvent( + StreamEventType.functionCall, + meta: event.meta as Map?)); // _tokenStreamController.add(StreamEvent( // StreamEventType.error, // content: '收到函数调用,该流仅支持文本响应' @@ -170,7 +174,7 @@ class OpenAIServiceAdapter implements AiService { /// 发送消息并获取流式回复 @override - Stream sendMessageStream({ + Stream sendMessageStream({ required List> messages, required String systemPrompt, }) async* { @@ -180,7 +184,7 @@ class OpenAIServiceAdapter implements AiService { messages.map((m) => Map.from(m)).toList(); // 创建用于接收token的控制器 - final localController = StreamController(); + final localController = StreamController(); // 添加从广播流到本地流的订阅 final subscription = _tokenStreamController.stream.listen((streamEvent) { @@ -190,6 +194,11 @@ class OpenAIServiceAdapter implements AiService { localController.add(streamEvent.content!); } break; + case StreamEventType.functionCall: + if (streamEvent.meta != null && !localController.isClosed) { + localController.add(streamEvent.meta!); + } + break; case StreamEventType.complete: if (!localController.isClosed) { localController.close(); diff --git a/lib/data/services/spotify_service.dart b/lib/data/services/spotify_service.dart new file mode 100644 index 000000000..6ccf56389 --- /dev/null +++ b/lib/data/services/spotify_service.dart @@ -0,0 +1,176 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:convert'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:spotify_sdk/spotify_sdk.dart'; +import 'package:get/get.dart'; +import 'ai_service.dart'; +import 'package:flutter/services.dart'; +import 'package:path_provider/path_provider.dart'; + +/// OpenAI服务适配器 - 连接AiService接口与OpenAIService插件 +class SpotifService { + bool isInitialized = false; + + /// 初始化OpenAI服务 + Future initialize() async { + if (!isInitialized) { + getAccessToken(); + connectToSpotifyRemote(); + isInitialized = true; + printInfo(info: "SpotifService----------- initialize"); + } + + return true; + } + + /// 获取Spotify SDK的版本信息 + Future connectToSpotifyRemote() async { + try { + var result = await SpotifySdk.connectToSpotifyRemote( + clientId: dotenv.env['SPOTIFY_CLIENT_ID'].toString(), + redirectUrl: dotenv.env['SPOTIFY_REDIRECT_URL'].toString(), + scope: + 'user-read-private user-read-email streaming app-remote-control user-modify-playback-state', + ); + printInfo( + info: "SpotifService----------- connectToSpotifyRemote $result"); + } on PlatformException catch (e) { + printError(info: "SpotifService------------ PlatformException$e"); + } on MissingPluginException { + printError(info: "SpotifService------------ MissingPluginException"); + } + } + + /// 获取Spotify的访问令牌 + Future getAccessToken() async { + try { + var authenticationToken = await SpotifySdk.getAccessToken( + clientId: dotenv.env['SPOTIFY_CLIENT_ID'].toString(), + redirectUrl: dotenv.env['SPOTIFY_REDIRECT_URL'].toString(), + scope: 'app-remote-control, ' + 'user-modify-playback-state, ' + 'playlist-read-private, ' + 'playlist-modify-public,user-read-currently-playing', + ); + printInfo( + info: "SpotifService----------- getAccessToken $authenticationToken"); + return authenticationToken; + } on PlatformException catch (e) { + printError( + info: + "SpotifService------------getAccessToken ${e.code}: ${e.message}"); + return Future.error('$e.code: $e.message'); + } on MissingPluginException { + printError( + info: "SpotifService------------getAccessToken not implemented"); + return Future.error('not implemented'); + } + } + + //播放 音乐 + Future play(String url) async { + try { + await SpotifySdk.play(spotifyUri: url); + printInfo(info: "SpotifService----------- play $url"); + } on PlatformException catch (e) { + printError(info: "SpotifService------------play ${e.code}: ${e.message}"); + } on MissingPluginException { + printError( + info: "SpotifService------------getAccessToken not implemented"); + } + } + + //暂停 音乐 + Future pause() async { + try { + await SpotifySdk.pause(); + printInfo(info: "SpotifService----------- pause"); + } on PlatformException catch (e) { + printError( + info: "SpotifService------------pause ${e.code}: ${e.message}"); + } on MissingPluginException { + printError( + info: "SpotifService------------getAccessToken not implemented"); + } + } + + //停止 音乐 + Future resume() async { + try { + await SpotifySdk.resume(); + printInfo(info: "SpotifService----------- resume"); + } on PlatformException catch (e) { + printError( + info: "SpotifService------------pause ${e.code}: ${e.message}"); + } on MissingPluginException { + printError( + info: "SpotifService------------getAccessToken not implemented"); + } + } + + //下一首 音乐 + Future skipNext() async { + try { + await SpotifySdk.skipNext(); + } on PlatformException catch (e) { + printError( + info: "SpotifService------------pause ${e.code}: ${e.message}"); + } on MissingPluginException { + printError( + info: "SpotifService------------getAccessToken not implemented"); + } + } + + //跳过上一首 + Future skipPrevious() async { + try { + await SpotifySdk.skipPrevious(); + } on PlatformException catch (e) { + printError( + info: "SpotifService------------pause ${e.code}: ${e.message}"); + } on MissingPluginException { + printError( + info: "SpotifService------------getAccessToken not implemented"); + } + } + + //获取当前播放状态 + Future seekTo() async { + try { + await SpotifySdk.seekTo(positionedMilliseconds: 20000); + } on PlatformException catch (e) { + printError( + info: "SpotifService------------pause ${e.code}: ${e.message}"); + } on MissingPluginException { + printError( + info: "SpotifService------------getAccessToken not implemented"); + } + } + + //获取当前播放状态 + Future seekToRelative() async { + try { + await SpotifySdk.seekToRelativePosition(relativeMilliseconds: 20000); + } on PlatformException catch (e) { + printError( + info: "SpotifService------------pause ${e.code}: ${e.message}"); + } on MissingPluginException { + printError( + info: "SpotifService------------getAccessToken not implemented"); + } + } + + //获取当前播放状态 + Future switchToLocalDevice() async { + try { + await SpotifySdk.switchToLocalDevice(); + } on PlatformException catch (e) { + printError( + info: "SpotifService------------pause ${e.code}: ${e.message}"); + } on MissingPluginException { + printError( + info: "SpotifService------------getAccessToken not implemented"); + } + } +} diff --git a/lib/modules/chat/controllers/chat_controller.dart b/lib/modules/chat/controllers/chat_controller.dart index c08f6a51b..2cc4856f5 100644 --- a/lib/modules/chat/controllers/chat_controller.dart +++ b/lib/modules/chat/controllers/chat_controller.dart @@ -1,9 +1,11 @@ import 'dart:async'; +import 'package:deep_voice/data/services/spotify_service.dart'; import 'package:get/get.dart'; import 'package:flutter/widgets.dart'; import 'package:get_storage/get_storage.dart'; import 'package:just_audio/just_audio.dart'; import 'package:chat_storage/chat_storage.dart'; +import 'package:path/path.dart'; import '../../../data/services/ai_service.dart'; import '../../../data/services/deapsound_ai_service.dart'; import '../../../data/services/kouzi_ai_service.dart'; @@ -25,6 +27,7 @@ class ChatController extends GetxController { final AsrService _asrService = Get.find(); final VoiceInteractionService _voiceInteractionService = Get.find(); + late SpotifService _spotifService; // 添加ScrollController用于控制列表滚动 final ScrollController scrollController = ScrollController(); @@ -117,7 +120,8 @@ class ChatController extends GetxController { _aiService = Get.find(); Logger.info('使用OpenAIServiceAdapter'); } - + Get.put(SpotifService()); + _spotifService = Get.find(); // 使用克隆音色语音合成 final GetStorage storage = GetStorage(); String timbre = storage.read('timbre') ?? '1'; @@ -370,11 +374,11 @@ class ChatController extends GetxController { _aiResponseSubscription = responseStream.listen( (chunk) { if (_shouldCancelAiResponse) return; - + Logger.info('chat ${chunk}'); if (chunk is Map) { _updateIntermediateMessage(data: chunk); - if (chunk['musicaudio'] != null) { - play(chunk['musicaudio']); + if (chunk['card_spotify'] != null) { + play(chunk['card_spotify']); } } else { fullResponse += chunk; @@ -453,18 +457,10 @@ class ChatController extends GetxController { } // 播放音乐 - void play(Map musicaudio) async { + void play(Map card) async { try { - isPlay.value = false; - if (audiourl.value != musicaudio['audiourl']) { - audiourl.value = ''; - await player.setUrl(musicaudio['audiourl']); - audiourl.value = musicaudio['audiourl']; - player.play(); - } else { - player.play(); - } - isPlay.value = true; + await _spotifService.initialize(); + _spotifService.play(card['url'] as String); } catch (e) { Logger.error('加载异常: $e'); } diff --git a/lib/modules/chat/views/chat_music_card.dart b/lib/modules/chat/views/chat_music_card.dart index 9cade7e3e..5519c7143 100644 --- a/lib/modules/chat/views/chat_music_card.dart +++ b/lib/modules/chat/views/chat_music_card.dart @@ -12,8 +12,8 @@ class ChatMusicCard extends GetView { @override Widget build(BuildContext context) { - final Map musiccards = data?['musiccards'].first; - final Map musicaudio = data?['musicaudio']; + // final Map musiccards = data?['musiccards'].first; + // final Map musicaudio = data?['musicaudio']; return Row( children: [ @@ -25,14 +25,14 @@ class ChatMusicCard extends GetView { borderRadius: BorderRadius.circular(6.r), ), child: Image.network( - musiccards['image'], + data?['image'], fit: BoxFit.cover, ), ), 10.horizontalSpace, Expanded( child: Text( - musiccards['name'], + data?['name'], overflow: TextOverflow.ellipsis, style: TextStyle( color: isDarkMode ? Colors.white : Colors.black, @@ -43,8 +43,7 @@ class ChatMusicCard extends GetView { ), 10.horizontalSpace, Obx( - () => controller.audiourl == musicaudio['audiourl'] && - controller.isPlay.value + () => controller.audiourl == data?['url'] && controller.isPlay.value ? GestureDetector( onTap: () { controller.pause(); @@ -52,30 +51,6 @@ class ChatMusicCard extends GetView { child: Stack( alignment: Alignment.center, children: [ - SizedBox( - width: 38.w, - height: 38.w, - child: StreamBuilder( - stream: controller.player.positionStream, - builder: (context, snapshot) { - final position = snapshot.data ?? Duration.zero; - final total = - controller.player.duration ?? Duration.zero; - final progress = total.inSeconds > 0 - ? position.inSeconds / total.inSeconds - : 0.0; - return CircularProgressIndicator( - value: progress, - backgroundColor: isDarkMode - ? Colors.grey[700]! - : Colors.grey[300]!, - valueColor: AlwaysStoppedAnimation( - isDarkMode ? Colors.white : Colors.black), - strokeWidth: 2, // 线条粗细 - ); - }, - ), - ), Icon( Icons.pause_rounded, color: isDarkMode ? Colors.white : Colors.black, @@ -85,7 +60,7 @@ class ChatMusicCard extends GetView { ) : GestureDetector( onTap: () { - controller.play(musicaudio); + controller.play(data!); }, child: Container( width: 38.w, diff --git a/lib/modules/chat/views/chat_view.dart b/lib/modules/chat/views/chat_view.dart index f46f8f3c8..7318bbd39 100644 --- a/lib/modules/chat/views/chat_view.dart +++ b/lib/modules/chat/views/chat_view.dart @@ -356,11 +356,11 @@ class ChatView extends GetView { ); } if (message.data != null) { - if (message.data?['weatherDays'] != null) { + if (message.data?['card_weather'] != null) { box = WeatherDayCard(message.data, isDarkMode); } - if (message.data?['musicaudio'] != null) { - box = ChatMusicCard(message.data, isDarkMode); + if (message.data?['card_spotify'] != null) { + box = ChatMusicCard(message.data?['card_spotify'], isDarkMode); } if (message.data?['navigationcard'] != null) { box = ChatNavigationCard(message.data, isDarkMode); 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 15f0dc761..60acd48db 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 @@ -391,10 +391,10 @@ class MCPSubClient( else -> "" } } - JSONObject().apply {put("meta",result?._meta?.toMap()); put("context",context) } + JSONObject().apply { put("name",name);put("meta",result?._meta?.toMap()); put("context",context) } } catch (e: Exception) { Log.e(TAG, "调用MCP工具失败: ${e.message}", e) - JSONObject().apply { put("context", "调用MCP工具失败: ${e.message}") } + JSONObject().apply { put("context", "调用MCP工具失败: ${e.message}")} } } diff --git a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIServicePlugin.kt b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIServicePlugin.kt index 40af80aa9..1948a2f13 100644 --- a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIServicePlugin.kt +++ b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIServicePlugin.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlin.math.min /** OpenAIServicePlugin */ class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler { @@ -164,24 +165,35 @@ class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler { } override fun onFunctionCall(functionCall: JSONObject) { - Log.e("OpenAIServicePlugin", "工具调用结果: ${functionCall.toString()}") + Log.e("OpenAIServicePlugin", "context:${functionCall.toString()}") val context = functionCall["context"] -// val meta = functionCall["meta"] - val meta: Map? = functionCall.optJSONObject("meta")?.let { metaJson -> - metaJson.keys().asSequence().associateWith { key -> - metaJson.get(key) // 或 opt(key),更安全 + if (functionCall.has("meta")) { + val meta = mapOf( + "card_spotify" to mapOf( + "name" to "泡沫", + "image" to "https://i.scdn.co/image/ab67616d0000b2739f6a38e4bb9c2f824c215e58", + "url" to "spotify:track:2IfSPLPDW6RGosdB5huwAA", + "sgener" to "Xposed", + ) + ) + Log.e("OpenAIServicePlugin", "工具调用结果 context:${context} meta:${meta.toString()}") + // 发送函数调用事件 + mainHandler.post { + eventSink?.success(mapOf("type" to "functionCall", "content" to context,"meta" to meta)) } } - val card_spotifyKey = meta?.containsKey("card_spotify") ?: false - if (card_spotifyKey) { //是否是音乐卡片 - } +// val meta: Map? = functionCall.optJSONObject("meta")?.let { metaJson -> +// metaJson.keys().asSequence().associateWith { key -> +// metaJson.get(key) // 或 opt(key),更安全 +// } +// } ?: mapOf( +// // 如果 null,就用默认值 +// "card_spotify" to { +// "url" to "spotify:track:58kNJana4w5BIjlZE2wq5m" +// }, +// ) - // 发送函数调用事件 - mainHandler.post { -// val functionCallMap = functionCall.toMap() - eventSink?.success(mapOf("type" to "functionCall", "content" to context,"meta" to meta)) - } } } ) diff --git a/local_plugins/open_ai_service/lib/open_ai_service.dart b/local_plugins/open_ai_service/lib/open_ai_service.dart index 022d53dbd..f67d16169 100644 --- a/local_plugins/open_ai_service/lib/open_ai_service.dart +++ b/local_plugins/open_ai_service/lib/open_ai_service.dart @@ -25,17 +25,19 @@ enum OpenAIEventType { class OpenAIEvent { final OpenAIEventType type; final dynamic content; - final dynamic meta; + final Map? meta; OpenAIEvent({required this.type, this.content, this.meta}); factory OpenAIEvent.fromMap(Map map) { final typeStr = map['type'] as String; final content = map['content']; - + final rawMeta = map['meta']; + final meta = rawMeta is Map ? Map.from(rawMeta) : null; return OpenAIEvent( type: _typeFromString(typeStr), content: content, + meta: meta, ); } @@ -77,6 +79,7 @@ class OpenAIService { // 监听原生事件 _eventChannel.receiveBroadcastStream().listen( (dynamic event) { + print("收到事件 OpenAIEvent: $event"); if (event is Map) { final eventMap = Map.from(event); final openAIEvent = OpenAIEvent.fromMap(eventMap);