From b45510cc1bed652dd0b75d619299b2c32767688b Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Thu, 5 Feb 2026 15:08:04 +0800 Subject: [PATCH] =?UTF-8?q?feat(chat=5Fapi):=20=E6=B7=BB=E5=8A=A0=E7=BD=91?= =?UTF-8?q?=E7=BB=9C=E6=90=9C=E7=B4=A2=E5=B7=A5=E5=85=B7=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 MCPClient 中新增 initializeToolsFunctions 方法,用于注册 web_search 工具函数。 该函数通过外部 API 实现网络搜索能力,可用于百科、新闻、汇率等查询。 需要传入服务地址和用户令牌进行鉴权。 --- .../agent_service/AgentService.kt | 4 +- .../chat_api/ChatApiService.kt | 8 +- .../com/yunqiinnovation/chat_api/MCPClient.kt | 14 +- .../chat_api/ToolsSystemFunctionHandler.kt | 213 ++++++++++++++++++ 4 files changed, 233 insertions(+), 6 deletions(-) create mode 100644 local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ToolsSystemFunctionHandler.kt diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 44bbb941a..37adabf4f 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -294,7 +294,9 @@ object AgentService : CoroutineScope { config["openaiModel"]?.toString() ?: "", config["openaivisionModel"]?.toString() ?: "", config["mcpServer"]?.toString() ?: "", - config["musicServiceType"]?.toString() ?: "" + config["musicServiceType"]?.toString() ?: "", + serverurl, + usertoken, ) Log.e(TAG, "liwei -----------读取服务地址和用户token: $serverurl $usertoken") diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt index 7be026f4c..d9d2423dd 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt @@ -219,7 +219,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor /** * 初始化ChatAPI服务 */ - fun initialize(apiKey: String, baseUrl: String, model: String, visionModel: String, mcpServer: String,musicType: String = "AUTO"): Boolean { + fun initialize(apiKey: String, baseUrl: String, model: String, visionModel: String, mcpServer: String,musicType: String = "AUTO",serverhUrl: String = "",token: String = ""): Boolean { this.apiKey = apiKey if (baseUrl.isNotEmpty()) { this.baseUrl = baseUrl @@ -257,7 +257,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor mcpConfigJson = mcpServer // 异步初始化MCP客户端 launch { - initializeMcpClient(mcpServer,musicType) + initializeMcpClient(mcpServer,musicType,serverhUrl,token) // initializeMcpClient("{}") } @@ -941,9 +941,9 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor /** * 初始化MCP客户端 */ - fun initializeMcpClient(serverUrl: String,musicType: String = "AUTO"): Boolean { + fun initializeMcpClient(serverUrl: String,musicType: String = "AUTO",searchUrl: String = "",token: String = ""): Boolean { if (_mcpClient == null) { - _mcpClient = MCPClient(context,musicType) + _mcpClient = MCPClient(context,musicType,searchUrl,token) } // 直接使用类的CoroutineScope启动协程 diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt index d0c2c6ad5..08f05d3f1 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt +++ b/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,musicType: String = "AUTO") : AutoCloseable { +class MCPClient(private val context: Context? = null,musicType: String = "AUTO",searchUrl: String = "",token: String = "") : AutoCloseable { companion object { private const val TAG = "MCPClient" @@ -54,6 +54,7 @@ class MCPClient(private val context: Context? = null,musicType: String = "AUTO") init { initializeSystemFunctions() + initializeToolsFunctions(searchUrl,token) Log.d(TAG, "初始化音乐服务: $musicType") when (musicType) { "WY" -> initializeMusiceFunctions() //网易 @@ -81,6 +82,17 @@ class MCPClient(private val context: Context? = null,musicType: String = "AUTO") Log.w(TAG, "Failed to initialize system functions", e) } } + /** + * 初始化系统函数 + */ + private fun initializeToolsFunctions(searchUrl: String = "",token: String = "") { + try { + val handler = ToolsSystemFunctionHandler(context,searchUrl,token) + handler.registerAllFunctions(this) + } catch (e: Exception) { + Log.w(TAG, "Failed to initialize system functions", e) + } + } /** * 初始化音乐函数 diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ToolsSystemFunctionHandler.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ToolsSystemFunctionHandler.kt new file mode 100644 index 000000000..565d1226c --- /dev/null +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ToolsSystemFunctionHandler.kt @@ -0,0 +1,213 @@ +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 +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 java.io.IOException +import java.util.concurrent.TimeUnit + +/** + * QQ音乐系统功能处理器 + * 负责注册QQ音乐相关的MCP函数 + */ +class ToolsSystemFunctionHandler(private val context: Context? = null,private val searchUrl:String,private val token:String) { + companion object { + const val TAG = "ToolsSystem" + } + + fun registerAllFunctions(client: MCPClient) { + try { + client.registerLocalFunction( + "web_search", + "通过搜索网络搜索回复用户相关问题,例如今日新闻、百科、医疗、股票、汇率等等", + mapOf( + "type" to "object", + "properties" to mapOf( + "keyword" to mapOf( + "type" to "string", + "description" to "搜索关键词,例如今日新闻、今日人民币汇率等等" + ), + "count" to mapOf( + "type" to "integer", + "description" to "返回的搜索结果数量,默认值为5", + "default" to 5 + ) + ), + "required" to listOf("keyword") + ), + SearchSearchHandler(context,searchUrl,token) + ) + Log.d(TAG, "音乐所有功能注册完成") + } catch (e: Exception) { + Log.e(TAG, "注册音乐功能失败: ${e.message}", e) + } + } +} + +// 搜索系统函数处理类 +private class SearchSearchHandler(private val context: Context?,private val searchUrl:String,private val token:String) : FunctionHandler { + companion object { + const val TAG = "SearchSearchHandler" + } + private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + /** + * 搜索网络内容 + */ + fun webSearch( + keyword: String, + count: Int = 0, + callback: (Result>>) -> 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("count", count) + } + + val json = jsonObject.toString() + val mediaType = "application/json".toMediaType() + val body = json.toRequestBody(mediaType) + + // 构建请求 + val request = Request.Builder() + .url("$searchUrl/api/home/aitools_bachasearch") + .addHeader("Authorization", token) + .addHeader("Content-Type", "application/json") + .post(body) + .build() + + Log.d("SearchSearchHandler", "发送搜索请求: $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 results = parseResultsFromResponse(jsonResponse) + callback(Result.success(results)) + } 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 parseResultsFromResponse(jsonResponse: JSONObject): List> { + val results = mutableListOf>() + try { + val data = jsonResponse.optJSONObject("data") + val resultsArray = data?.optJSONArray("results") + + resultsArray?.let { array -> + for (i in 0 until array.length()) { + val songObj = array.getJSONObject(i) + results.add(mapOf( + "role" to songObj.optLong("role"), + "type" to songObj.optString("type"), + "content_type" to songObj.optString("content_type"), + "content" to songObj.optString("content"), + )) + } + } + } catch (e: Exception) { + Log.e(TAG, "Failed to parse songs from response", e) + } + return results + } + + + override suspend fun handle(arguments: Map): String { + return try { + val keyword = arguments["keyword"] as? String + ?: return "{\"success\": false, \"message\": \"缺少搜索关键词\"}" + val count = arguments["count"] as? Int ?: 5 // 默认搜索歌曲 + // 使用 suspendCoroutine 将回调转换为协程 + suspendCoroutine { continuation -> + webSearch(keyword, count) { searchResult -> + val result = if (searchResult.isSuccess) { + val songs = searchResult.getOrNull() ?: emptyList() + + val songsJson = songs.joinToString(",") { song -> + val songMap = song as Map + // 添加调试日志 + Log.d("QQMusicSearchHandler", "原始数据: $songMap") + // 修正字段名映射 + val id = songMap["id"] ?: 0 + val name = songMap["name"] ?: "unknown" + val singer = songMap["singer"] ?: "unknown" + val image = songMap["image"] ?: "" + "{\"id\": $id, \"name\": \"$name\", \"singer\": \"$singer\",\"image\": \"$image\"}" + } + "{\"success\": true, \"iscard\": 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}\"}" + } + } +} +