Browse Source

上传音乐服务和音乐列表实现

weicu
liwei1dao 1 year ago
parent
commit
29fa0d6a27
  1. 4
      lib/data/models/appconfig.dart
  2. 3
      lib/data/services/ble_manager.dart
  3. 21
      lib/data/services/music_manager.dart
  4. 50
      lib/modules/agent/controllers/agent_controller.dart
  5. 83
      lib/modules/agent/views/message_bubble.dart
  6. 51
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  7. 1
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  8. 3
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt
  9. 42
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt
  10. 15
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt
  11. 11
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt
  12. 7
      local_plugins/music_service/lib/music_service.dart

4
lib/data/models/appconfig.dart

@ -21,8 +21,8 @@ class AppConfig {
}
//智能体配置
static String agent(String agentId) {
static String? agentsystemPrompt(String agentId) {
final foundAgent = config.agents.firstWhere((agent) => agent.id == agentId);
return jsonEncode((foundAgent));
return foundAgent.systemPrompt;
}
}

3
lib/data/services/ble_manager.dart

@ -404,6 +404,8 @@ class BleManager extends GetxService {
final openaiBaseUrl =
AppConfig.env('OPENAI_BASE_URL') ?? 'https://api.openai.com/v1';
final openaiModel = AppConfig.env('OPENAI_MODEL') ?? 'gpt-3.5-turbo';
final systemPrompt = AppConfig.agentsystemPrompt("personal_assistant");
// 从Flutter资源包中加载.mcp.json
final jsonData = AppConfig.mcpConfig();
@ -414,6 +416,7 @@ class BleManager extends GetxService {
'openaiApiKey': openaiApiKey,
'openaiBaseUrl': openaiBaseUrl,
'openaiModel': openaiModel,
'systemPrompt': systemPrompt,
'mcpServer': jsonEncode(jsonData),
};

21
lib/data/services/music_manager.dart

@ -22,6 +22,23 @@ class MusicManager extends GetxService {
super.onClose();
}
//播放 音乐
Future<void> play(String url) async {}
//播放
Future<void> play(String url) async {
await _musicService.playMusic(url);
}
//暂停
Future<void> pause() async {
await _musicService.pauseMusic();
}
//继续
Future<void> resume() async {
await _musicService.resumeMusic();
}
//状态
Future<Map<String, dynamic>> getStatus() async {
return await _musicService.getStatus();
}
}

50
lib/modules/agent/controllers/agent_controller.dart

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:deep_voice/data/services/music_manager.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:agent_service/agent_service.dart';
@ -40,6 +41,11 @@ class AgentController extends GetxController {
final TextEditingController textController = TextEditingController();
final ScrollController scrollController = ScrollController();
//音乐播放管理
final _musiceManager = Get.find<MusicManager>();
var isPlaying = false.obs;
var currentUrl = "".obs;
// 状态标志
final isInitialized = false.obs;
final isListening = false.obs;
@ -101,13 +107,17 @@ class AgentController extends GetxController {
if (result.messages.isNotEmpty) {
// 转换并添加消息到列表
final historyMessages = result.messages
.map((msg) => Message(
isUser: msg.sender == 'user',
text: msg.message,
timestamp: DateTime.parse(msg.timestamp),
))
.toList();
final historyMessages = result.messages.map((msg) {
final meta = msg.metadata != null
? jsonDecode(msg.metadata!)
: null; // 如果为空,使用空对象
return Message(
isUser: msg.sender == 'user',
text: msg.message,
meta: meta,
timestamp: DateTime.parse(msg.timestamp),
);
}).toList();
// 消息按时间排序,最新的消息在底部
historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
@ -607,4 +617,30 @@ class AgentController extends GetxController {
void translateImageText() {
useImagePrompt('请翻译这张图片中的文字');
}
// 播放音乐
void musicStatus() async {
var status = await _musiceManager.getStatus();
logger.i('获取当前音乐播放状态 $status');
isPlaying.value = status['isPlaying'];
currentUrl.value = status['url'];
}
// 播放音乐
void musicPlay(String url) {
_musiceManager.play(url);
isPlaying.value = true;
currentUrl.value = url;
}
// 播放音乐
void musicPause() {
_musiceManager.pause();
isPlaying.value = false;
}
void musicResume() {
_musiceManager.resume();
isPlaying.value = true;
}
}

83
lib/modules/agent/views/message_bubble.dart

@ -1,9 +1,12 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'dart:io';
import '../controllers/agent_controller.dart';
/// 消息气泡组件
class MessageBubble extends StatelessWidget {
class MessageBubble extends GetView<AgentController> {
final bool isUser;
final String message;
final DateTime timestamp;
@ -199,8 +202,14 @@ class MessageBubble extends StatelessWidget {
//音乐卡片
return _buildMusicItem(context, musicName, musicUrl, musicSinger);
} else if (mate!.keys.contains('card_musiclist')) {
final rawList = mate!['card_musiclist']['musics'] as List;
final playlist =
rawList.map((e) => Map<String, dynamic>.from(e)).toList();
// final playlist =
// mate!['card_musiclist']['musics'] as List<Map<String, String>>;
//音乐播放列表
return _buildMusicContent(context);
return _buildMusicContent(context, playlist);
} else if (mate!.keys.contains('card_weather')) {
//天气
return _buildWeatherContent(context);
@ -250,11 +259,12 @@ class MessageBubble extends StatelessWidget {
}
/// 构建音乐内容卡片
Widget _buildMusicContent(BuildContext context) {
Widget _buildMusicContent(
BuildContext context, List<Map<String, dynamic>> playlist) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
const Text(
'以下是一些推荐的音乐:',
style: TextStyle(
color: Colors.black87,
@ -301,18 +311,22 @@ class MessageBubble extends StatelessWidget {
),
const SizedBox(height: 10),
// 歌曲示例
_buildMusicItem(
context,
'雨声与钢琴曲',
"",
'纯音乐',
),
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
_buildMusicItem(context, 'Rainy Day', "", 'Coldplay'),
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
_buildMusicItem(context, '雨天的爵士咖啡馆', "", '爵士合集'),
// 动态生成音乐条目
...playlist.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
final name = item["name"] ?? "未知歌曲";
final url = item["url"] ?? "";
final singer = item["sgener"] ?? "未知歌手";
return Column(
children: [
_buildMusicItem(context, name, url, singer),
if (index != playlist.length - 1)
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
],
);
}).toList(),
const SizedBox(height: 8),
Center(
child: TextButton(
@ -507,6 +521,8 @@ class MessageBubble extends StatelessWidget {
/// 构建音乐项目
Widget _buildMusicItem(
BuildContext context, String name, String url, String singer) {
final controller = Get.find<AgentController>(); // 获取控制器
controller.musicStatus(); //刷新状态
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
@ -550,14 +566,35 @@ class MessageBubble extends StatelessWidget {
],
),
),
IconButton(
onPressed: () {},
icon:
Icon(Icons.play_circle, color: Theme.of(context).primaryColor),
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
iconSize: 24,
),
Obx(() {
final isThisPlaying = controller.isPlaying.value &&
controller.currentUrl.value == url;
return IconButton(
onPressed: () {
if (controller.isPlaying.value) {
if (controller.currentUrl.value == url) {
controller.musicPause();
} else {
controller.musicPlay(url);
}
} else {
if (controller.currentUrl.value == url) {
controller.musicResume();
} else {
controller.musicPlay(url);
}
}
},
icon: Icon(
isThisPlaying ? Icons.pause_circle : Icons.play_circle,
color: Theme.of(context).primaryColor,
),
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
iconSize: 24,
);
}),
],
),
);

51
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -132,6 +132,7 @@ object AgentService : CoroutineScope {
// 自定义系统提示词
val customSystemPrompt = config["systemPrompt"] as? String
FileLogger.i(TAG, "初始化 系统提示词:$config")
if (!customSystemPrompt.isNullOrBlank()) {
systemPrompt = customSystemPrompt
} else {
@ -559,6 +560,7 @@ object AgentService : CoroutineScope {
// 使用历史记录作为上下文发送到OpenAI
val responseBuilder = StringBuilder()
var metadata:String = ""
// 添加系统提示词到历史消息的副本中
val messagesWithSystemPrompt = JSONArray()
@ -637,7 +639,7 @@ object AgentService : CoroutineScope {
addToHistoryMessages(openAIService.createAssistantMessage(response))
// 保存聊天记录
saveChatMessage(displayText, response)
saveChatMessage(displayText, response,metadata)
}
// 标记AI流式输出已完成
@ -675,15 +677,38 @@ object AgentService : CoroutineScope {
FileLogger.d(TAG, "mcp调用结果: $functionCallResult")
val metaStr = functionCallResult.optString("meta")
if (metaStr.isNotEmpty()) {
metadata = metaStr;
val meta = JSONObject(metaStr)
FileLogger.d(TAG, "mcp调用结果: $metaStr")
if (meta.has("card_music")) {
if (meta.has("card_music")) { //音乐卡片
val cardMusic = meta.getJSONObject("card_music")
FileLogger.d(TAG, "检查到音乐卡片: $cardMusic")
val musicUrl = cardMusic.optString("url", "")
val name = cardMusic.optString("name", "")
val sgener = cardMusic.optString("sgener", "")
processMusicPlay(musicUrl, name, sgener)
}else if (meta.has("card_musiclist")) { //音乐列表
val cardMusiclist = meta.getJSONObject("card_musiclist")
val musics = cardMusiclist.optJSONArray("musics")
if (musics != null) {
val playlist = mutableListOf<Map<String, String>>()
for (i in 0 until musics.length()) {
val item = musics.optJSONObject(i) ?: continue
val url = item.optString("url", "")
val name = item.optString("name", "")
val sgener = item.optString("sgener", "")
if (url.isNotEmpty()) {
playlist.add(mapOf("url" to url, "name" to name, "sgener" to sgener))
}
}
if (playlist.isNotEmpty()) {
processMusicPlayList(playlist)
} else {
FileLogger.w(TAG, "card_musiclist 中没有有效的音乐条目")
}
} else {
FileLogger.w(TAG, "card_musiclist.musics 不是一个数组")
}
}
}
}
@ -784,7 +809,7 @@ object AgentService : CoroutineScope {
/**
* 保存聊天消息
*/
private fun saveChatMessage(userMessage: String, assistantMessage: String) {
private fun saveChatMessage(userMessage: String, assistantMessage: String,metadata : String) {
launch(Dispatchers.IO) {
try {
// 保存用户消息
@ -799,7 +824,8 @@ object AgentService : CoroutineScope {
val assistantMessageId = chatStorageHelper.saveMessage(
sessionId = sessionId,
message = assistantMessage,
sender = "assistant"
sender = "assistant",
metadata = metadata
)
// if (assistantMessageId != -1L) {
@ -977,4 +1003,21 @@ object AgentService : CoroutineScope {
FileLogger.i(TAG, "启动音乐服务 播放音乐 $musicUrl")
MusicServiceStarter.startServiceWithCommand(context, "play", url = musicUrl)
}
/**
* 处理音乐播放列表
* 将ai返回的音乐连接 在后台启动播放
*
* @param musicUrl 图片文件路径
* @param name 歌曲名称,默认为空
* @param singer 歌手,默认为false
* @return 是否成功开始处理
*/
fun processMusicPlayList(playlist:List<Map<String,String>>){
// 在其他 Service、BroadcastReceiver 或 Application 中调用
val urls = playlist.mapNotNull { it["url"] }.distinct()
FileLogger.i(TAG, "启动音乐服务 播放音乐列表 $urls")
MusicServiceStarter.startServiceWithPlaylist(context, "playlist", urls = urls)
}
}

1
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt

@ -57,6 +57,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
config["openaiApiKey"]?.toString()?.let { intent.putExtra("openaiApiKey", it) }
config["openaiBaseUrl"]?.toString()?.let { intent.putExtra("openaiBaseUrl", it) }
config["openaiModel"]?.toString()?.let { intent.putExtra("openaiModel", it) }
config["systemPrompt"]?.toString()?.let { intent.putExtra("systemPrompt", it) }
config["mcpServer"]?.toString()?.let { intent.putExtra("mcpServer", it) }
// 启动前台服务

3
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt

@ -54,6 +54,7 @@ class BleMediaService : Service(), CoroutineScope {
private const val KEY_OPENAI_API_KEY = "openaiApiKey"
private const val KEY_OPENAI_BASE_URL = "openaiBaseUrl"
private const val KEY_OPENAI_MODEL = "openaiModel"
private const val KEY_SYSTEM_PROMPT = "systemPrompt"
private const val KEY_MCP_SERVER = "mcpServer"
// 用于跟踪服务是否正在运行
@ -213,6 +214,7 @@ class BleMediaService : Service(), CoroutineScope {
it.getStringExtra(KEY_OPENAI_API_KEY)?.let { value -> config[KEY_OPENAI_API_KEY] = value }
it.getStringExtra(KEY_OPENAI_BASE_URL)?.let { value -> config[KEY_OPENAI_BASE_URL] = value }
it.getStringExtra(KEY_OPENAI_MODEL)?.let { value -> config[KEY_OPENAI_MODEL] = value }
it.getStringExtra(KEY_SYSTEM_PROMPT)?.let { value -> config[KEY_SYSTEM_PROMPT] = value }
it.getStringExtra(KEY_MCP_SERVER)?.let { value -> config[KEY_MCP_SERVER] = value }
}
FileLogger.i(TAG, "从Intent中提取到配置参数: $config")
@ -243,6 +245,7 @@ class BleMediaService : Service(), CoroutineScope {
KEY_OPENAI_API_KEY to getConfigString(prefs, KEY_OPENAI_API_KEY, ""),
KEY_OPENAI_BASE_URL to getConfigString(prefs, KEY_OPENAI_BASE_URL, "https://api.openai.com/v1"),
KEY_OPENAI_MODEL to getConfigString(prefs, KEY_OPENAI_MODEL, "gpt-3.5-turbo"),
KEY_SYSTEM_PROMPT to getConfigString(prefs, KEY_SYSTEM_PROMPT, "gpt-3.5-turbo"),
KEY_MCP_SERVER to getConfigString(prefs, KEY_MCP_SERVER, "")
)
}

42
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt

@ -20,13 +20,18 @@ class MusicService : Service() {
private const val NOTIFICATION_ID = 1002
private val isRunning = AtomicBoolean(false)
private var currentStatus: Boolean = false
private var currentUrlInternal: String? = null
fun isServiceRunning(): Boolean = isRunning.get()
fun isPlaying(): Boolean = currentStatus
fun getCurrentUrl(): String? = currentUrlInternal
}
private var mediaPlayer: MediaPlayer? = null
private val playlist = mutableListOf<String>() // 可选:初始化为预置歌曲
private var currentIndex = 0
private var isPlaying = false
override fun onCreate() {
super.onCreate()
@ -40,7 +45,7 @@ class MusicService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val command = intent?.getStringExtra("command") ?: return START_NOT_STICKY
Log.i(TAG, "onStartCommand: $command")
Log.i(TAG, "onStartCommand: $command" )
when (command) {
"play" -> {
@ -49,6 +54,18 @@ class MusicService : Service() {
currentIndex = playlist.indexOf(url)
play(url)
}
"playlist" -> {
val urls = intent.getStringArrayListExtra("urls")?.filter { it.isNotEmpty() } ?: return START_NOT_STICKY
Log.i(TAG, "后台服务播放 playlist: $urls" )
if (urls.isNotEmpty()) {
playlist.clear()
playlist.addAll(urls)
currentIndex = 0
play(playlist[currentIndex])
} else {
Log.w(TAG, "playlist 命令收到空列表")
}
}
"pause" -> pause()
"resume" -> resume()
"stop" -> stopSelf()
@ -76,24 +93,31 @@ class MusicService : Service() {
private fun play(url: String) {
Log.i(TAG, "播放音乐: $url")
releaseMediaPlayer()
currentStatus = true;
currentUrlInternal = url;
mediaPlayer = MediaPlayer().apply {
setDataSource(url)
setAudioStreamType(AudioManager.STREAM_MUSIC)
setOnPreparedListener {
start()
this@MusicService.isPlaying = true
currentStatus = true
updateNotification("正在播放")
Log.i(TAG, "开始播放")
}
setOnCompletionListener {
Log.i(TAG, "播放完成")
this@MusicService.isPlaying = false
stopSelf()
// 播放下一首
if (currentIndex + 1 < playlist.size) {
currentIndex++
play(playlist[currentIndex]) // 播放下一首
} else {
currentStatus = false
stopSelf() // 如果没有更多歌曲了,可以停止服务
}
}
setOnErrorListener { _, what, extra ->
Log.e(TAG, "播放错误: what=$what extra=$extra")
this@MusicService.isPlaying = false
currentStatus = false
stopSelf()
true
}
@ -105,7 +129,7 @@ class MusicService : Service() {
mediaPlayer?.let {
if (it.isPlaying) {
it.pause()
this@MusicService.isPlaying = false
currentStatus= false
updateNotification("已暂停")
Log.i(TAG, "暂停播放")
}
@ -116,7 +140,7 @@ class MusicService : Service() {
mediaPlayer?.let {
if (!it.isPlaying) {
it.start()
this@MusicService.isPlaying = true
currentStatus = true
updateNotification("继续播放")
Log.i(TAG, "继续播放")
}

15
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt

@ -18,7 +18,7 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler {
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "music_service")
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.music_service")
channel.setMethodCallHandler(this)
}
@ -30,6 +30,10 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler {
val url = call.argument<String>("url")
MusicServiceStarter.startServiceWithCommand(context, "play", url)
}
"playlist" -> {
val urls = call.argument<List<String>>("urls")
MusicServiceStarter.startServiceWithPlaylist(context, "playlist", urls)
}
"pause" -> MusicServiceStarter.startServiceWithCommand(context, "pause")
"resume" -> MusicServiceStarter.startServiceWithCommand(context, "resume")
"stop" -> MusicServiceStarter.startServiceWithCommand(context, "stop")
@ -37,6 +41,15 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler {
val volume = call.argument<Double>("volume")?.toFloat()
MusicServiceStarter.startServiceWithCommand(context, "setVolume", volume = volume)
}
"getStatus" -> {
val isPlaying = MusicService.isPlaying()
val currentUrl = MusicService.getCurrentUrl()
val status = mapOf(
"isPlaying" to isPlaying,
"url" to currentUrl
)
result.success(status)
}
else -> result.notImplemented()
}
result.success(null)

11
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt

@ -7,6 +7,7 @@ import androidx.core.content.ContextCompat
object MusicServiceStarter {
private val TAG = "MusicServiceStarter"
//播放单曲
fun startServiceWithCommand(context: Context, command: String, url: String? = null, volume: Float? = null) {
Log.i(TAG, "startServiceWithCommand: $command $url")
val intent = Intent(context, MusicService::class.java).apply {
@ -16,4 +17,14 @@ object MusicServiceStarter {
}
ContextCompat.startForegroundService(context, intent)
}
//启动播放列表
fun startServiceWithPlaylist(context: Context,command: String, urls: List<String>?,volume: Float? = null) {
Log.i(TAG, "startServiceWithPlaylist: $urls")
val intent = Intent(context, MusicService::class.java).apply {
putExtra("command", command)
putStringArrayListExtra("urls", ArrayList(urls!!)) // 注意类型是 ArrayList<String>
}
ContextCompat.startForegroundService(context, intent)
}
}

7
local_plugins/music_service/lib/music_service.dart

@ -52,4 +52,11 @@ class MusicService {
'setVolume',
{'volume': volume},
); // volume: 0.0~1.0
Future<Map<String, dynamic>> getStatus() async {
final result = await _methodChannel.invokeMethod<Map>('getStatus');
return {
'isPlaying': result?['isPlaying'] ?? false,
'url': result?['url'] ?? "",
};
}
}

Loading…
Cancel
Save