Browse Source

上传音乐服务逻辑代码

weicu
liwei1dao 1 year ago
parent
commit
945d4530fb
  1. BIN
      assets/images/music.png
  2. 18
      lib/data/services/ble_manager.dart
  3. 25
      lib/modules/agent/views/message_bubble.dart
  4. 81
      lib/modules/agent/views/music_card.dart
  5. 4
      lib/modules/music/controllers/music_playlist_controller.dart
  6. 4
      lib/modules/music/views/music_playlist_view.dart
  7. 2
      lib/modules/qqmusic/views/qqmusic_playlist_view.dart
  8. 9
      lib/modules/qqmusic_test/bindings/qqmusic_binding.dart
  9. 326
      lib/modules/qqmusic_test/controllers/qqmusic_controller.dart
  10. 49
      lib/modules/qqmusic_test/models/song_model.dart
  11. 242
      lib/modules/qqmusic_test/views/qqmusic_view.dart
  12. 2
      lib/routes/app_pages.dart
  13. 23
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  14. 1
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  15. 4
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt
  16. 3
      local_plugins/chat_api/android/build.gradle.kts
  17. 8
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt
  18. 25
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt
  19. 307
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MusicSystemFunctionHandler.kt
  20. 9
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt
  21. 199
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt
  22. 1
      local_plugins/music_service/android/build.gradle
  23. 18
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt
  24. 55
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt
  25. 202
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicSingleton.kt
  26. 2
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt
  27. 2
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt
  28. 1
      pubspec.yaml

BIN
assets/images/music.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

18
lib/data/services/ble_manager.dart

@ -537,17 +537,6 @@ class BleManager extends GetxService {
}
var jsonString = _storage.read<String>("logintoken");
Logger.info('启动BLE服务,serverurl: ${serverurl}');
// final azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? 'eastasia';
// final openaiApiKey = dotenv.env['OPENAI_API_KEY'] ?? '';
// final openaiBaseUrl =
// dotenv.env['OPENAI_BASE_URL'] ?? 'https://api.openai.com/v1';
// final openaiModel = dotenv.env['OPENAI_MODEL'] ?? 'gpt-3.5-turbo';
// 从Flutter资源包中加载.mcp.json
// final mcpConfig = await rootBundle.loadString('.mcp.json');
// 验证JSON格式
// final jsonData = jsonDecode(mcpConfig);
final azureSpeechKey = AppConfig.env('AZURE_SPEECH_KEY') ?? '';
final azureSpeechRegion =
@ -561,10 +550,10 @@ class BleManager extends GetxService {
final openaiagentid =
AppConfig.env('OPENAI_AGENT_ID') ?? 'personal_assistant';
final systemPrompt = AppConfig.agentsystemPrompt(openaiagentid);
final musicServiceType = AppConfig.env('APP_MUSICSERVICE_TYPE') ?? 'AUTO';
// 从Flutter资源包中加载.mcp.json
final jsonData = AppConfig.mcpConfig();
Logger.info('启动BLE服务,musicServiceType: ${musicServiceType}');
// 根据APP语言动态配置语音识别语言
List<String> supportedLanguages = [];
try {
@ -611,7 +600,7 @@ class BleManager extends GetxService {
supportedLanguages.add('en-US');
}
Logger.info('动态配置语音识别语言: ${supportedLanguages.join(', ')}');
// Logger.info('动态配置语音识别语言: ${supportedLanguages.join(', ')}');
} catch (e) {
Logger.error('获取APP语言配置失败,使用默认语言: $e');
supportedLanguages = ['zh-CN', 'en-US'];
@ -632,6 +621,7 @@ class BleManager extends GetxService {
'serverurl': serverurl,
'usertoken': jsonString,
'supportedLanguages': supportedLanguages,
'musicServiceType': musicServiceType,
};
// 记录日志(不记录敏感信息)

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

@ -1,4 +1,5 @@
import 'package:voitrans/core/utils/logger.dart';
import 'package:voitrans/modules/agent/views/music_card.dart';
import 'package:voitrans/modules/agent/views/qqmusic_card.dart';
import '../../../data/models/song_model.dart';
@ -233,18 +234,18 @@ class MessageBubble extends GetView<AgentController> {
// return buildMusicItem(context, musicId, musicName, musicUrl,
// musicSinger, musicImage, ts);
// }
// } else if (meta!.keys.contains('music_playlist')) {
// final card_musiclist =
// meta!["music_playlist"]['card_musiclist'] as Map<String, dynamic>?;
// if (card_musiclist != null) {
// final rawList = card_musiclist['musics'] as List? ?? [];
// final playlist =
// rawList.map((e) => Map<String, dynamic>.from(e)).toList();
// //音乐播放列表
// return buildMusicContent(context, playlist);
// }
// } else
if (meta!.keys.contains('qqmusic_searchandplay_songs')) {
if (meta!.keys.contains('music_searchandplay_songs')) {
final music_search_songs =
meta!['music_searchandplay_songs'] as Map<String, dynamic>;
final data = music_search_songs['data'];
if (data != null && data is List) {
final rawList = data as List? ?? [];
final playlist =
rawList.map((e) => Map<String, dynamic>.from(e)).toList();
//音乐播放列表
return buildMusicContent(context, playlist);
}
} else if (meta!.keys.contains('qqmusic_searchandplay_songs')) {
final qqmusic_search_songs =
meta!['qqmusic_searchandplay_songs'] as Map<String, dynamic>;
final data = qqmusic_search_songs['data'];

81
lib/modules/agent/views/music_card.dart

@ -72,23 +72,23 @@ Widget buildMusicItem(BuildContext context, int id, String name, String url,
right: 2.w,
bottom: 2.w,
child: Container(
width: 12.w,
height: 12.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(2.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 1,
offset: const Offset(0, 0.5),
),
],
),
width: 16.w,
height: 16.w,
// decoration: BoxDecoration(
// color: Colors.white,
// borderRadius: BorderRadius.circular(2.r),
// boxShadow: [
// BoxShadow(
// color: Colors.black.withOpacity(0.2),
// blurRadius: 1,
// offset: const Offset(0, 0.5),
// ),
// ],
// ),
child: ClipRRect(
borderRadius: BorderRadius.circular(2.r),
child: Image.asset(
'assets/images/qqmusic.png',
'assets/images/music.png',
fit: BoxFit.contain,
),
),
@ -203,11 +203,13 @@ Widget buildMusicContent(
children: [
Row(
children: [
Icon(
Icons.music_note,
size: 16,
// ignore: deprecated_member_use
color: Theme.of(context).primaryColor.withOpacity(0.7),
Container(
width: 18.w,
height: 18.w,
child: Image.asset(
'assets/images/music.png',
fit: BoxFit.contain,
),
),
const SizedBox(width: 6),
Text(
@ -273,47 +275,6 @@ Widget buildMusicContent(
),
],
),
//),
//const SizedBox(height: 10),
// Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// Expanded(
// child: OutlinedButton.icon(
// onPressed: () {},
// icon: Icon(Icons.play_arrow, size: 16),
// label: Text('播放全部'),
// style: OutlinedButton.styleFrom(
// foregroundColor: Theme.of(context).primaryColor,
// side: BorderSide(
// color: Theme.of(context).primaryColor.withOpacity(0.5)),
// padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(16),
// ),
// textStyle: TextStyle(fontSize: 12),
// ),
// ),
// ),
// SizedBox(width: 8),
// Expanded(
// child: OutlinedButton.icon(
// onPressed: () {},
// icon: Icon(Icons.shuffle, size: 16),
// label: Text('随机播放'),
// style: OutlinedButton.styleFrom(
// foregroundColor: Colors.grey[700],
// side: BorderSide(color: Colors.grey[400]!),
// padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(16),
// ),
// textStyle: TextStyle(fontSize: 12),
// ),
// ),
// ),
// ],
// ),
],
);
}

4
lib/modules/music/controllers/music_playlist_controller.dart

@ -3,7 +3,7 @@ import 'package:get/get.dart';
class MusicPlaylistController extends GetxController {
// 歌单数据
final playlist = <Map<String, dynamic>>[].obs;
@override
void onInit() {
super.onInit();
@ -11,6 +11,4 @@ class MusicPlaylistController extends GetxController {
final List<Map<String, dynamic>> playlistData = Get.arguments ?? [];
playlist.assignAll(playlistData);
}
}

4
lib/modules/music/views/music_playlist_view.dart

@ -314,7 +314,7 @@ class MusicPlaylistView extends StatelessWidget {
child: ClipRRect(
borderRadius: BorderRadius.circular(2.r),
child: Image.asset(
'assets/images/qqmusic.png',
'assets/images/music.png',
fit: BoxFit.contain,
),
),
@ -463,7 +463,7 @@ class MusicPlaylistView extends StatelessWidget {
child: ClipRRect(
borderRadius: BorderRadius.circular(4.r),
child: Image.asset(
'assets/images/qqmusic.png',
'assets/images/music.png',
fit: BoxFit.contain,
),
),

2
lib/modules/qqmusic/views/qqmusic_playlist_view.dart

@ -113,7 +113,7 @@ class QqmusicPlaylistView extends GetView<QQMusicPlaylistController> {
_buildProgressBar(isDarkMode),
SizedBox(height: 15.h),
// 播放控制按钮
_buildPlayControls(isDarkMode),
// _buildPlayControls(isDarkMode),
],
),
);

9
lib/modules/qqmusic_test/bindings/qqmusic_binding.dart

@ -1,9 +0,0 @@
import 'package:get/get.dart';
import '../controllers/qqmusic_controller.dart';
class QQMusicBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<QQMusicController>(() => QQMusicController());
}
}

326
lib/modules/qqmusic_test/controllers/qqmusic_controller.dart

@ -1,326 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:qq_music/qq_music.dart';
import 'package:voitrans/data/services/qq_music.dart';
import '../models/song_model.dart';
class QQMusicController extends GetxController {
final QqMusicService _qqMusic = Get.find<QqMusicService>();
// 登录状态
final isLoggedIn = false.obs;
final userInfo = Rxn<QQMusicLoginResult>();
// 搜索相关
final searchResults = <SongModel>[].obs;
final isSearching = false.obs;
final searchKeyword = ''.obs;
// 播放相关
final currentSong = Rxn<SongModel>();
final isPlaying = false.obs;
final currentPosition = 0.obs;
final totalDuration = 0.obs;
final playlist = <SongModel>[].obs;
final currentIndex = 0.obs;
@override
void onInit() {
super.onInit();
_checkLoginStatus();
_startPlaybackStateMonitor();
}
/// 检查登录状态
void _checkLoginStatus() {
// 这里可以检查本地存储的登录信息
// 暂时设为未登录状态
isLoggedIn.value = false;
}
/// QQ音乐登录
Future<void> login() async {
try {
print('开始QQ音乐登录...');
Get.dialog(
const Center(
child: CircularProgressIndicator(),
),
barrierDismissible: false,
);
// 添加超时处理
final result = await _qqMusic.login().timeout(
const Duration(seconds: 30),
onTimeout: () {
print('QQ音乐登录超时');
return {'success': false, 'error': '登录超时,请重试'};
},
);
print('QQ音乐登录结果: $result');
Get.back(); // 关闭加载对话框
if (result['success'] == true) {
userInfo.value = QQMusicLoginResult.fromMap(result);
isLoggedIn.value = true;
print('QQ音乐登录成功: ${result['openId']}');
Get.snackbar('登录成功', '欢迎使用QQ音乐');
} else {
print('QQ音乐登录失败: ${result['error']}');
Get.snackbar('登录失败', result['error'] ?? '未知错误');
}
} catch (e) {
print('QQ音乐登录异常: $e');
Get.back();
Get.snackbar('登录失败', e.toString());
}
}
/// 退出登录
void logout() {
userInfo.value = null;
isLoggedIn.value = false;
currentSong.value = null;
isPlaying.value = false;
playlist.clear();
Get.snackbar('已退出', '已退出QQ音乐登录');
}
/// 搜索歌曲
Future<void> searchSongs(String keyword) async {
if (keyword.trim().isEmpty) return;
print('开始搜索歌曲: $keyword');
searchKeyword.value = keyword;
isSearching.value = true;
try {
// 检查登录状态
if (!isLoggedIn.value) {
print('用户未登录,无法搜索');
Get.snackbar('搜索失败', '请先登录QQ音乐');
return;
}
print('调用搜索接口...');
final result = await _qqMusic.searchSongs(keyword: keyword).timeout(
const Duration(seconds: 15),
onTimeout: () {
print('搜索超时');
return {'success': false, 'error': '搜索超时,请重试'};
},
);
print('搜索结果: $result');
if (result['success'] == true && result['songs'] != null) {
final List<dynamic> songsData = result['songs'];
print('找到 ${songsData.length} 首歌曲');
final songs = songsData.map((songData) {
return SongModel(
mid: songData['mid'] ?? '',
name: songData['name'] ?? '',
artist: songData['singer'] ?? '',
album: songData['album'] ?? '',
albumPic: songData['pic'],
duration: songData['duration'] ?? 0,
);
}).toList();
searchResults.assignAll(songs);
print('搜索成功,更新UI');
} else {
print('搜索失败: ${result['error']}');
Get.snackbar('搜索失败', result['error'] ?? '搜索结果为空');
searchResults.clear();
}
} catch (e) {
print('搜索异常: $e');
Get.snackbar('搜索失败', e.toString());
searchResults.clear();
} finally {
print('搜索完成,重置状态');
isSearching.value = false;
}
}
/// 播放歌曲
Future<void> playSong(SongModel song, {List<SongModel>? songList}) async {
try {
currentSong.value = song;
if (songList != null) {
playlist.assignAll(songList);
currentIndex.value = songList.indexOf(song);
} else {
playlist.assignAll([song]);
currentIndex.value = 0;
}
// 获取歌曲MID列表
final songMids = playlist.map((s) => s.mid).toList();
final playIndex = currentIndex.value;
// 调用QQ音乐播放接口
final result = await _qqMusic.playSongs(
songMids: songMids,
index: playIndex,
);
if (result['success'] == true) {
isPlaying.value = true;
totalDuration.value = song.duration;
currentPosition.value = 0;
Get.snackbar('开始播放', song.name);
} else {
Get.snackbar('播放失败', result['error'] ?? '未知错误');
}
} catch (e) {
Get.snackbar('播放失败', e.toString());
}
}
/// 暂停/继续播放
Future<void> togglePlayPause() async {
try {
if (isPlaying.value) {
final result = await _qqMusic.pauseMusic();
if (result['success'] == true) {
isPlaying.value = false;
}
} else {
final result = await _qqMusic.resumeMusic();
if (result['success'] == true) {
isPlaying.value = true;
}
}
} catch (e) {
Get.snackbar('操作失败', e.toString());
}
}
/// 上一首
Future<void> previousSong() async {
try {
final result = await _qqMusic.skipToPrevious();
if (result['success'] == true) {
if (playlist.isNotEmpty) {
int newIndex = currentIndex.value - 1;
if (newIndex < 0) newIndex = playlist.length - 1;
currentIndex.value = newIndex;
currentSong.value = playlist[newIndex];
}
await _updateCurrentSong();
}
} catch (e) {
Get.snackbar('切换失败', e.toString());
}
}
/// 下一首
Future<void> nextSong() async {
try {
final result = await _qqMusic.skipToNext();
if (result['success'] == true) {
if (playlist.isNotEmpty) {
int newIndex = currentIndex.value + 1;
if (newIndex >= playlist.length) newIndex = 0;
currentIndex.value = newIndex;
currentSong.value = playlist[newIndex];
}
await _updateCurrentSong();
}
} catch (e) {
Get.snackbar('切换失败', e.toString());
}
}
/// 停止播放
Future<void> stopMusic() async {
try {
final result = await _qqMusic.stopMusic();
if (result['success'] == true) {
isPlaying.value = false;
currentPosition.value = 0;
}
} catch (e) {
Get.snackbar('停止失败', e.toString());
}
}
/// 更新当前播放歌曲信息
Future<void> _updateCurrentSong() async {
try {
final result = await _qqMusic.getCurrentSong();
if (result['success'] == true && result['song'] != null) {
final songData = result['song'];
final song = SongModel(
mid: songData['mid'] ?? '',
name: songData['name'] ?? '',
artist: songData['singer'] ?? '',
album: songData['album'] ?? '',
albumPic: songData['pic'],
duration: songData['duration'] ?? 0,
);
currentSong.value = song;
}
} catch (e) {
print('更新当前歌曲信息失败: $e');
}
}
/// 启动播放状态监控
void _startPlaybackStateMonitor() {
// 每秒检查一次播放状态和时间
Stream.periodic(const Duration(seconds: 1)).listen((_) async {
if (isLoggedIn.value) {
await _updatePlaybackState();
await _updatePlayTime();
}
});
}
/// 更新播放状态
Future<void> _updatePlaybackState() async {
try {
final result = await _qqMusic.getPlaybackState();
if (result['success'] == true && result['state'] != null) {
final state = result['state'];
// QQ音乐播放状态:0-停止,1-播放,2-暂停
isPlaying.value = state == 1;
}
} catch (e) {
// 静默处理错误,避免频繁弹窗
}
}
/// 更新播放时间
Future<void> _updatePlayTime() async {
try {
final result = await _qqMusic.getPlayTime();
if (result['success'] == true) {
currentPosition.value = result['currentTime'] ?? 0;
totalDuration.value = result['totalTime'] ?? 0;
}
} catch (e) {
// 静默处理错误,避免频繁弹窗
}
}
/// 跳转到指定位置(暂时保留,QQ音乐可能不支持)
void seekTo(int position) {
currentPosition.value = position;
// QQ音乐SDK可能不支持跳转功能
Get.snackbar('提示', '当前版本不支持跳转功能');
}
@override
void onClose() {
// 清理资源
super.onClose();
}
}

49
lib/modules/qqmusic_test/models/song_model.dart

@ -1,49 +0,0 @@
class SongModel {
final String mid;
final String name;
final String artist;
final String album;
final String? albumPic;
final int duration;
final bool isFavorite;
SongModel({
required this.mid,
required this.name,
required this.artist,
required this.album,
this.albumPic,
required this.duration,
this.isFavorite = false,
});
factory SongModel.fromJson(Map<String, dynamic> json) {
return SongModel(
mid: json['mid'] ?? '',
name: json['name'] ?? '',
artist: json['artist'] ?? '',
album: json['album'] ?? '',
albumPic: json['albumPic'],
duration: json['duration'] ?? 0,
isFavorite: json['isFavorite'] ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'mid': mid,
'name': name,
'artist': artist,
'album': album,
'albumPic': albumPic,
'duration': duration,
'isFavorite': isFavorite,
};
}
String get durationText {
final minutes = duration ~/ 60;
final seconds = duration % 60;
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
}

242
lib/modules/qqmusic_test/views/qqmusic_view.dart

@ -1,242 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/qqmusic_controller.dart';
import '../models/song_model.dart';
class QQMusicView extends GetView<QQMusicController> {
const QQMusicView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('QQ音乐'),
actions: [
Obx(() => controller.isLoggedIn.value
? IconButton(
icon: const Icon(Icons.logout),
onPressed: controller.logout,
)
: IconButton(
icon: const Icon(Icons.login),
onPressed: controller.login,
)),
],
),
body: Obx(() {
if (!controller.isLoggedIn.value) {
return _buildLoginView();
}
return _buildMainView();
}),
bottomNavigationBar: Obx(() {
if (controller.currentSong.value != null) {
return _buildMiniPlayer();
}
return const SizedBox.shrink();
}),
);
}
Widget _buildLoginView() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.music_note,
size: 100,
color: Colors.blue,
),
const SizedBox(height: 20),
const Text(
'请先登录QQ音乐',
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: controller.login,
child: const Text('登录QQ音乐'),
),
],
),
);
}
Widget _buildMainView() {
return Column(
children: [
_buildSearchBar(),
Expanded(
child: Obx(() {
if (controller.isSearching.value) {
return const Center(child: CircularProgressIndicator());
}
if (controller.searchResults.isEmpty) {
return const Center(
child: Text('搜索歌曲开始您的音乐之旅'),
);
}
return _buildSearchResults();
}),
),
],
);
}
Widget _buildSearchBar() {
return Container(
padding: const EdgeInsets.all(16),
child: TextField(
decoration: const InputDecoration(
hintText: '搜索歌曲、歌手、专辑',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
),
onSubmitted: controller.searchSongs,
),
);
}
Widget _buildSearchResults() {
return ListView.builder(
itemCount: controller.searchResults.length,
itemBuilder: (context, index) {
final song = controller.searchResults[index];
return _buildSongItem(song);
},
);
}
Widget _buildSongItem(SongModel song) {
return ListTile(
leading: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(8),
),
child: song.albumPic != null
? ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
song.albumPic!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return const Icon(Icons.music_note);
},
),
)
: const Icon(Icons.music_note),
),
title: Text(
song.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
'${song.artist} • ${song.album}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(song.durationText),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.play_arrow),
onPressed: () => controller.playSong(
song,
songList: controller.searchResults,
),
),
],
),
);
}
Widget _buildMiniPlayer() {
return Obx(() {
final song = controller.currentSong.value!;
return Container(
height: 80,
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, -2),
),
],
),
child: Column(
children: [
// 进度条
Obx(() => LinearProgressIndicator(
value: controller.totalDuration.value > 0
? controller.currentPosition.value /
controller.totalDuration.value
: 0,
backgroundColor: Colors.grey[300],
valueColor: const AlwaysStoppedAnimation<Color>(Colors.blue),
)),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
// 歌曲信息
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
song.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(
song.artist,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: Colors.grey[600]),
),
],
),
),
// 控制按钮
Row(
children: [
IconButton(
icon: const Icon(Icons.skip_previous),
onPressed: controller.previousSong,
),
Obx(() => IconButton(
icon: Icon(
controller.isPlaying.value
? Icons.pause
: Icons.play_arrow,
),
onPressed: controller.togglePlayPause,
)),
IconButton(
icon: const Icon(Icons.skip_next),
onPressed: controller.nextSong,
),
],
),
],
),
),
),
],
),
);
});
}
}

2
lib/routes/app_pages.dart

@ -2,8 +2,6 @@ import 'package:get/get.dart';
import 'package:qq_music/qq_music.dart';
import 'package:voitrans/modules/qqmusic/bindings/qqmusic_playlist_binding.dart';
import 'package:voitrans/modules/qqmusic/views/qqmusic_playlist_view.dart';
import 'package:voitrans/modules/qqmusic_test/bindings/qqmusic_binding.dart';
import 'package:voitrans/modules/qqmusic_test/views/qqmusic_view.dart';
import '/modules/ble/bindings/ble_binding.dart';
import '../modules/ble/views/ble_view.dart';

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

@ -11,7 +11,7 @@ import com.yunqiinnovation.azure_speech.AzureTtsHelper
import com.yunqiinnovation.chat_api.ChatApiService
import com.yunqiinnovation.chat_api.StreamCallback
import com.yunqiinnovation.chat_storage.ChatStorageHelper
import com.yunqiinnovation.music_service.MusicServiceStarter
import com.yunqiinnovation.music_service.MusicSingleton
import com.yunqiinnovation.amap_service.NavigationServiceHelper
import com.yunqiinnovation.user_portrait.UserPortrait
import com.yunqiinnovation.location_service.LocationService
@ -24,13 +24,10 @@ import com.yunqiinnovation.ble_service.BleService
import android.media.MediaPlayer
import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import com.deep_voice.speech.tts.TtsEvent
import com.deep_voice.speech.tts.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType
import com.deep_voice.bytedance_speech.BytedanceTTS
import java.util.UUID
import com.yunqiinnovation.agent_service.Utils
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
@ -187,6 +184,8 @@ object AgentService : CoroutineScope {
if (isInitialized) return true
try {
serverurl = config["serverurl"]?.toString() ?: "";
usertoken = config["usertoken"]?.toString() ?: "";
// 初始化上下文和服务
this.context = appContext.applicationContext
this.chatApiService = ChatApiService(context)
@ -196,6 +195,7 @@ object AgentService : CoroutineScope {
UserPortrait.initialize(appContext);
//初始化定位服务
LocationService.initialize(appContext)
MusicSingleton.getInstance().initialize(appContext,serverurl,usertoken)
QQMusicSingleton.getInstance().initialize(appContext)
// 设置系统提示词
val customSystemPrompt = config["systemPrompt"] as? String
@ -243,10 +243,10 @@ object AgentService : CoroutineScope {
config["openaiBaseUrl"]?.toString() ?: "",
config["openaiModel"]?.toString() ?: "",
config["openaivisionModel"]?.toString() ?: "",
config["mcpServer"]?.toString() ?: ""
config["mcpServer"]?.toString() ?: "",
config["musicServiceType"]?.toString() ?: ""
)
serverurl = config["serverurl"]?.toString() ?: "";
usertoken = config["usertoken"]?.toString() ?: "";
Log.e(TAG, "liwei -----------读取服务地址和用户token: $serverurl $usertoken")
// 加载最近的聊天记录
@ -1892,9 +1892,8 @@ object AgentService : CoroutineScope {
"ts" to ts
)
)
} else if (meta.has("card_musiclist")) { //音乐列表
val cardMusiclist = meta.getJSONObject("card_musiclist")
val musics = cardMusiclist.optJSONArray("musics")
} else if (toolname == "music_searchandplay_songs" && meta.has("data")) { //qq音乐
val musics = meta.optJSONArray("data")
if (musics != null) {
val playlist = mutableListOf<Map<String, Any>>()
for (i in 0 until musics.length()) {
@ -1985,7 +1984,7 @@ object AgentService : CoroutineScope {
fun processMusicPlay(song: Map<String, Any>) {
// 在其他 Service、BroadcastReceiver 或 Application 中调用
Log.i(TAG, "播放音乐: ${song}")
MusicServiceStarter.startServiceWithCommand(context, command = "play", song = song)
MusicSingleton.startServiceWithCommand(context, command = "play", song = song)
}
/**
@ -2000,7 +1999,7 @@ object AgentService : CoroutineScope {
fun processMusicPlayList(songs: List<Map<String, Any>>) {
// 如果 playlist 中有有效的歌曲,开始播放
if (songs.isNotEmpty()) {
MusicServiceStarter.startServiceWithPlaylist(
MusicSingleton.startServiceWithPlaylist(
context,
command = "playlist",
songs = songs

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

@ -73,6 +73,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
config["volcanoToken"]?.toString()?.let { intent.putExtra("volcanoToken", it) }
config["serverurl"]?.toString()?.let { intent.putExtra("serverurl", it) }
config["usertoken"]?.toString()?.let { intent.putExtra("usertoken", it) }
config["musicServiceType"]?.toString()?.let { intent.putExtra("musicServiceType", it) }
// 添加 supportedLanguages 参数处理
config["supportedLanguages"]?.let { languages ->

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

@ -52,6 +52,8 @@ class BleMediaService : Service(), CoroutineScope {
private const val KEY_SERVER_URL = "serverurl"
private const val KEY_USER_TOKEN = "usertoken"
private const val KEY_SUPPORTED_LANGUAGES = "supportedLanguages"
private const val KEY_MUSICSERVICE_TYPE = "musicServiceType"
// 用于跟踪服务是否正在运行
private val isRunning = AtomicBoolean(false)
@ -179,6 +181,7 @@ class BleMediaService : Service(), CoroutineScope {
it.getStringExtra(KEY_VOLCANO_TOKEN)?.let { value -> config[KEY_VOLCANO_TOKEN] = value }
it.getStringExtra(KEY_SERVER_URL)?.let { value -> config[KEY_SERVER_URL] = value }
it.getStringExtra(KEY_USER_TOKEN)?.let { value -> config[KEY_USER_TOKEN] = value }
it.getStringExtra(KEY_MUSICSERVICE_TYPE)?.let { value -> config[KEY_MUSICSERVICE_TYPE] = value }
// 提取 supportedLanguages 参数
it.getStringArrayListExtra(KEY_SUPPORTED_LANGUAGES)?.let { languages ->
@ -238,6 +241,7 @@ class BleMediaService : Service(), CoroutineScope {
config[KEY_VOLCANO_TOKEN] = getConfigString(prefs, KEY_VOLCANO_TOKEN, "")
config[KEY_SERVER_URL] = getConfigString(prefs, KEY_SERVER_URL, "")
config[KEY_USER_TOKEN] = getConfigString(prefs, KEY_USER_TOKEN, "")
config[KEY_MUSICSERVICE_TYPE] = getConfigString(prefs, KEY_MUSICSERVICE_TYPE, "")
// 添加 supportedLanguages 参数处理
val supportedLanguagesStr = getConfigString(prefs, KEY_SUPPORTED_LANGUAGES, "")

3
local_plugins/chat_api/android/build.gradle.kts

@ -60,5 +60,6 @@ dependencies {
add("compileOnly", project(":location_service"))
add("compileOnly", project(":qq_music"))
add("compileOnly", project(":music_service"))
add("compileOnly", project(":qq_music"))
}

8
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt

@ -198,7 +198,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
/**
* 初始化ChatAPI服务
*/
fun initialize(apiKey: String, baseUrl: String, model: String, visionModel: String, mcpServer: String): Boolean {
fun initialize(apiKey: String, baseUrl: String, model: String, visionModel: String, mcpServer: String,musicType: String = "AUTO"): Boolean {
this.apiKey = apiKey
if (baseUrl.isNotEmpty()) {
this.baseUrl = baseUrl
@ -236,7 +236,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
mcpConfigJson = mcpServer
// 异步初始化MCP客户端
launch {
initializeMcpClient(mcpServer)
initializeMcpClient(mcpServer,musicType)
// initializeMcpClient("{}")
}
@ -784,9 +784,9 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
/**
* 初始化MCP客户端
*/
fun initializeMcpClient(serverUrl: String): Boolean {
fun initializeMcpClient(serverUrl: String,musicType: String = "AUTO"): Boolean {
if (_mcpClient == null) {
_mcpClient = MCPClient(context)
_mcpClient = MCPClient(context,musicType)
}
// 直接使用类的CoroutineScope启动协程

25
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt

@ -31,7 +31,7 @@ interface FunctionHandler {
* MCP客户端
* 与 iOS 版本 MCPClient 功能对等
*/
class MCPClient(private val context: Context? = null) : AutoCloseable {
class MCPClient(private val context: Context? = null,musicType: String = "AUTO") : AutoCloseable {
companion object {
private const val TAG = "MCPClient"
@ -54,7 +54,17 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
init {
initializeSystemFunctions()
initializeQQMusiceFunctions()
when (musicType) {
"WY" -> initializeMusiceFunctions() //网易
"QQ" -> initializeQQMusiceFunctions() //QQ
else -> {
if (QQMusicSystemFunctionHandler.CheckQQMusicInstall()) { //检查是否安装qq音乐
initializeQQMusiceFunctions();
}else{
initializeMusiceFunctions()
}
}
}
}
/**
@ -69,6 +79,17 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
}
}
/**
* 初始化音乐函数
*/
private fun initializeMusiceFunctions() {
try {
val handler = MusicSystemFunctionHandler(context)
handler.registerAllFunctions(this)
} catch (e: Exception) {
Log.w(TAG, "Failed to initialize system functions", e)
}
}
/**
* 初始化音乐函数
*/

307
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MusicSystemFunctionHandler.kt

@ -0,0 +1,307 @@
package com.yunqiinnovation.chat_api
import android.content.Context
import android.content.Intent
import android.util.Log
import com.yunqiinnovation.music_service.MusicSingleton
import org.json.JSONObject
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
/**
* QQ音乐系统功能处理器
* 负责注册QQ音乐相关的MCP函数
*/
class MusicSystemFunctionHandler(private val context: Context? = null) {
companion object {
const val TAG = "MusicSystem"
}
/**
* 注册所有QQ音乐相关函数到MCPClient
*/
fun registerAllFunctions(client: MCPClient) {
try {
// Only register QQ Music search function
client.registerLocalFunction(
"music_searchandplay_songs",
"在音乐播放服务中搜索并播放歌曲,支持歌曲、专辑、歌手等搜索类型",
mapOf(
"type" to "object",
"properties" to mapOf(
"keyword" to mapOf(
"type" to "string",
"description" to "搜索关键词,歌曲名、专辑名或歌手名"
),
"searchType" to mapOf(
"type" to "integer",
"description" to "搜索类型:0-歌名搜索,1-歌手搜索,2-歌曲类型搜索",
"default" to 0
)
),
"required" to listOf("keyword")
),
MusicSearchHandler(context)
)
// 注册媒体播放功能
client.registerLocalFunction(
name = "media_play",
description = "播放媒体",
parameters = mapOf(
"type" to "object",
"properties" to emptyMap<String, Any>(),
"required" to emptyList<String>()
),
handler = MediaPlayHandler(context)
)
// 注册媒体暂停功能
client.registerLocalFunction(
name = "media_pause",
description = "暂停媒体播放",
parameters = mapOf(
"type" to "object",
"properties" to emptyMap<String, Any>(),
"required" to emptyList<String>()
),
handler = MediaPauseHandler(context)
)
// 注册媒体暂停功能
client.registerLocalFunction(
name = "media_close",
description = "关闭媒体播放",
parameters = mapOf(
"type" to "object",
"properties" to emptyMap<String, Any>(),
"required" to emptyList<String>()
),
handler = MediaCloseHandler(context)
)
// 注册媒体上一首功能
client.registerLocalFunction(
name = "media_previous",
description = "播放上一首",
parameters = mapOf(
"type" to "object",
"properties" to emptyMap<String, Any>(),
"required" to emptyList<String>()
),
handler = MediaPreviousHandler(context)
)
// 注册媒体下一首功能
client.registerLocalFunction(
name = "media_next",
description = "播放下一首",
parameters = mapOf(
"type" to "object",
"properties" to emptyMap<String, Any>(),
"required" to emptyList<String>()
),
handler = MediaNextHandler(context)
)
Log.d(TAG, "音乐所有功能注册完成")
} catch (e: Exception) {
Log.e(TAG, "注册音乐功能失败: ${e.message}", e)
}
}
}
private class MusicSearchHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
return try {
val keyword = arguments["keyword"] as? String
?: return "{\"success\": false, \"message\": \"缺少搜索关键词\"}"
val searchType = arguments["searchType"] as? Int ?: 0 // 默认搜索歌曲
val musicSingleton = MusicSingleton.getInstance()
// 使用 suspendCoroutine 将回调转换为协程
suspendCoroutine { continuation ->
musicSingleton.searchSongs(keyword) { searchResult ->
val result = if (searchResult.isSuccess) {
val songs = searchResult.getOrNull() ?: emptyList()
// 根据搜索类型处理结果
val processedSongs = when (searchType) {
0 -> { // 歌曲搜索,返回所有结果
songs
}
else -> { // 专辑、歌手等其他类型,返回所有结果
songs
}
}
val songsJson = processedSongs.joinToString(",") { song ->
val songMap = song as Map<String, Any?>
// 添加调试日志
Log.d("QQMusicSearchHandler", "原始数据: $songMap")
// 修正字段名映射
val id = songMap["id"] ?: 0
val name = songMap["name"] ?: "unknown"
val singer = songMap["singer"] ?: "unknown"
val url = songMap["url"] ?: ""
val image = songMap["image"] ?: ""
val ts = songMap["ts"] ?: 0
"{\"id\": $id, \"name\": \"$name\", \"singer\": \"$singer\", \"url\": \"$url\", \"ts\":$ts,\"image\": \"$image\"}"
}
"{\"success\": true, \"broadcast\": false, \"message\": \"搜索成功,正在播放\", \"data\": [$songsJson]}"
} else {
val error = searchResult.exceptionOrNull()
"{\"success\": false, \"message\": \"搜索失败: ${error?.message}\"}"
}
// 恢复协程执行
continuation.resume(result)
}
}
} catch (e: Exception) {
Log.e("QQMusicSearchHandler", "QQ音乐搜索失败", e)
"{\"success\": false, \"message\": \"搜索异常: ${e.message}\"}"
}
}
}
/**
* 媒体播放处理器
*/
private class MediaPlayHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
}
// 发送媒体播放广播
val intent = Intent("com.yunqiinnovation.music_service.MEDIA_PLAY").apply {
setPackage(context.packageName)
}
context.sendBroadcast(intent)
return JSONObject().apply {
put("result", "已开始播放媒体")
put("notreply", true)
put("broadcast", false)
}.toString()
}
}
/**
* 媒体暂停处理器
*/
private class MediaPauseHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
}
Log.d("MediaPauseHandler", "📤 正在发送广播 context = $context")
// 发送媒体暂停广播
val intent = Intent("com.yunqiinnovation.music_service.MEDIA_PAUSE").apply {
setPackage(context.packageName)
}
context.sendBroadcast(intent)
return JSONObject().apply {
put("result", "已暂停媒体播放")
put("notreply", true)
put("broadcast", false)
}.toString()
}
}
/**
* 媒体关闭处理器
*/
private class MediaCloseHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
}
Log.d("MediaCloseHandler", "📤 正在发送广播 context = $context")
// 发送媒体关闭广播
val intent = Intent("com.yunqiinnovation.music_service.MEDIA_CLOSE").apply {
setPackage(context.packageName)
}
context.sendBroadcast(intent)
return JSONObject().apply {
put("result", "已关闭媒体播放")
put("notreply", true)
put("broadcast", false)
}.toString()
}
}
/**
* 媒体上一首处理器
*/
private class MediaPreviousHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
}
// 发送切换上一首广播
val intent = Intent("com.yunqiinnovation.music_service.MEDIA_PREVIOUS").apply {
setPackage(context.packageName)
}
context.sendBroadcast(intent)
return JSONObject().apply {
put("result", "已切换到上一首")
put("notreply", true)
put("broadcast", false)
}.toString()
}
}
/**
* 媒体下一首处理器
*/
private class MediaNextHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
}
// 发送切换下一首广播
val intent = Intent("com.yunqiinnovation.music_service.MEDIA_NEXT").apply {
setPackage(context.packageName)
}
context.sendBroadcast(intent)
return JSONObject().apply {
put("result", "已切换到下一首")
put("notreply", true)
put("broadcast", false)
}.toString()
}
}

9
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt

@ -13,8 +13,15 @@ import kotlin.coroutines.resume
class QQMusicSystemFunctionHandler(private val context: Context? = null) {
companion object {
const val TAG = "QQMusicSystemFunctionHandler"
fun CheckQQMusicInstall(): Boolean {
val qqMusicSingleton = QQMusicSingleton.getInstance()
return qqMusicSingleton.isQQMusicInstalled()
}
}
/**
* 注册所有QQ音乐相关函数到MCPClient
*/

199
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/SystemFunctionHandler.kt

@ -151,65 +151,7 @@ class SystemFunctionHandler(private val context: Context? = null) {
),
handler = GetCurrentLocationHandler(context)
)
// // 注册媒体播放功能
// client.registerLocalFunction(
// name = "media_play",
// description = "播放媒体",
// parameters = mapOf(
// "type" to "object",
// "properties" to emptyMap<String, Any>(),
// "required" to emptyList<String>()
// ),
// handler = MediaPlayHandler(context)
// )
// // 注册媒体暂停功能
// client.registerLocalFunction(
// name = "media_pause",
// description = "暂停媒体播放",
// parameters = mapOf(
// "type" to "object",
// "properties" to emptyMap<String, Any>(),
// "required" to emptyList<String>()
// ),
// handler = MediaPauseHandler(context)
// )
// // 注册媒体暂停功能
// client.registerLocalFunction(
// name = "media_close",
// description = "关闭媒体播放",
// parameters = mapOf(
// "type" to "object",
// "properties" to emptyMap<String, Any>(),
// "required" to emptyList<String>()
// ),
// handler = MediaCloseHandler(context)
// )
// // 注册媒体上一首功能
// client.registerLocalFunction(
// name = "media_previous",
// description = "播放上一首",
// parameters = mapOf(
// "type" to "object",
// "properties" to emptyMap<String, Any>(),
// "required" to emptyList<String>()
// ),
// handler = MediaPreviousHandler(context)
// )
// // 注册媒体下一首功能
// client.registerLocalFunction(
// name = "media_next",
// description = "播放下一首",
// parameters = mapOf(
// "type" to "object",
// "properties" to emptyMap<String, Any>(),
// "required" to emptyList<String>()
// ),
// handler = MediaNextHandler(context)
// )
// 注册打开录音机功能
client.registerLocalFunction(
name = "open_recorder",
@ -1189,145 +1131,6 @@ private class GetCurrentLocationHandler(private val context: Context?) : Functio
}
}
/**
* 媒体播放处理器
*/
private class MediaPlayHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
}
// 发送媒体播放广播
val intent = Intent("com.yunqiinnovation.music_service.MEDIA_PLAY").apply {
setPackage(context.packageName)
}
context.sendBroadcast(intent)
return JSONObject().apply {
put("result", "已开始播放媒体")
put("notreply", true)
put("broadcast", false)
}.toString()
}
}
/**
* 媒体暂停处理器
*/
private class MediaPauseHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
}
Log.d("MediaPauseHandler", "📤 正在发送广播 context = $context")
// 发送媒体暂停广播
val intent = Intent("com.yunqiinnovation.music_service.MEDIA_PAUSE").apply {
setPackage(context.packageName)
}
context.sendBroadcast(intent)
return JSONObject().apply {
put("result", "已暂停媒体播放")
put("notreply", true)
put("broadcast", false)
}.toString()
}
}
/**
* 媒体关闭处理器
*/
private class MediaCloseHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
}
Log.d("MediaCloseHandler", "📤 正在发送广播 context = $context")
// 发送媒体关闭广播
val intent = Intent("com.yunqiinnovation.music_service.MEDIA_CLOSE").apply {
setPackage(context.packageName)
}
context.sendBroadcast(intent)
return JSONObject().apply {
put("result", "已关闭媒体播放")
put("notreply", true)
put("broadcast", false)
}.toString()
}
}
/**
* 媒体上一首处理器
*/
private class MediaPreviousHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
}
// 发送切换上一首广播
val intent = Intent("com.yunqiinnovation.music_service.MEDIA_PREVIOUS").apply {
setPackage(context.packageName)
}
context.sendBroadcast(intent)
return JSONObject().apply {
put("result", "已切换到上一首")
put("notreply", true)
put("broadcast", false)
}.toString()
}
}
/**
* 媒体下一首处理器
*/
private class MediaNextHandler(private val context: Context?) : FunctionHandler {
override suspend fun handle(arguments: Map<String, Any>): String {
if (context == null) {
return JSONObject().apply {
put("error", JSONObject().apply {
put("code", "context_error")
put("message", "上下文未初始化")
})
}.toString()
}
// 发送切换下一首广播
val intent = Intent("com.yunqiinnovation.music_service.MEDIA_NEXT").apply {
setPackage(context.packageName)
}
context.sendBroadcast(intent)
return JSONObject().apply {
put("result", "已切换到下一首")
put("notreply", true)
put("broadcast", false)
}.toString()
}
}
/**
* 打开录音机处理器

1
local_plugins/music_service/android/build.gradle

@ -67,4 +67,5 @@ android {
dependencies {
implementation("androidx.media:media:1.6.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
}

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

@ -1,10 +1,6 @@
package com.yunqiinnovation.music_service
import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
import android.util.Log
import com.yunqiinnovation.music_service.MusicServiceStarter
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
@ -29,7 +25,7 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler {
"play" -> {
val song = call.argument<Map<String, Any>>("song")
if (song != null) {
MusicServiceStarter.startServiceWithCommand(context, "play", song)
MusicSingleton.startServiceWithCommand(context, "play", song)
result.success(null)
} else {
result.error("INVALID_ARGUMENT", "Missing 'song' map", null)
@ -38,27 +34,27 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler {
"playlist" -> {
val songs = call.argument<List<Map<String, Any>>>("songs")
if (songs != null && songs.isNotEmpty()) {
MusicServiceStarter.startServiceWithPlaylist(context, "playlist", songs)
MusicSingleton.startServiceWithPlaylist(context, "playlist", songs)
result.success(null)
} else {
result.error("INVALID_ARGUMENT", "Missing or empty 'songs' list", null)
}
}
"pause" -> {
MusicServiceStarter.startServiceWithCommand(context, "pause")
MusicSingleton.startServiceWithCommand(context, "pause")
result.success(null)
}
"resume" -> {
MusicServiceStarter.startServiceWithCommand(context, "resume")
MusicSingleton.startServiceWithCommand(context, "resume")
result.success(null)
}
"stop" -> {
MusicServiceStarter.startServiceWithCommand(context, "stop")
MusicSingleton.startServiceWithCommand(context, "stop")
result.success(null)
}
"setVolume" -> {
val volume = call.argument<Double>("volume")?.toFloat()
MusicServiceStarter.startServiceWithCommand(context, "setVolume", volume = volume)
MusicSingleton.startServiceWithCommand(context, "setVolume", volume = volume)
result.success(null)
}
"getStatus" -> {
@ -77,7 +73,7 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler {
"seekTo" -> {
val position = call.argument<Int>("position")
if (position != null) {
MusicServiceStarter.startServiceWithCommand(context, "seekTo", position = position)
MusicSingleton.startServiceWithCommand(context, "seekTo", position = position)
result.success(null)
} else {
result.error("INVALID_ARGUMENT", "Missing 'position' parameter", null)

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

@ -1,55 +0,0 @@
package com.yunqiinnovation.music_service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import androidx.core.content.ContextCompat
import java.util.jar.Manifest
//@Parcelize
//data class Song(
// val url: String,
// val title: String,
// val artist: String,
// val coverUrl: String?
//)
object MusicServiceStarter {
private val TAG = "MusicServiceStarter"
//播放单曲
fun startServiceWithCommand(context: Context, command: String, song: Map<String,Any>? = null, volume: Float? = null, position: Int? = null) {
Log.i(TAG, "liwei------------------- startServiceWithCommand: $command $song")
val intent = Intent(context, MusicService::class.java).apply {
putExtra("command", command)
song?.let {
putExtra("song", HashMap(it)) // 只有非 null 时才 putExtra
}
volume?.let { putExtra("volume", it) }
position?.let { putExtra("position", it) }
}
val success = try {
ContextCompat.startForegroundService(context, intent)
true
} catch (e: Exception) {
Log.e("MusicServiceStarter", "liwei------------------- Failed to start service", e)
false
}
Log.d(" MusicServiceStarter", "Service start success: $success")
}
//启动播放列表
fun startServiceWithPlaylist(context: Context,command: String, songs: List<Map<String,Any>>,volume: Float? = null) {
Log.i(TAG, "liwei------------------- startServiceWithPlaylist: $command $songs")
val serializableList = ArrayList<HashMap<String, Any>>().apply {
songs.forEach { add(HashMap(it)) }
}
val intent = Intent(context, MusicService::class.java).apply {
putExtra("command", command)
putExtra("songs", serializableList) // ✅ Serializable
volume?.let { putExtra("volume", it) }
}
ContextCompat.startForegroundService(context, intent)
}
}

202
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicSingleton.kt

@ -0,0 +1,202 @@
package com.yunqiinnovation.music_service
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.core.content.ContextCompat
import kotlinx.coroutines.launch
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import okhttp3.Call
import okhttp3.Callback
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.json.JSONArray
import org.json.JSONObject
import java.io.IOException
import java.util.concurrent.TimeUnit
class MusicSingleton private constructor() {
companion object {
private val TAG = "MusicServiceStarter"
@Volatile
private var INSTANCE: MusicSingleton? = null
fun getInstance(): MusicSingleton {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: MusicSingleton().also { INSTANCE = it }
}
}
//播放单曲
fun startServiceWithCommand(
context: Context,
command: String,
song: Map<String, Any>? = null,
volume: Float? = null,
position: Int? = null
) {
Log.i(TAG, "liwei------------------- startServiceWithCommand: $command $song")
val intent = Intent(context, MusicService::class.java).apply {
putExtra("command", command)
song?.let {
putExtra("song", HashMap(it)) // 只有非 null 时才 putExtra
}
volume?.let { putExtra("volume", it) }
position?.let { putExtra("position", it) }
}
val success = try {
ContextCompat.startForegroundService(context, intent)
true
} catch (e: Exception) {
Log.e("MusicServiceStarter", "liwei------------------- Failed to start service", e)
false
}
Log.d(" MusicServiceStarter", "Service start success: $success")
}
//启动播放列表
fun startServiceWithPlaylist(
context: Context,
command: String,
songs: List<Map<String, Any>>,
volume: Float? = null
) {
Log.i(TAG, "liwei------------------- startServiceWithPlaylist: $command $songs")
val serializableList = ArrayList<HashMap<String, Any>>().apply {
songs.forEach { add(HashMap(it)) }
}
val intent = Intent(context, MusicService::class.java).apply {
putExtra("command", command)
putExtra("songs", serializableList) // ✅ Serializable
volume?.let { putExtra("volume", it) }
}
ContextCompat.startForegroundService(context, intent)
}
}
private var url = ""
private var token = ""
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
/**
* 初始化单例,设置Activity上下文
*/
fun initialize(context: Context,serviceUrl:String,usertoken:String) {
url = serviceUrl;
token = usertoken;
}
/**
* 搜索歌曲(带自动登录检查和重试机制)
*/
fun searchSongs(
keyword: String,
searchType: Int = 0,
callback: (Result<List<Map<String, Any?>>>) -> Unit
) {
// 使用协程异步处理,避免阻塞主线程
coroutineScope.launch {
try {
// 配置带超时的 OkHttpClient
val client = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
// 构建搜索请求的 JSON 数据
val jsonObject = JSONObject().apply {
put("keywords", keyword)
put("searchtype", searchType)
put("limit", 20) // 默认返回20首歌曲
put("offset", 0) // 默认返回20首歌曲
}
val json = jsonObject.toString()
val mediaType = "application/json".toMediaType()
val body = json.toRequestBody(mediaType)
// 构建请求
val request = Request.Builder()
.url("$url/api/home/music_search")
.addHeader("Authorization", token)
.addHeader("Content-Type", "application/json")
.post(body)
.build()
Log.d(TAG, "发送搜索请求: $json")
// 使用异步调用
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e(TAG, "搜索歌曲请求失败: ${e.message}", e)
callback(Result.failure(e))
}
override fun onResponse(call: Call, response: Response) {
response.use {
try {
if (response.isSuccessful) {
val responseBody = response.body?.string()
Log.d(TAG, "搜索响应: $responseBody")
if (responseBody != null) {
val jsonResponse = JSONObject(responseBody)
val songs = parseSongsFromResponse(jsonResponse)
callback(Result.success(songs))
} else {
callback(Result.failure(Exception("响应体为空")))
}
} else {
val errorMsg = "搜索失败,HTTP状态码: ${response.code}"
Log.w(TAG, errorMsg)
callback(Result.failure(Exception(errorMsg)))
}
} catch (e: Exception) {
Log.e(TAG, "解析搜索响应时发生异常: ${e.message}", e)
callback(Result.failure(e))
}
}
}
})
} catch (e: Exception) {
Log.e(TAG, "搜索歌曲时发生异常: ${e.message}", e)
callback(Result.failure(e))
}
}
}
/**
* 解析服务器响应中的歌曲列表
*/
private fun parseSongsFromResponse(jsonResponse: JSONObject): List<Map<String, Any?>> {
val songs = mutableListOf<Map<String, Any?>>()
try {
val data = jsonResponse.optJSONObject("data")
val songArray = data?.optJSONArray("musics")
songArray?.let { array ->
for (i in 0 until array.length()) {
val songObj = array.getJSONObject(i)
songs.add(mapOf(
"id" to songObj.optLong("id"),
"name" to songObj.optString("name"),
"singer" to songObj.optString("singer"),
"url" to songObj.optString("url"),
"image" to songObj.optString("image"),
"ts" to songObj.optLong("ts"),
))
}
}
} catch (e: Exception) {
Log.e(TAG, "Failed to parse songs from response", e)
}
return songs
}
}

2
local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt

@ -59,6 +59,8 @@ class QQMusicManager(private val context: Context) {
Log.d(TAG, "QQ音乐服务连接断开")
qqmusicApi = null
isBindQQMusicService = false
//重连绑定
bindQQMusicApiService();
}
}

2
local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt

@ -60,7 +60,7 @@ class QQMusicSingleton private constructor() {
/**
* 检查QQ音乐App是否已安装
*/
private fun isQQMusicInstalled(): Boolean {
fun isQQMusicInstalled(): Boolean {
val packageName = when (Config.BIND_PLATFORM) {
CommonCmd.AIDL_PLATFORM_TYPE_PHONE -> "com.tencent.qqmusic"
CommonCmd.AIDL_PLATFORM_TYPE_CAR -> "com.tencent.qqmusiccar"

1
pubspec.yaml

@ -139,5 +139,6 @@ flutter:
- assets/images/headphone_new4.png
- assets/images/headphone_new3.png
- assets/images/qqmusic.png
- assets/images/music.png
- .env
- .mcp.json

Loading…
Cancel
Save