Browse Source

Merge branch 'new_dev' of https://github.com/deepcloud2048/deep_voice into new_dev

newdev_shunjiawei
fdp 1 year ago
parent
commit
8e23924cc5
  1. 160
      lib/modules/home/controllers/home_controller.dart
  2. 2
      lib/modules/home/views/home_view.dart
  3. 47
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  4. 16
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MusicSystemFunctionHandler.kt
  5. 2
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt
  6. 2
      local_plugins/chat_api/ios/chat_api/Package.swift
  7. 7
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiPlugin.swift
  8. 10
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift
  9. 36
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift
  10. 223
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/MusicSystemFunctionHandler.swift
  11. 10
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/QQMusicSystemFunctionHandler.swift
  12. 74
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift
  13. 100
      local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift
  14. 7
      local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift
  15. 36
      local_plugins/qq_music/ios/qq_music/Sources/qq_music/QPlayAutoManager.swift

160
lib/modules/home/controllers/home_controller.dart

@ -1,3 +1,4 @@
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'dart:async';
import '../../../data/services/novice_guide_service.dart';
@ -6,6 +7,7 @@ import '../../../core/utils/logger.dart';
import 'package:flutter/material.dart';
import '../../../data/services/bluetooth_manager.dart';
import '../../../data/services/ble_manager.dart';
import '../../ota_upgrade/controllers/ota_core_contropller.dart';
/// 连接状态枚举
enum ConnectionStatus {
@ -28,6 +30,10 @@ class HomeController extends GetxController {
static const Duration _batteryRefreshInterval =
Duration(seconds: 2); // 2秒刷新一次
// 添加OTA升级检查相关变量
final _otaCoreService = OtaCoreService();
static bool _hasCheckedOta = false; // 防止重复检查
bool get isFullyConnected => _isFullyConnected.value;
RxBool get isFullyConnectedRx => _isFullyConnected;
BluetoothDevice? get currentDevice => _bluetoothManager.currentDevice;
@ -145,12 +151,18 @@ class HomeController extends GetxController {
Future.delayed(const Duration(milliseconds: 500), () {
_refreshBatteryInfo();
});
// 连接成功后检查OTA升级
Future.delayed(const Duration(seconds: 2), () {
_checkOtaUpgrade();
});
}
// 根据连接状态控制定时器
if (isNowConnected) {
_startBatteryRefreshTimer(); // 连接时启动定时器
} else {
_stopBatteryRefreshTimer(); // 断开时停止定时器
_hasCheckedOta = false; // 重置OTA检查状态
}
}
@ -161,6 +173,7 @@ class HomeController extends GetxController {
_isFullyConnected.value = false;
_isFullyConnected.refresh();
_stopBatteryRefreshTimer(); // 发生错误时停止定时器
_hasCheckedOta = false; // 重置OTA检查状态
update();
}
}
@ -327,4 +340,151 @@ class HomeController extends GetxController {
//Logger.e(_tag, '刷新电池信息时发生错误: $e');
}
}
/// 检查OTA固件升级
/// 在BLE连接成功后调用,检查是否需要升级
Future<void> _checkOtaUpgrade() async {
try {
// 防止重复检查
if (_hasCheckedOta) {
return;
}
Logger.d(_tag, '开始检查OTA固件升级...');
// 检查BLE连接状态
if (!_isFullyConnected.value) {
Logger.d(_tag, '设备未完全连接,跳过OTA检查');
return;
}
// 获取当前版本信息
final currentInfo = await _otaCoreService.getCurrentVersionInfo();
final currentVersion = currentInfo['currentVersion'] as String;
// 获取最新版本信息
final latestInfo = await _otaCoreService.getLatestVersionInfo();
final latestVersion = latestInfo['latestVersion'] as String;
Logger.d(_tag, '当前版本: $currentVersion, 最新版本: $latestVersion');
// 检查是否有更新
final hasUpdate =
_otaCoreService.isNewVersionAvailable(currentVersion, latestVersion);
if (hasUpdate) {
Logger.d(_tag, '发现新版本,显示升级提醒');
_showOtaUpgradeDialog(
latestVersion, latestInfo['updateDescription'] as String?);
} else {
Logger.d(_tag, '当前已是最新版本');
}
_hasCheckedOta = true;
} catch (e) {
Logger.e(_tag, '检查OTA升级失败: $e');
// 即使检查失败,也标记为已检查,避免重复尝试
_hasCheckedOta = true;
}
}
/// 显示OTA升级提醒对话框
/// [latestVersion] 最新版本号
/// [updateDescription] 更新描述
void _showOtaUpgradeDialog(String latestVersion, String? updateDescription) {
Get.dialog(
AlertDialog(
title: Row(
children: [
Icon(
Icons.headset,
color: Colors.blue[600],
size: 24.sp,
),
SizedBox(width: 8.w),
Text(
'ota_newVersionFound'.tr,
style: TextStyle(
color: Colors.blue[700],
fontWeight: FontWeight.w600,
),
),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${'latestFirmwareVersion'.tr}: $latestVersion',
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16.sp,
),
),
// if (updateDescription != null && updateDescription.isNotEmpty)
// SizedBox(height: 8.sp),
// if (updateDescription != null && updateDescription.isNotEmpty)
// Text(
// updateDescription,
// style: TextStyle(
// color: Colors.grey[600],
// fontSize: 14.sp,
// ),
// ),
// if (updateDescription != null && updateDescription.isNotEmpty)
// SizedBox(height: 8.sp),
// Text(
// '建议及时升级以获得更好的使用体验',
// style: TextStyle(
// color: Colors.orange[700],
// fontSize: 12.sp,
// ),
// ),
],
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// 稍后再说 (靠左)
TextButton(
onPressed: () {
Logger.d(_tag, '用户取消OTA升级');
Get.back();
},
child: Text(
'talkLater'.tr,
style: TextStyle(color: Colors.grey[600]),
),
),
// 立即升级 (靠右)
ElevatedButton(
onPressed: () {
Logger.d(_tag, '用户确认OTA升级,跳转到升级页面');
Get.back();
Get.toNamed(Routes.OTA_UPGRADE);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue[600],
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.sp),
),
),
child: Text(
'ota_updateNow'.tr,
style: TextStyle(fontSize: 14.sp),
),
),
],
),
],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.sp),
),
backgroundColor: Colors.white,
),
);
}
}

2
lib/modules/home/views/home_view.dart

@ -504,7 +504,7 @@ class HomeView extends GetView<HomeController> {
// 充电盒
_buildCompactBatteryItem(
label: '充电盒',
label: 'chargingCase'.tr,
batteryLevel: caseBattery.value,
isCharging: caseCharging.value,
isDarkMode: isDarkMode,

47
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift

@ -46,6 +46,7 @@ class AgentServiceImpl: NSObject {
private var chatHistory: [[String: Any]] = []
private var mcpServer: String = ""
private var musicServiceType:String = ""
// 聊天存储服务(使用单例)
private var chatStorageHelper: ChatStorageHelper {
@ -62,6 +63,8 @@ class AgentServiceImpl: NSObject {
var currsessionId: String = ""
var language: String = ""
private var serverurl = ""
private var usertoken = ""
private var idleTimer: DispatchSourceTimer?
private let maxIdleSeconds: TimeInterval = 10
@ -146,7 +149,14 @@ class AgentServiceImpl: NSObject {
// os_log("initialize: config=%{public}@", log: logger, type: .info, config)
self.config = config
if let serverurl = config["serverurl"] as? String {
self.serverurl = serverurl
}
if let usertoken = config["usertoken"] as? String {
self.usertoken = usertoken
}
MusicService.shared.initialize(serviceUrl: serverurl, userToken: usertoken)
if let openaiApiKey = config["openaiApiKey"] as? String {
self.apiKey = openaiApiKey
} else {
@ -191,6 +201,10 @@ class AgentServiceImpl: NSObject {
if let mcpServer = config["mcpServer"] as? String {
self.mcpServer = mcpServer
}
if let musicServiceType = config["musicServiceType"] as? String {
self.musicServiceType = musicServiceType
}
// self.mcpServer = """
// {
// "mcpServers": {
@ -270,7 +284,8 @@ class AgentServiceImpl: NSObject {
baseUrl: baseUrl,
model: model,
visionModel:visionModel,
mcpServer: mcpServer
mcpServer: mcpServer,
musicServiceType:musicServiceType
)
os_log("ChatAPI服务初始化结果: %{public}@", log: logger, type: .info, success ? "成功" : "失败")
@ -379,7 +394,7 @@ class AgentServiceImpl: NSObject {
} else {
//AudioSessionHub.shared.begin(.voice)
}
print("ai启动语音=\(audioSourceType)=\(useBle)")
print("ai启动语音=\(audioSourceType)=\(useBle)")
guard let success = azureAsrHelper?.startContinuousRecognition(
// callback: self,
audioSourceType: audioSourceType
@ -775,11 +790,11 @@ private func handleJSONDictionary(_ jsonDict: [String: Any], toolName: String) -
}
metadata["notreply"] = notreply
// 处理卡片数据
if let cardMusic = jsonDict["card_music"] as? [String: Any] {
processMusicCard(cardMusic)
} else if let cardMusiclist = jsonDict["card_musiclist"] as? [String: Any],
let musics = cardMusiclist["musics"] as? [[String: Any]] {
processMusicListCard(musics)
// if let cardMusic = jsonDict["card_music"] as? [String: Any] {
// processMusicCard(cardMusic)
// } else
if let cardMusiclist = jsonDict["data"] as? [[String: Any]], toolName == "music_searchandplay_songs" {
processMusicListCard(cardMusiclist)
} else if let cardNavigation = jsonDict["card_navigation"] as? [String: Any] {
processNavigationCard(cardNavigation)
} else if let cardQQMusiclist = jsonDict["data"] as? [[String: Any]], toolName == "qqmusic_searchandplay_songs" {
@ -922,22 +937,6 @@ private func jsonToString(_ json: [String: Any]) -> String? {
MusicService.shared.playPlaylist(songs: songs)
}
// 添加定位监听
// private func addlocationmonitor(){
// // 通常放在启动后或模块初始化时
// NotificationCenter.default.addObserver(
// forName: .updatelocationEvent,
// object: nil,
// queue: .main
// ) { notification in
// if let info = notification.userInfo as? [String: String] {
// print("📤 地址信息:\(info)")
// self.locationInfo = info
// }
// }
// }
/**
* 填充模板字符串,使用正则表达式匹配 {key} 格式的占位符
* 对应 Kotlin 版本的 fillTemplate 方法

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

@ -47,8 +47,8 @@ class MusicSystemFunctionHandler(private val context: Context? = null) {
// 注册媒体播放功能
client.registerLocalFunction(
name = "media_play",
description = "播放媒体",
name = "music_resume",
description = "恢复播放音乐",
parameters = mapOf(
"type" to "object",
"properties" to emptyMap<String, Any>(),
@ -59,8 +59,8 @@ class MusicSystemFunctionHandler(private val context: Context? = null) {
// 注册媒体暂停功能
client.registerLocalFunction(
name = "media_pause",
description = "暂停媒体播放",
name = "music_pause",
description = "暂停音乐播放",
parameters = mapOf(
"type" to "object",
"properties" to emptyMap<String, Any>(),
@ -70,8 +70,8 @@ class MusicSystemFunctionHandler(private val context: Context? = null) {
)
// 注册媒体暂停功能
client.registerLocalFunction(
name = "media_close",
description = "关闭媒体播放",
name = "music_close",
description = "关闭音乐播放",
parameters = mapOf(
"type" to "object",
"properties" to emptyMap<String, Any>(),
@ -81,7 +81,7 @@ class MusicSystemFunctionHandler(private val context: Context? = null) {
)
// 注册媒体上一首功能
client.registerLocalFunction(
name = "media_previous",
name = "music_previous",
description = "播放上一首",
parameters = mapOf(
"type" to "object",
@ -93,7 +93,7 @@ class MusicSystemFunctionHandler(private val context: Context? = null) {
// 注册媒体下一首功能
client.registerLocalFunction(
name = "media_next",
name = "music_next",
description = "播放下一首",
parameters = mapOf(
"type" to "object",

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

@ -71,7 +71,7 @@ class QQMusicSystemFunctionHandler(private val context: Context? = null) {
// QQMusicPlaySongsHandler(context)
// )
// QQ音乐播放控制
//QQ音乐播放控制
// client.registerLocalFunction(
// "qqmusic_play",
// "播放QQ音乐",

2
local_plugins/chat_api/ios/chat_api/Package.swift

@ -16,6 +16,7 @@ let package = Package(
.package(url: "https://github.com/MacPaw/OpenAI.git", exact: "0.4.3"),
.package(url: "https://github.com/modelcontextprotocol/swift-sdk.git", from: "0.9.0"),
.package(path: "../../../location_service/ios/location_service"),
.package(path: "../../../music_service/ios/music_service"),
.package(path: "../../../qq_music/ios/qq_music"),
@ -26,6 +27,7 @@ let package = Package(
dependencies: [
.product(name: "MCP", package: "swift-sdk"),
.product(name: "OpenAI", package: "OpenAI"),
.product(name: "music-service", package: "music_service"),
.product(name: "qq-music", package: "qq_music"),
.product(name: "location-service", package: "location_service")
],

7
local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiPlugin.swift

@ -65,7 +65,7 @@ public class ChatApiPlugin: NSObject, FlutterPlugin {
let model = args["model"] as? String ?? ""
let visionModel = args["modelmedia"] as? String ?? ""
let mcpServer = args["mcpServer"] as? String ?? ""
let musicServiceType = args["musicServiceType"] as? String ?? ""
// service.setStreamCallback(StreamCallbackImpl(eventSink: eventSink))
let success = service.initialize(
@ -73,7 +73,8 @@ public class ChatApiPlugin: NSObject, FlutterPlugin {
baseUrl: baseUrl,
model: model,
visionModel:visionModel,
mcpServer: mcpServer
mcpServer: mcpServer,
musicServiceType:musicServiceType
)
result(success)
@ -166,7 +167,7 @@ public class ChatApiPlugin: NSObject, FlutterPlugin {
return
}
let success = service.initializeMcpClient(serverUrl: serverUrl)
let success = service.initializeMcpClient(serverUrl: serverUrl,musicServiceType:"")
result(success)
}

10
local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift

@ -88,7 +88,7 @@ public class ChatApiService: NSObject {
}
/// 初始化ChatAPI服务
public func initialize(apiKey: String, baseUrl: String, model: String, visionModel:String, mcpServer: String) -> Bool {
public func initialize(apiKey: String, baseUrl: String, model: String, visionModel:String, mcpServer: String,musicServiceType:String) -> Bool {
self.apiKey = apiKey
if !baseUrl.isEmpty {
self.baseUrl = baseUrl
@ -136,9 +136,9 @@ public class ChatApiService: NSObject {
// 初始化MCP客户端
if mcpClient == nil {
mcpClient = MCPClient()
mcpClient = MCPClient(musicServiceType:musicServiceType)
}
_ = initializeMcpClient(serverUrl: mcpServer)
_ = initializeMcpClient(serverUrl: mcpServer,musicServiceType:musicServiceType)
isInitialized = !apiKey.isEmpty
return isInitialized
@ -672,8 +672,8 @@ private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] {
}
/// 初始化MCP客户端
public func initializeMcpClient(serverUrl: String) -> Bool {
mcpClient = MCPClient()
public func initializeMcpClient(serverUrl: String,musicServiceType:String) -> Bool {
mcpClient = MCPClient(musicServiceType:musicServiceType)
Task {
do {

36
local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift

@ -422,12 +422,28 @@ public class MCPSubClient {
/// MCP客户端
public class MCPClient {
// private let musicServiceType: String?
private var localFunctions: [String: FunctionHandler] = [:]
private var localFunctionDefs: [String: [String: Any]] = [:]
private var subClients: [String: MCPSubClient] = [:]
public init() {
public init(musicServiceType: String) {
initializeSystemFunctions()
switch musicServiceType {
case "WY":
initializeMusicSystemFunction() // 更明确的命名
case "QQ":
initializeQQMusicSystemFunction()
default:
// 正确传入参数,并明确检查的是QQ音乐
if QQMusicSystemFunctionHandler.isMusicAppInstalled() {
initializeQQMusicSystemFunction()
} else {
// 可以考虑是否需要检查网易云音乐是否安装
initializeMusicSystemFunction()
}
}
}
private func initializeSystemFunctions() {
@ -435,14 +451,30 @@ public class MCPClient {
let handler = SystemFunctionHandler()
handler.registerAllFunctions(client: self)
} catch {
// 静默处理错误
}
}
private func initializeMusicSystemFunction() {
do {
// 初始化QQ音乐功能
let qqMusicHandler = MusicSystemFunctionHandler()
qqMusicHandler.registerAllFunctions(client: self)
} catch {
// 静默处理错误
}
}
private func initializeQQMusicSystemFunction() {
do {
// 初始化QQ音乐功能
let qqMusicHandler = QQMusicSystemFunctionHandler()
qqMusicHandler.registerAllFunctions(client: self)
} catch {
} catch {
// 静默处理错误
}
}
public func connectToSSE(mcpConfigJson: String) async throws -> Bool {
// 清除现有连接
for client in subClients.values {

223
local_plugins/chat_api/ios/chat_api/Sources/chat_api/MusicSystemFunctionHandler.swift

@ -0,0 +1,223 @@
import Foundation
import music_service
class MusicSystemFunctionHandler {
// 添加 registerAllFunctions 方法
func registerAllFunctions(client: MCPClient) {
// 注册QQ音乐搜索和播放功能
client.registerLocalFunction(
name: "music_searchandplay_songs",
description: "在音乐播放服务中搜索并播放歌曲,支持歌曲、专辑、歌手等搜索类型",
parameters: [
"type": "object",
"properties": [
"keyword": [
"type": "string",
"description": "搜索关键词,歌曲名、专辑名或歌手名"
],
"searchType": [
"type": "integer",
"description": "搜索类型:0-歌名搜索,1-歌手搜索,2-歌曲类型搜索",
"default": 0
]
],
"required": ["keyword"]
],
handler: MusicSearchHandler()
)
// 注册媒体控制函数
registerMediaPlayFunction(client: client)
registerMediaPauseFunction(client: client)
registerMediaCloseFunction(client: client)
registerMediaPreviousFunction(client: client)
registerMediaNextFunction(client: client)
}
}
// MARK: - Private Handler Classes
private class MusicSearchHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
guard let keyword = arguments["keyword"] as? String else {
let result: [String: Any] = ["success": false, "message": "缺少搜索关键词"]
let jsonData = try JSONSerialization.data(withJSONObject: result)
return String(data: jsonData, encoding: .utf8) ?? "{}"
}
let searchType = arguments["searchType"] as? Int ?? 0
return await withCheckedContinuation { continuation in
MusicService.shared.searchSongs(keyword: keyword, searchType: searchType) { searchResult in
switch searchResult {
case .success(let songs):
let songsJson = songs.map { song -> [String: Any] in
return [
"id": song["id"] ?? 0,
"name": song["name"] ?? "unknown",
"singer": song["singer"] ?? "unknown",
"url": song["url"] ?? "",
"image": song["image"] ?? "",
"ts": song["ts"] ?? 0
]
}
let result: [String: Any] = [
"success": true,
"broadcast": false,
"message": "搜索成功,正在播放",
"data": songsJson
]
do {
let jsonData = try JSONSerialization.data(withJSONObject: result, options: [])
let jsonString = String(data: jsonData, encoding: .utf8) ?? "{}"
continuation.resume(returning: jsonString)
} catch {
continuation.resume(returning: "{\"success\": false, \"message\": \"JSON serialization failed\"}")
}
case .failure(let error):
let result: [String: Any] = [
"success": false,
"message": "搜索失败: \(error.localizedDescription)"
]
do {
let jsonData = try JSONSerialization.data(withJSONObject: result)
let jsonString = String(data: jsonData, encoding: .utf8) ?? "{}"
continuation.resume(returning: jsonString)
} catch {
continuation.resume(returning: "{\"success\": false, \"message\": \"Search failed and JSON serialization failed\"}")
}
}
}
}
}
}
/// 注册媒体播放函数
private func registerMediaPlayFunction(client: MCPClient) {
let parameters: [String: Any] = [
"type": "object",
"properties": [:],
"required": []
]
let handler = MediaPlayHandler()
_ = client.registerLocalFunction(
name: "music_resume",
description: "恢复音乐播放",
parameters: parameters,
handler: handler
)
}
/// 注册媒体暂停函数
private func registerMediaPauseFunction(client: MCPClient) {
let parameters: [String: Any] = [
"type": "object",
"properties": [:],
"required": []
]
let handler = MediaPauseHandler()
_ = client.registerLocalFunction(
name: "music_pause",
description: "暂停音乐播放",
parameters: parameters,
handler: handler
)
}
/// 注册媒体关闭函数
private func registerMediaCloseFunction(client: MCPClient) {
let parameters: [String: Any] = [
"type": "object",
"properties": [:],
"required": []
]
let handler = MediaCloseHandler()
_ = client.registerLocalFunction(
name: "music_close",
description: "关闭音乐播放",
parameters: parameters,
handler: handler
)
}
/// 注册媒体上一首函数
private func registerMediaPreviousFunction(client: MCPClient) {
let parameters: [String: Any] = [
"type": "object",
"properties": [:],
"required": []
]
let handler = MediaPreviousHandler()
_ = client.registerLocalFunction(
name: "music_previous",
description: "播放上一首",
parameters: parameters,
handler: handler
)
}
/// 注册媒体下一首函数
private func registerMediaNextFunction(client: MCPClient) {
let parameters: [String: Any] = [
"type": "object",
"properties": [:],
"required": []
]
let handler = MediaNextHandler()
_ = client.registerLocalFunction(
name: "music_next",
description: "播放下一首",
parameters: parameters,
handler: handler
)
}
/// 媒体播放处理器
private class MediaPlayHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
NotificationCenter.default.post(name: NSNotification.Name("MediaPlay"), object: nil)
return "{\"broadcast\": false,\"result\": \"已开始音乐播放\"}"
}
}
/// 媒体暂停处理器
private class MediaPauseHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
NotificationCenter.default.post(name: NSNotification.Name("MediaStop"), object: nil)
return "{\"broadcast\": false,\"result\": \"已停止音乐播放\"}"
}
}
/// 媒体暂停处理器
private class MediaCloseHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
NotificationCenter.default.post(name: NSNotification.Name("MediaPause"), object: nil)
return "{\"broadcast\": false,\"result\": \"已暂停音乐播放\"}"
}
}
/// 媒体上一首处理器
private class MediaPreviousHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
NotificationCenter.default.post(name: NSNotification.Name("MediaPrevious"), object: nil)
return "{\"broadcast\": false,\"result\": \"已切换到上一首\"}"
}
}
/// 媒体下一首处理器
private class MediaNextHandler: FunctionHandler {
func handle(arguments: [String: Any]) async throws -> String {
NotificationCenter.default.post(name: NSNotification.Name("MediaNext"), object: nil)
return "{\"broadcast\": false,\"result\": \"已切换到下一首\"}"
}
}

10
local_plugins/chat_api/ios/chat_api/Sources/chat_api/QQMusicSystemFunctionHandler.swift

@ -3,6 +3,16 @@ import qq_music
class QQMusicSystemFunctionHandler {
// 静态方法:检查指定音乐应用是否安装
static func isMusicAppInstalled() -> Bool {
// 检查QQ音乐是否安装
guard QPlayAutoManager.shared.isQQMusicInstalled else {
print("QPlayAutoManager: QQ音乐未安装,无法处理URL")
return false
}
return true
}
// 添加 registerAllFunctions 方法
func registerAllFunctions(client: MCPClient) {
// 注册QQ音乐搜索和播放功能

74
local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift

@ -83,12 +83,6 @@ public class SystemFunctionHandler: NSObject {
// 注册获取当前位置函数
registerGetCurrentLocationFunction(client: client)
// 注册媒体控制函数
// registerMediaPlayFunction(client: client)
// registerMediaPauseFunction(client: client)
// registerMediaPreviousFunction(client: client)
// registerMediaNextFunction(client: client)
// 注册打开录音机功能
registerOpenRecorderFunction(client: client)
@ -260,73 +254,7 @@ public class SystemFunctionHandler: NSObject {
)
}
/// 注册媒体播放函数
private func registerMediaPlayFunction(client: MCPClient) {
let parameters: [String: Any] = [
"type": "object",
"properties": [:],
"required": []
]
let handler = MediaPlayHandler()
_ = client.registerLocalFunction(
name: "media_play",
description: "播放媒体",
parameters: parameters,
handler: handler
)
}
/// 注册媒体暂停函数
private func registerMediaPauseFunction(client: MCPClient) {
let parameters: [String: Any] = [
"type": "object",
"properties": [:],
"required": []
]
let handler = MediaPauseHandler()
_ = client.registerLocalFunction(
name: "media_pause",
description: "暂停媒体播放",
parameters: parameters,
handler: handler
)
}
/// 注册媒体上一首函数
private func registerMediaPreviousFunction(client: MCPClient) {
let parameters: [String: Any] = [
"type": "object",
"properties": [:],
"required": []
]
let handler = MediaPreviousHandler()
_ = client.registerLocalFunction(
name: "media_previous",
description: "播放上一首",
parameters: parameters,
handler: handler
)
}
/// 注册媒体下一首函数
private func registerMediaNextFunction(client: MCPClient) {
let parameters: [String: Any] = [
"type": "object",
"properties": [:],
"required": []
]
let handler = MediaNextHandler()
_ = client.registerLocalFunction(
name: "media_next",
description: "播放下一首",
parameters: parameters,
handler: handler
)
}
/// 注册打开录音机函数
private func registerOpenRecorderFunction(client: MCPClient) {

100
local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift

@ -20,6 +20,9 @@ public class MusicService: NSObject {
private var flutterChannel: FlutterMethodChannel?
private var progressTimer: Timer?
private var serviceUrl: String = ""
private var userToken: String = ""
// 音量控制相关
private var currentVolume: Float = 1.0
private var originalVolume: Float = 1.0
@ -34,6 +37,13 @@ public class MusicService: NSObject {
setupAudioSession()
}
public func initialize(serviceUrl: String, userToken: String) {
self.serviceUrl = serviceUrl
self.userToken = userToken
print("MusicService initialized with url: \(serviceUrl)")
}
// MARK: - 音频会话设置
private func setupAudioSession() {
do {
@ -56,6 +66,94 @@ public class MusicService: NSObject {
return currentIdInternal
}
// MARK: - 歌曲搜索
public func searchSongs(keyword: String, searchType: Int = 0, completion: @escaping (Result<[[String: Any?]], Error>) -> Void) {
guard !serviceUrl.isEmpty, let url = URL(string: "\(serviceUrl)/api/home/music_search") else {
completion(.failure(NSError(domain: "MusicService", code: -1, userInfo: [NSLocalizedDescriptionKey: "Service URL is not configured"])))
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue(userToken, forHTTPHeaderField: "Authorization")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: Any] = [
"keywords": keyword,
"searchtype": searchType,
"limit": 20,
"offset": 0
]
do {
request.httpBody = try JSONSerialization.data(withJSONObject: body, options: [])
} catch {
completion(.failure(error))
return
}
print("Sending search request to \(url) with keyword: \(keyword)")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("Search request failed: \(error.localizedDescription)")
completion(.failure(error))
return
}
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
print("Search failed with status code: \(statusCode)")
completion(.failure(NSError(domain: "MusicService", code: statusCode, userInfo: [NSLocalizedDescriptionKey: "Search failed with status code: \(statusCode)"])))
return
}
guard let data = data else {
print("Search response data is empty")
completion(.failure(NSError(domain: "MusicService", code: -2, userInfo: [NSLocalizedDescriptionKey: "Response data is empty"])))
return
}
do {
if let jsonResponse = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
let songs = self.parseSongsFromResponse(jsonResponse: jsonResponse)
print("Search successful, found \(songs.count) songs")
completion(.success(songs))
} else {
completion(.failure(NSError(domain: "MusicService", code: -3, userInfo: [NSLocalizedDescriptionKey: "Failed to parse JSON response"])))
}
} catch {
print("Failed to parse search response: \(error.localizedDescription)")
completion(.failure(error))
}
}
task.resume()
}
private func parseSongsFromResponse(jsonResponse: [String: Any]) -> [[String: Any?]] {
var songs: [[String: Any?]] = []
guard let data = jsonResponse["data"] as? [String: Any],
let musics = data["musics"] as? [[String: Any]] else {
return songs
}
for songObj in musics {
songs.append([
"id": songObj["id"] as? Int64,
"name": songObj["name"] as? String,
"singer": songObj["singer"] as? String,
"url": songObj["url"] as? String,
"image": songObj["image"] as? String,
"ts": songObj["ts"] as? Int64
])
}
return songs
}
public func play(song: [String: Any]) {
guard let urlString = song["url"] as? String, let url = URL(string: urlString) else { return }
@ -624,4 +722,4 @@ override public func observeValue(forKeyPath keyPath: String?, of object: Any?,
break
}
}
}
}

7
local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift

@ -110,6 +110,13 @@ public class MusicServicePlugin: NSObject, FlutterPlugin {
}
private func addNSNotification(){
NotificationCenter.default.addObserver(
forName: NSNotification.Name("MediaStop"),
object: nil,
queue: .main
) { _ in
MusicService.shared.stop()
}
NotificationCenter.default.addObserver(
forName: NSNotification.Name("MediaPause"),
object: nil,

36
local_plugins/qq_music/ios/qq_music/Sources/qq_music/QPlayAutoManager.swift

@ -59,7 +59,7 @@ import QPlayAutoSDK
registerAppWithDefaultConfig()
// 启用自动登录
tryAutoLoginWithStoredToken()
// tryAutoLoginWithStoredToken()
}
@objc private func appWillTerminate() {
@ -376,17 +376,34 @@ import QPlayAutoSDK
}
if isLoginOK {
// 已登录,直接执行操作
operation()
// 已登录,检查连接状态
if isConnected {
// 已连接,直接执行操作
operation()
} else {
// 未连接,先连接
print("QPlayAutoManager: 未连接,正在尝试连接...")
connect(forceLogin: false)
// 使用延迟检查连接状态
DispatchQueue.main.asyncAfter(deadline: .now() + 10.0) { [weak self] in
guard let self = self else { return }
if self.isConnected {
print("QPlayAutoManager: 连接成功,执行操作")
operation()
} else {
print("QPlayAutoManager: 连接失败,无法执行操作")
onFailure?(.connectionFailed)
}
}
}
} else {
// 未登录,先登录
print("QPlayAutoManager: 未登录,正在尝试登录...")
performLogin { [weak self] success in
guard let self = self else { return }
if success {
print("QPlayAutoManager: 登录成功,执行操作")
operation()
print("QPlayAutoManager: 登录成功,检查连接状态")
self.ensureLoginAndExecute(operation, onFailure: onFailure)
} else {
print("QPlayAutoManager: 登录失败,无法执行操作")
onFailure?(.loginFailed)
@ -450,7 +467,7 @@ import QPlayAutoSDK
print(" - 当前登录状态: \(isLoginOK)")
// 强制登录,会跳转到QQ音乐
connect(forceLogin: true)
connect(forceLogin: false)
// 设置超时监控(仅记录,不清理回调)
DispatchQueue.main.asyncAfter(deadline: .now() + 60.0) { [weak self] in
@ -823,6 +840,9 @@ extension QPlayAutoManager: QPlayAutoSDKDelegate {
for weakDelegate in delegates {
weakDelegate.delegate?.onConnectStateChanged?(newState)
}
if newState == QPlayAutoConnectState.disconnect {
connect(forceLogin: false)
}
}
public func onQPlayAutoPlayStateChanged(_ playState: QPlayAutoPlayState, song: QPlayAutoListItem, position: Int) {

Loading…
Cancel
Save