From 210733897c5d7a04e3f65c0d92d4ac00e31fb7f4 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Sat, 23 Aug 2025 19:16:21 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E9=9F=B3=E4=B9=90?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=88=87=E6=8D=A2=E6=8E=A7=E5=88=B6=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agent_service/AgentServiceImpl.swift | 47 ++-- .../chat_api/MusicSystemFunctionHandler.kt | 16 +- .../chat_api/QQMusicSystemFunctionHandler.kt | 2 +- .../chat_api/ios/chat_api/Package.swift | 2 + .../Sources/chat_api/ChatApiPlugin.swift | 7 +- .../Sources/chat_api/ChatApiService.swift | 10 +- .../chat_api/Sources/chat_api/MCPClient.swift | 36 ++- .../chat_api/MusicSystemFunctionHandler.swift | 223 ++++++++++++++++++ .../QQMusicSystemFunctionHandler.swift | 10 + .../chat_api/SystemFunctionHandler.swift | 74 +----- .../Sources/music_service/MusicService.swift | 100 +++++++- .../music_service/MusicServicePlugin.swift | 7 + .../Sources/qq_music/QPlayAutoManager.swift | 36 ++- 13 files changed, 445 insertions(+), 125 deletions(-) create mode 100644 local_plugins/chat_api/ios/chat_api/Sources/chat_api/MusicSystemFunctionHandler.swift diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index ed21cefaa..858e1baec 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/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 方法 diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MusicSystemFunctionHandler.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MusicSystemFunctionHandler.kt index 918ebc967..8cc1fd991 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MusicSystemFunctionHandler.kt +++ b/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(), @@ -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(), @@ -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(), @@ -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", diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt index 1b9e9fe80..b500773cf 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt +++ b/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音乐", diff --git a/local_plugins/chat_api/ios/chat_api/Package.swift b/local_plugins/chat_api/ios/chat_api/Package.swift index 6772cc3c9..3fe0ccdb5 100644 --- a/local_plugins/chat_api/ios/chat_api/Package.swift +++ b/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") ], diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiPlugin.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiPlugin.swift index 5bb2c7eab..edb01085d 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiPlugin.swift +++ b/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) } diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift index b23b6dca5..486c13e2c 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift +++ b/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 { diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift index 5f4ad0f10..7ad6db402 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift +++ b/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 { diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MusicSystemFunctionHandler.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MusicSystemFunctionHandler.swift new file mode 100644 index 000000000..c2e2c3409 --- /dev/null +++ b/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\": \"已切换到下一首\"}" + } +} diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/QQMusicSystemFunctionHandler.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/QQMusicSystemFunctionHandler.swift index 001176fe5..41021c005 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/QQMusicSystemFunctionHandler.swift +++ b/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音乐搜索和播放功能 diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift index e1b6368e3..ab4ed49d3 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift +++ b/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) { diff --git a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift index ce5ca2ab5..1e3d36863 100644 --- a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift +++ b/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 } } -} \ No newline at end of file +} diff --git a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift index 462a3f53d..16a2983a1 100644 --- a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicServicePlugin.swift +++ b/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, diff --git a/local_plugins/qq_music/ios/qq_music/Sources/qq_music/QPlayAutoManager.swift b/local_plugins/qq_music/ios/qq_music/Sources/qq_music/QPlayAutoManager.swift index f78bdf4c3..e08fa3bef 100644 --- a/local_plugins/qq_music/ios/qq_music/Sources/qq_music/QPlayAutoManager.swift +++ b/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) { From a49b291041e3222df7fb8af82b4bd47d8b0ff095 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sat, 23 Aug 2025 19:23:29 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat(home):=20=E6=B7=BB=E5=8A=A0OTA?= =?UTF-8?q?=E5=8D=87=E7=BA=A7=E6=A3=80=E6=9F=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在连接成功后自动检查OTA固件升级 - 显示升级提醒对话框并提供升级选项 - 将充电盒标签改为国际化字符串 --- .../home/controllers/home_controller.dart | 160 ++++++++++++++++++ lib/modules/home/views/home_view.dart | 2 +- 2 files changed, 161 insertions(+), 1 deletion(-) diff --git a/lib/modules/home/controllers/home_controller.dart b/lib/modules/home/controllers/home_controller.dart index 83a2330e2..68c70b643 100644 --- a/lib/modules/home/controllers/home_controller.dart +++ b/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 _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, + ), + ); + } } diff --git a/lib/modules/home/views/home_view.dart b/lib/modules/home/views/home_view.dart index d24e726fb..5afc3586f 100644 --- a/lib/modules/home/views/home_view.dart +++ b/lib/modules/home/views/home_view.dart @@ -504,7 +504,7 @@ class HomeView extends GetView { // 充电盒 _buildCompactBatteryItem( - label: '充电盒', + label: 'chargingCase'.tr, batteryLevel: caseBattery.value, isCharging: caseCharging.value, isDarkMode: isDarkMode,