Browse Source

上传音乐代码

weicu
liwei1dao 2 years ago
parent
commit
343fc87606
  1. 8
      .mcp.json
  2. 17
      lib/data/services/open_ai_service_adapter.dart
  3. 176
      lib/data/services/spotify_service.dart
  4. 26
      lib/modules/chat/controllers/chat_controller.dart
  5. 37
      lib/modules/chat/views/chat_music_card.dart
  6. 6
      lib/modules/chat/views/chat_view.dart
  7. 4
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt
  8. 38
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIServicePlugin.kt
  9. 7
      local_plugins/open_ai_service/lib/open_ai_service.dart

8
.mcp.json

@ -1,11 +1,7 @@
{ {
"mcpServers": { "mcpServers": {
"deepsound-mcp": {
"amap-amap-sse": { "url": "https://api.deapsound.com/sse"
"url": "https://mcp.amap.com/sse?key=e5fdc9605eabdeb5626f18f5721f343d"
},
"web-search": {
"url": "http://mcp.ideapsound.com:8000/sse"
} }
} }

17
lib/data/services/open_ai_service_adapter.dart

@ -9,14 +9,15 @@ import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
/// 定义流事件类型,用于区分不同类型的事件 /// 定义流事件类型,用于区分不同类型的事件
enum StreamEventType { token, complete, error } enum StreamEventType { token, functionCall, complete, error }
/// 流事件包装类 /// 流事件包装类
class StreamEvent { class StreamEvent {
final StreamEventType type; final StreamEventType type;
final String? content; final String? content;
final Map? meta;
StreamEvent(this.type, {this.content}); StreamEvent(this.type, {this.content, this.meta});
} }
/// OpenAI服务适配器 - 连接AiService接口与OpenAIService插件 /// OpenAI服务适配器 - 连接AiService接口与OpenAIService插件
@ -63,6 +64,9 @@ class OpenAIServiceAdapter implements AiService {
printInfo( printInfo(
info: info:
'接收到 functionCall content: ${event.content} meta: ${event.meta}'); '接收到 functionCall content: ${event.content} meta: ${event.meta}');
_tokenStreamController.add(StreamEvent(
StreamEventType.functionCall,
meta: event.meta as Map<String, dynamic>?));
// _tokenStreamController.add(StreamEvent( // _tokenStreamController.add(StreamEvent(
// StreamEventType.error, // StreamEventType.error,
// content: '收到函数调用,该流仅支持文本响应' // content: '收到函数调用,该流仅支持文本响应'
@ -170,7 +174,7 @@ class OpenAIServiceAdapter implements AiService {
/// 发送消息并获取流式回复 /// 发送消息并获取流式回复
@override @override
Stream<String> sendMessageStream({ Stream sendMessageStream({
required List<Map<String, String>> messages, required List<Map<String, String>> messages,
required String systemPrompt, required String systemPrompt,
}) async* { }) async* {
@ -180,7 +184,7 @@ class OpenAIServiceAdapter implements AiService {
messages.map((m) => Map<String, dynamic>.from(m)).toList(); messages.map((m) => Map<String, dynamic>.from(m)).toList();
// 创建用于接收token的控制器 // 创建用于接收token的控制器
final localController = StreamController<String>(); final localController = StreamController();
// 添加从广播流到本地流的订阅 // 添加从广播流到本地流的订阅
final subscription = _tokenStreamController.stream.listen((streamEvent) { final subscription = _tokenStreamController.stream.listen((streamEvent) {
@ -190,6 +194,11 @@ class OpenAIServiceAdapter implements AiService {
localController.add(streamEvent.content!); localController.add(streamEvent.content!);
} }
break; break;
case StreamEventType.functionCall:
if (streamEvent.meta != null && !localController.isClosed) {
localController.add(streamEvent.meta!);
}
break;
case StreamEventType.complete: case StreamEventType.complete:
if (!localController.isClosed) { if (!localController.isClosed) {
localController.close(); localController.close();

176
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<bool> initialize() async {
if (!isInitialized) {
getAccessToken();
connectToSpotifyRemote();
isInitialized = true;
printInfo(info: "SpotifService----------- initialize");
}
return true;
}
/// 获取Spotify SDK的版本信息
Future<void> 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<String> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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");
}
}
}

26
lib/modules/chat/controllers/chat_controller.dart

@ -1,9 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'package:deep_voice/data/services/spotify_service.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
import 'package:chat_storage/chat_storage.dart'; import 'package:chat_storage/chat_storage.dart';
import 'package:path/path.dart';
import '../../../data/services/ai_service.dart'; import '../../../data/services/ai_service.dart';
import '../../../data/services/deapsound_ai_service.dart'; import '../../../data/services/deapsound_ai_service.dart';
import '../../../data/services/kouzi_ai_service.dart'; import '../../../data/services/kouzi_ai_service.dart';
@ -25,6 +27,7 @@ class ChatController extends GetxController {
final AsrService _asrService = Get.find<AsrService>(); final AsrService _asrService = Get.find<AsrService>();
final VoiceInteractionService _voiceInteractionService = final VoiceInteractionService _voiceInteractionService =
Get.find<VoiceInteractionService>(); Get.find<VoiceInteractionService>();
late SpotifService _spotifService;
// 添加ScrollController用于控制列表滚动 // 添加ScrollController用于控制列表滚动
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
@ -117,7 +120,8 @@ class ChatController extends GetxController {
_aiService = Get.find<OpenAIServiceAdapter>(); _aiService = Get.find<OpenAIServiceAdapter>();
Logger.info('使用OpenAIServiceAdapter'); Logger.info('使用OpenAIServiceAdapter');
} }
Get.put(SpotifService());
_spotifService = Get.find<SpotifService>();
// 使用克隆音色语音合成 // 使用克隆音色语音合成
final GetStorage storage = GetStorage(); final GetStorage storage = GetStorage();
String timbre = storage.read('timbre') ?? '1'; String timbre = storage.read('timbre') ?? '1';
@ -370,11 +374,11 @@ class ChatController extends GetxController {
_aiResponseSubscription = responseStream.listen( _aiResponseSubscription = responseStream.listen(
(chunk) { (chunk) {
if (_shouldCancelAiResponse) return; if (_shouldCancelAiResponse) return;
Logger.info('chat ${chunk}');
if (chunk is Map) { if (chunk is Map) {
_updateIntermediateMessage(data: chunk); _updateIntermediateMessage(data: chunk);
if (chunk['musicaudio'] != null) { if (chunk['card_spotify'] != null) {
play(chunk['musicaudio']); play(chunk['card_spotify']);
} }
} else { } else {
fullResponse += chunk; fullResponse += chunk;
@ -453,18 +457,10 @@ class ChatController extends GetxController {
} }
// 播放音乐 // 播放音乐
void play(Map musicaudio) async { void play(Map card) async {
try { try {
isPlay.value = false; await _spotifService.initialize();
if (audiourl.value != musicaudio['audiourl']) { _spotifService.play(card['url'] as String);
audiourl.value = '';
await player.setUrl(musicaudio['audiourl']);
audiourl.value = musicaudio['audiourl'];
player.play();
} else {
player.play();
}
isPlay.value = true;
} catch (e) { } catch (e) {
Logger.error('加载异常: $e'); Logger.error('加载异常: $e');
} }

37
lib/modules/chat/views/chat_music_card.dart

@ -12,8 +12,8 @@ class ChatMusicCard extends GetView<ChatController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Map musiccards = data?['musiccards'].first; // final Map musiccards = data?['musiccards'].first;
final Map musicaudio = data?['musicaudio']; // final Map musicaudio = data?['musicaudio'];
return Row( return Row(
children: [ children: [
@ -25,14 +25,14 @@ class ChatMusicCard extends GetView<ChatController> {
borderRadius: BorderRadius.circular(6.r), borderRadius: BorderRadius.circular(6.r),
), ),
child: Image.network( child: Image.network(
musiccards['image'], data?['image'],
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),
10.horizontalSpace, 10.horizontalSpace,
Expanded( Expanded(
child: Text( child: Text(
musiccards['name'], data?['name'],
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black, color: isDarkMode ? Colors.white : Colors.black,
@ -43,8 +43,7 @@ class ChatMusicCard extends GetView<ChatController> {
), ),
10.horizontalSpace, 10.horizontalSpace,
Obx( Obx(
() => controller.audiourl == musicaudio['audiourl'] && () => controller.audiourl == data?['url'] && controller.isPlay.value
controller.isPlay.value
? GestureDetector( ? GestureDetector(
onTap: () { onTap: () {
controller.pause(); controller.pause();
@ -52,30 +51,6 @@ class ChatMusicCard extends GetView<ChatController> {
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
SizedBox(
width: 38.w,
height: 38.w,
child: StreamBuilder<Duration>(
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<Color>(
isDarkMode ? Colors.white : Colors.black),
strokeWidth: 2, // 线条粗细
);
},
),
),
Icon( Icon(
Icons.pause_rounded, Icons.pause_rounded,
color: isDarkMode ? Colors.white : Colors.black, color: isDarkMode ? Colors.white : Colors.black,
@ -85,7 +60,7 @@ class ChatMusicCard extends GetView<ChatController> {
) )
: GestureDetector( : GestureDetector(
onTap: () { onTap: () {
controller.play(musicaudio); controller.play(data!);
}, },
child: Container( child: Container(
width: 38.w, width: 38.w,

6
lib/modules/chat/views/chat_view.dart

@ -356,11 +356,11 @@ class ChatView extends GetView<ChatController> {
); );
} }
if (message.data != null) { if (message.data != null) {
if (message.data?['weatherDays'] != null) { if (message.data?['card_weather'] != null) {
box = WeatherDayCard(message.data, isDarkMode); box = WeatherDayCard(message.data, isDarkMode);
} }
if (message.data?['musicaudio'] != null) { if (message.data?['card_spotify'] != null) {
box = ChatMusicCard(message.data, isDarkMode); box = ChatMusicCard(message.data?['card_spotify'], isDarkMode);
} }
if (message.data?['navigationcard'] != null) { if (message.data?['navigationcard'] != null) {
box = ChatNavigationCard(message.data, isDarkMode); box = ChatNavigationCard(message.data, isDarkMode);

4
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt

@ -391,10 +391,10 @@ class MCPSubClient(
else -> "" 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) { } catch (e: Exception) {
Log.e(TAG, "调用MCP工具失败: ${e.message}", e) Log.e(TAG, "调用MCP工具失败: ${e.message}", e)
JSONObject().apply { put("context", "调用MCP工具失败: ${e.message}") } JSONObject().apply { put("context", "调用MCP工具失败: ${e.message}")}
} }
} }

38
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.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.math.min
/** OpenAIServicePlugin */ /** OpenAIServicePlugin */
class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler { class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler {
@ -164,24 +165,35 @@ class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler {
} }
override fun onFunctionCall(functionCall: JSONObject) { override fun onFunctionCall(functionCall: JSONObject) {
Log.e("OpenAIServicePlugin", "工具调用结果: ${functionCall.toString()}") Log.e("OpenAIServicePlugin", "context:${functionCall.toString()}")
val context = functionCall["context"] val context = functionCall["context"]
// val meta = functionCall["meta"] if (functionCall.has("meta")) {
val meta: Map<String, Any>? = functionCall.optJSONObject("meta")?.let { metaJson -> val meta = mapOf<String, Any>(
metaJson.keys().asSequence().associateWith { key -> "card_spotify" to mapOf<String, Any>(
metaJson.get(key) // 或 opt(key),更安全 "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<String, Any>? = 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))
}
} }
} }
) )

7
local_plugins/open_ai_service/lib/open_ai_service.dart

@ -25,17 +25,19 @@ enum OpenAIEventType {
class OpenAIEvent { class OpenAIEvent {
final OpenAIEventType type; final OpenAIEventType type;
final dynamic content; final dynamic content;
final dynamic meta; final Map<String, dynamic>? meta;
OpenAIEvent({required this.type, this.content, this.meta}); OpenAIEvent({required this.type, this.content, this.meta});
factory OpenAIEvent.fromMap(Map<String, dynamic> map) { factory OpenAIEvent.fromMap(Map<String, dynamic> map) {
final typeStr = map['type'] as String; final typeStr = map['type'] as String;
final content = map['content']; final content = map['content'];
final rawMeta = map['meta'];
final meta = rawMeta is Map ? Map<String, dynamic>.from(rawMeta) : null;
return OpenAIEvent( return OpenAIEvent(
type: _typeFromString(typeStr), type: _typeFromString(typeStr),
content: content, content: content,
meta: meta,
); );
} }
@ -77,6 +79,7 @@ class OpenAIService {
// 监听原生事件 // 监听原生事件
_eventChannel.receiveBroadcastStream().listen( _eventChannel.receiveBroadcastStream().listen(
(dynamic event) { (dynamic event) {
print("收到事件 OpenAIEvent: $event");
if (event is Map<dynamic, dynamic>) { if (event is Map<dynamic, dynamic>) {
final eventMap = Map<String, dynamic>.from(event); final eventMap = Map<String, dynamic>.from(event);
final openAIEvent = OpenAIEvent.fromMap(eventMap); final openAIEvent = OpenAIEvent.fromMap(eventMap);

Loading…
Cancel
Save