Browse Source

feat(chat_api): 添加网络搜索工具函数支持

在 MCPClient 中新增 initializeToolsFunctions 方法,用于注册 web_search 工具函数。
该函数通过外部 API 实现网络搜索能力,可用于百科、新闻、汇率等查询。
需要传入服务地址和用户令牌进行鉴权。
weicu
liwei1dao 8 months ago
parent
commit
614aa16b34
  1. 4
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  2. 8
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt
  3. 14
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt
  4. 213
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ToolsSystemFunctionHandler.kt

4
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["openaiModel"]?.toString() ?: "",
config["openaivisionModel"]?.toString() ?: "", config["openaivisionModel"]?.toString() ?: "",
config["mcpServer"]?.toString() ?: "", config["mcpServer"]?.toString() ?: "",
config["musicServiceType"]?.toString() ?: "" config["musicServiceType"]?.toString() ?: "",
serverurl,
usertoken,
) )
Log.e(TAG, "liwei -----------读取服务地址和用户token: $serverurl $usertoken") Log.e(TAG, "liwei -----------读取服务地址和用户token: $serverurl $usertoken")

8
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服务 * 初始化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 this.apiKey = apiKey
if (baseUrl.isNotEmpty()) { if (baseUrl.isNotEmpty()) {
this.baseUrl = baseUrl this.baseUrl = baseUrl
@ -257,7 +257,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
mcpConfigJson = mcpServer mcpConfigJson = mcpServer
// 异步初始化MCP客户端 // 异步初始化MCP客户端
launch { launch {
initializeMcpClient(mcpServer,musicType) initializeMcpClient(mcpServer,musicType,serverhUrl,token)
// initializeMcpClient("{}") // initializeMcpClient("{}")
} }
@ -941,9 +941,9 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
/** /**
* 初始化MCP客户端 * 初始化MCP客户端
*/ */
fun initializeMcpClient(serverUrl: String,musicType: String = "AUTO"): Boolean { fun initializeMcpClient(serverUrl: String,musicType: String = "AUTO",searchUrl: String = "",token: String = ""): Boolean {
if (_mcpClient == null) { if (_mcpClient == null) {
_mcpClient = MCPClient(context,musicType) _mcpClient = MCPClient(context,musicType,searchUrl,token)
} }
// 直接使用类的CoroutineScope启动协程 // 直接使用类的CoroutineScope启动协程

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

@ -31,7 +31,7 @@ interface FunctionHandler {
* MCP客户端 * MCP客户端
* 与 iOS 版本 MCPClient 功能对等 * 与 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 { companion object {
private const val TAG = "MCPClient" private const val TAG = "MCPClient"
@ -54,6 +54,7 @@ class MCPClient(private val context: Context? = null,musicType: String = "AUTO")
init { init {
initializeSystemFunctions() initializeSystemFunctions()
initializeToolsFunctions(searchUrl,token)
Log.d(TAG, "初始化音乐服务: $musicType") Log.d(TAG, "初始化音乐服务: $musicType")
when (musicType) { when (musicType) {
"WY" -> initializeMusiceFunctions() //网易 "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) 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)
}
}
/** /**
* 初始化音乐函数 * 初始化音乐函数

213
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<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("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<Map<String, Any?>> {
val results = mutableListOf<Map<String, Any?>>()
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, Any>): 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<String, Any?>
// 添加调试日志
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}\"}"
}
}
}
Loading…
Cancel
Save