15 changed files with 2144 additions and 380 deletions
@ -1,70 +0,0 @@ |
|||
# ChatAPI Android 实现 |
|||
|
|||
基于 [openai-kotlin 4.0.1](https://github.com/aallam/openai-kotlin/tree/4.0.1) 的 ChatAPI 插件 Android 实现。 |
|||
|
|||
## 特性 |
|||
|
|||
- ✅ 基于 openai-kotlin 4.0.1 的 OpenAI API 客户端 |
|||
- ✅ 与 iOS 版本接口完全一致 |
|||
- ✅ 支持流式和非流式对话 |
|||
- ✅ 支持多模态内容(文本和图像识别) |
|||
- ✅ 工具调用架构预留(MCP 功能暂时留空) |
|||
- ✅ 协程支持和异步处理 |
|||
- ✅ 完整的错误处理和取消机制 |
|||
|
|||
## 依赖 |
|||
|
|||
- openai-kotlin 4.0.1 |
|||
- ktor-client-okhttp 2.3.2 |
|||
- kotlinx-coroutines |
|||
- gson |
|||
|
|||
## 主要文件 |
|||
|
|||
- `ChatApiPlugin.kt` - Flutter 插件桥接层 |
|||
- `ChatApiService.kt` - 核心 ChatAPI 服务实现 |
|||
- `StreamCallback.kt` - 流式回调接口 |
|||
|
|||
## 与 iOS 版本的一致性 |
|||
|
|||
Android 实现完全遵循 iOS 版本的接口定义: |
|||
|
|||
1. **初始化方法**: `initialize(apiKey, baseUrl, model, mcpServer)` |
|||
2. **消息创建**: `createUserMessage()`, `createAssistantMessage()` |
|||
3. **消息发送**: `sendMessage()` (非流式), `sendMessageStream()` (流式) |
|||
4. **函数注册**: `registerFunction()` |
|||
5. **MCP 相关**: 接口保留,功能暂时留空 |
|||
6. **流式事件**: 完全一致的事件类型和回调 |
|||
|
|||
## MCP 功能状态 |
|||
|
|||
MCP (Model Context Protocol) 相关功能在 Android 版本中暂时留空,包括: |
|||
|
|||
- `initializeMcpClient()` - 返回 false |
|||
- `isMcpInitialized()` - 返回 false |
|||
- `handleMcpToolCall()` - 返回占位符消息 |
|||
- 工具调用会通知上层但不会实际执行 |
|||
|
|||
## 使用示例 |
|||
|
|||
```kotlin |
|||
// 初始化 |
|||
chatApiService.initialize( |
|||
apiKey = "your-api-key", |
|||
baseUrl = "https://api.openai.com/v1/", |
|||
model = "gpt-3.5-turbo", |
|||
mcpServer = "" |
|||
) |
|||
|
|||
// 流式对话 |
|||
chatApiService.sendMessageStream(listOf( |
|||
mapOf("role" to "user", "content" to "Hello!") |
|||
)) |
|||
``` |
|||
|
|||
## 注意事项 |
|||
|
|||
1. 需要网络权限 `INTERNET` 和 `ACCESS_NETWORK_STATE` |
|||
2. 所有网络请求在后台线程执行 |
|||
3. 流式回调在主线程触发 |
|||
4. 支持取消正在进行的请求 |
|||
@ -0,0 +1,259 @@ |
|||
package com.yunqiinnovation.chat_api |
|||
|
|||
import android.util.Log |
|||
import io.ktor.client.* |
|||
import io.ktor.client.plugins.sse.* |
|||
import io.ktor.client.request.* |
|||
import io.ktor.client.statement.* |
|||
import io.ktor.http.* |
|||
import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage |
|||
import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport |
|||
import kotlinx.coroutines.* |
|||
import kotlinx.serialization.encodeToString |
|||
import kotlinx.serialization.json.Json |
|||
import kotlinx.serialization.decodeFromString |
|||
import kotlin.properties.Delegates |
|||
import kotlin.time.Duration |
|||
import java.util.concurrent.atomic.AtomicBoolean |
|||
|
|||
/** |
|||
* 自定义SSE客户端传输层,解决官方SseClientTransport的URL路径问题 |
|||
* 直接使用原始URL,不自动添加/sse后缀 |
|||
*/ |
|||
class CustomSseClientTransport( |
|||
private val client: HttpClient, |
|||
private val urlString: String?, |
|||
private val reconnectionTime: Duration? = null, |
|||
private val requestBuilder: HttpRequestBuilder.() -> Unit = {}, |
|||
) : AbstractTransport() { |
|||
private val TAG = "CustomSseClientTransport" |
|||
|
|||
private val scope by lazy { |
|||
CoroutineScope(session.coroutineContext + SupervisorJob()) |
|||
} |
|||
|
|||
private val initialized = AtomicBoolean(false) |
|||
private var session: ClientSSESession by Delegates.notNull() |
|||
private val endpoint = CompletableDeferred<String>() |
|||
|
|||
private var job: Job? = null |
|||
|
|||
// 创建JSON解析器 |
|||
private val json = Json { |
|||
ignoreUnknownKeys = true |
|||
isLenient = true |
|||
coerceInputValues = true |
|||
encodeDefaults = true |
|||
explicitNulls = false |
|||
} |
|||
|
|||
// URL解析结果 |
|||
private var hostPart: String = "" |
|||
private var pathPart: String = "" |
|||
private var queryParams: Map<String, String> = emptyMap() |
|||
|
|||
/** |
|||
* 解析URL,分离主机、路径和查询参数 |
|||
*/ |
|||
private fun parseUrl(url: String): Triple<String, String, Map<String, String>> { |
|||
return try { |
|||
val params = mutableMapOf<String, String>() |
|||
|
|||
var processedUrl = url.trim() |
|||
if (!processedUrl.startsWith("http://") && !processedUrl.startsWith("https://")) { |
|||
processedUrl = "https://$processedUrl" |
|||
} |
|||
|
|||
val urlObj = java.net.URL(processedUrl) |
|||
|
|||
// 解析查询参数 |
|||
if (urlObj.query != null) { |
|||
urlObj.query.split("&").forEach { param -> |
|||
val parts = param.split("=", limit = 2) |
|||
if (parts.size == 2) { |
|||
params[parts[0]] = parts[1] |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 构建主机部分URL |
|||
val port = if (urlObj.port == -1) "" else ":${urlObj.port}" |
|||
val hostUrl = "${urlObj.protocol}://${urlObj.host}$port" |
|||
|
|||
// 路径部分 |
|||
val path = urlObj.path |
|||
|
|||
Triple(hostUrl, path, params) |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "解析URL失败: $url, ${e.message}") |
|||
Triple(url, "", emptyMap()) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 收集SSE事件 |
|||
*/ |
|||
private suspend fun collectEvents() { |
|||
job = scope.launch(CoroutineName("CustomSseMcpClientTransport.collect#${hashCode()}")) { |
|||
session.incoming.collect { event -> |
|||
when (event.event) { |
|||
"error" -> { |
|||
val e = IllegalStateException("SSE error: ${event.data}") |
|||
Log.e(TAG, "SSE错误: ${event.data}") |
|||
_onError(e) |
|||
throw e |
|||
} |
|||
|
|||
"open" -> { |
|||
// SSE连接已打开 |
|||
} |
|||
|
|||
"endpoint" -> { |
|||
try { |
|||
val eventData = event.data ?: "" |
|||
|
|||
// 构建完整的端点URL |
|||
val fullEndpoint = if (eventData.contains(hostPart)) { |
|||
eventData |
|||
} else if (eventData.startsWith("/")) { |
|||
"$hostPart$eventData" |
|||
} else { |
|||
eventData |
|||
} |
|||
|
|||
// 添加查询参数 |
|||
val endpointWithParams = if (queryParams.isNotEmpty()) { |
|||
if (fullEndpoint.contains("?")) { |
|||
val queryString = queryParams.entries.joinToString("&") { "${it.key}=${it.value}" } |
|||
"$fullEndpoint&$queryString" |
|||
} else { |
|||
val queryString = queryParams.entries.joinToString("&") { "${it.key}=${it.value}" } |
|||
"$fullEndpoint?$queryString" |
|||
} |
|||
} else { |
|||
fullEndpoint |
|||
} |
|||
|
|||
endpoint.complete(endpointWithParams) |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "处理endpoint事件失败: ${e.message}", e) |
|||
_onError(e) |
|||
close() |
|||
error(e) |
|||
} |
|||
} |
|||
|
|||
else -> { |
|||
try { |
|||
val data = event.data |
|||
if (data != null) { |
|||
try { |
|||
val message = json.decodeFromString<JSONRPCMessage>(data) |
|||
_onMessage(message) |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "解析JSON-RPC消息失败: ${e.message}", e) |
|||
_onError(e) |
|||
} |
|||
} |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "处理事件失败: ${e.message}", e) |
|||
_onError(e) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 启动传输层 |
|||
*/ |
|||
override suspend fun start() { |
|||
if (!initialized.compareAndSet(false, true)) { |
|||
Log.e(TAG, "传输层已经启动,不能重复启动") |
|||
error("CustomSseClientTransport already started!") |
|||
} |
|||
|
|||
// 解析URL |
|||
if (urlString != null) { |
|||
val urlInfo = parseUrl(urlString) |
|||
hostPart = urlInfo.first |
|||
pathPart = urlInfo.second |
|||
queryParams = urlInfo.third |
|||
} |
|||
|
|||
// 创建SSE会话 - 直接使用原始URL |
|||
session = urlString?.let { |
|||
val sseConnectUrl = if (queryParams.isNotEmpty()) { |
|||
if (pathPart.contains("?")) { |
|||
"$hostPart$pathPart" |
|||
} else { |
|||
val queryString = queryParams.entries.joinToString("&") { "${it.key}=${it.value}" } |
|||
"$hostPart$pathPart?$queryString" |
|||
} |
|||
} else { |
|||
"$hostPart$pathPart" |
|||
} |
|||
|
|||
client.sseSession( |
|||
urlString = sseConnectUrl, |
|||
reconnectionTime = reconnectionTime, |
|||
block = requestBuilder, |
|||
) |
|||
} ?: client.sseSession( |
|||
reconnectionTime = reconnectionTime, |
|||
block = requestBuilder, |
|||
) |
|||
|
|||
// 收集SSE事件 |
|||
collectEvents() |
|||
|
|||
// 等待endpoint就绪 |
|||
endpoint.await() |
|||
} |
|||
|
|||
/** |
|||
* 发送消息 |
|||
*/ |
|||
@OptIn(ExperimentalCoroutinesApi::class) |
|||
override suspend fun send(message: JSONRPCMessage) { |
|||
if (!endpoint.isCompleted) { |
|||
Log.e(TAG, "发送失败: 未连接") |
|||
error("Not connected") |
|||
} |
|||
|
|||
try { |
|||
val messageEndpoint = endpoint.getCompleted() |
|||
|
|||
val jsonString = json.encodeToString(message) |
|||
|
|||
val response = client.post(messageEndpoint) { |
|||
headers.append(HttpHeaders.ContentType, ContentType.Application.Json.toString()) |
|||
setBody(jsonString) |
|||
} |
|||
|
|||
if (!response.status.isSuccess()) { |
|||
val text = response.bodyAsText() |
|||
Log.e(TAG, "发送消息失败: HTTP ${response.status}, $text") |
|||
error("Error POSTing to endpoint (HTTP ${response.status}): $text") |
|||
} |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "发送消息异常: ${e.message}", e) |
|||
_onError(e) |
|||
throw e |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 关闭传输层 |
|||
*/ |
|||
override suspend fun close() { |
|||
if (!initialized.get()) { |
|||
Log.e(TAG, "关闭失败: 传输层未初始化") |
|||
error("CustomSseClientTransport is not initialized!") |
|||
} |
|||
session.cancel() |
|||
_onClose() |
|||
job?.cancelAndJoin() |
|||
} |
|||
} |
|||
@ -0,0 +1,372 @@ |
|||
package com.yunqiinnovation.chat_api |
|||
|
|||
import android.content.Context |
|||
import android.util.Log |
|||
import kotlinx.coroutines.* |
|||
import org.json.JSONObject |
|||
import org.json.JSONArray |
|||
import io.ktor.client.* |
|||
|
|||
/** |
|||
* 工具类型枚举 |
|||
*/ |
|||
enum class ToolType { |
|||
LOCAL_FUNCTION, // 本地函数 |
|||
MCP_TOOL // MCP工具 |
|||
} |
|||
|
|||
/** |
|||
* 函数处理器接口 |
|||
*/ |
|||
interface FunctionHandler { |
|||
/** |
|||
* 处理函数调用 |
|||
* @param arguments 函数参数,Map格式 |
|||
* @return 函数执行结果,字符串 |
|||
*/ |
|||
suspend fun handle(arguments: Map<String, Any>): String |
|||
} |
|||
|
|||
/** |
|||
* MCP客户端 |
|||
* 与 iOS 版本 MCPClient 功能对等 |
|||
*/ |
|||
class MCPClient(private val context: Context? = null) : AutoCloseable { |
|||
|
|||
companion object { |
|||
private const val TAG = "MCPClient" |
|||
} |
|||
|
|||
// 本地函数Map,函数名 -> 处理器 |
|||
private val localFunctions = mutableMapOf<String, FunctionHandler>() |
|||
|
|||
// 本地函数定义Map,函数名 -> 定义 |
|||
private val localFunctionDefs = mutableMapOf<String, Map<String, Any>>() |
|||
|
|||
// 子客户端列表,每个连接一个MCP服务器 |
|||
private val subClients = mutableMapOf<String, MCPSubClient>() |
|||
|
|||
// 是否已连接 |
|||
private var isConnectedFlag = false |
|||
|
|||
// 共享的HttpClient,用于所有子客户端 |
|||
private val sharedHttpClient by lazy { createSslTrustAllClient() } |
|||
|
|||
init { |
|||
initializeSystemFunctions() |
|||
} |
|||
|
|||
/** |
|||
* 初始化系统函数 |
|||
*/ |
|||
private fun initializeSystemFunctions() { |
|||
try { |
|||
val handler = SystemFunctionHandler(context) |
|||
handler.registerAllFunctions(this) |
|||
} catch (e: Exception) { |
|||
Log.w(TAG, "Failed to initialize system functions", e) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 连接到SSE服务器 |
|||
* 直接接收完整的JSON配置字符串 |
|||
* |
|||
* @param mcpConfigJson 包含mcpServers字段的JSON配置字符串 |
|||
* @return 是否连接成功 |
|||
*/ |
|||
suspend fun connectToSSE(mcpConfigJson: String): Boolean { |
|||
// 清除现有连接 |
|||
closeAllConnections() |
|||
|
|||
return try { |
|||
val config = JSONObject(mcpConfigJson) |
|||
val mcpServers = config.optJSONObject("mcpServers") ?: return false |
|||
|
|||
var connectedCount = 0 |
|||
val serverIds = mcpServers.keys() |
|||
|
|||
while (serverIds.hasNext()) { |
|||
val serverId = serverIds.next() |
|||
val serverConfig = mcpServers.optJSONObject(serverId) ?: continue |
|||
|
|||
val url = serverConfig.optString("url", "") |
|||
if (url.isEmpty()) continue |
|||
|
|||
val subClient = MCPSubClient(serverId, url, sharedHttpClient) |
|||
|
|||
if (subClient.connect()) { |
|||
subClients[serverId] = subClient |
|||
connectedCount++ |
|||
} else { |
|||
Log.w(TAG, "Failed to connect to MCP server: $serverId") |
|||
} |
|||
} |
|||
|
|||
isConnectedFlag = connectedCount > 0 |
|||
connectedCount > 0 |
|||
|
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "Failed to connect to SSE", e) |
|||
false |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 关闭所有连接 |
|||
*/ |
|||
private fun closeAllConnections() { |
|||
subClients.forEach { (serverId, client) -> |
|||
try { |
|||
client.close() |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "关闭子客户端 [$serverId] 失败: ${e.message}") |
|||
} |
|||
} |
|||
subClients.clear() |
|||
isConnectedFlag = false |
|||
} |
|||
|
|||
/** |
|||
* 注册函数(简单版本,用于兼容) |
|||
*/ |
|||
fun registerFunction(name: String, handler: FunctionHandler) { |
|||
localFunctions[name] = handler |
|||
} |
|||
|
|||
/** |
|||
* 注册本地函数 |
|||
*/ |
|||
fun registerLocalFunction( |
|||
name: String, |
|||
description: String, |
|||
parameters: Any, |
|||
handler: FunctionHandler |
|||
): Boolean { |
|||
try { |
|||
// 将参数统一转换为Map格式 |
|||
val parametersMap: Map<String, Any> = when (parameters) { |
|||
is Map<*, *> -> { |
|||
@Suppress("UNCHECKED_CAST") |
|||
parameters as Map<String, Any> |
|||
} |
|||
is JSONObject -> { |
|||
convertJsonObjectToMap(parameters) |
|||
} |
|||
else -> { |
|||
Log.e(TAG, "参数类型不支持: ${parameters.javaClass.name}") |
|||
return false |
|||
} |
|||
} |
|||
|
|||
// 检查参数是否包含必要字段 |
|||
if (!parametersMap.containsKey("type") || (parametersMap["type"] != "object")) { |
|||
Log.e(TAG, "参数必须是object类型") |
|||
return false |
|||
} |
|||
|
|||
localFunctions[name] = handler |
|||
|
|||
val functionDef = mapOf( |
|||
"name" to name, |
|||
"description" to description, |
|||
"parameters" to parametersMap |
|||
) |
|||
localFunctionDefs[name] = functionDef |
|||
|
|||
return true |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "注册本地函数失败: ${e.message}", e) |
|||
return false |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 注销本地函数 |
|||
*/ |
|||
fun unregisterLocalFunction(name: String): Boolean { |
|||
val removed = localFunctions.remove(name) != null |
|||
if (removed) { |
|||
localFunctionDefs.remove(name) |
|||
} |
|||
return removed |
|||
} |
|||
|
|||
/** |
|||
* 获取工具映射列表 |
|||
*/ |
|||
fun getToolMaps(): List<Map<String, Any>> { |
|||
val allToolMaps = mutableListOf<Map<String, Any>>() |
|||
|
|||
// 添加本地函数 |
|||
for (functionDef in localFunctionDefs.values) { |
|||
allToolMaps.add(mapOf( |
|||
"type" to "function", |
|||
"function" to functionDef |
|||
)) |
|||
} |
|||
|
|||
// 添加MCP工具 |
|||
for (client in subClients.values) { |
|||
allToolMaps.addAll(client.getToolMaps()) |
|||
} |
|||
|
|||
return allToolMaps |
|||
} |
|||
|
|||
/** |
|||
* 获取工具类型 |
|||
*/ |
|||
fun getToolType(name: String): ToolType? { |
|||
if (localFunctions.containsKey(name)) { |
|||
return ToolType.LOCAL_FUNCTION |
|||
} |
|||
|
|||
if (subClients.values.any { it.containsTool(name) }) { |
|||
return ToolType.MCP_TOOL |
|||
} |
|||
|
|||
return null |
|||
} |
|||
|
|||
/** |
|||
* 调用工具 |
|||
*/ |
|||
suspend fun callTool(name: String, arguments: Map<String, Any>): Map<String, Any>? { |
|||
// 首先检查本地函数 |
|||
localFunctions[name]?.let { handler -> |
|||
return try { |
|||
val result = handler.handle(arguments) |
|||
mapOf("context" to result) |
|||
} catch (e: Exception) { |
|||
mapOf( |
|||
"content" to listOf(mapOf( |
|||
"type" to "text", |
|||
"text" to "Function call failed: ${e.message}" |
|||
)), |
|||
"isError" to true |
|||
) |
|||
} |
|||
} |
|||
|
|||
// 然后检查MCP工具 |
|||
for (client in subClients.values) { |
|||
if (client.containsTool(name)) { |
|||
return client.callTool(name, arguments) |
|||
} |
|||
} |
|||
|
|||
// 工具未找到 |
|||
return mapOf( |
|||
"content" to listOf(mapOf( |
|||
"type" to "text", |
|||
"text" to "Tool not found: $name" |
|||
)), |
|||
"isError" to true |
|||
) |
|||
} |
|||
|
|||
/** |
|||
* 检查是否有指定名称的工具 |
|||
*/ |
|||
fun hasToolWithName(name: String): Boolean { |
|||
return localFunctions.containsKey(name) || |
|||
subClients.values.any { it.containsTool(name) } |
|||
} |
|||
|
|||
/** |
|||
* 解析JSON参数 |
|||
*/ |
|||
fun parseJsonArguments(json: String): Map<String, Any> { |
|||
return try { |
|||
// 处理空字符串或空白字符串 |
|||
val trimmedJson = json.trim() |
|||
if (trimmedJson.isEmpty()) { |
|||
return emptyMap() |
|||
} |
|||
|
|||
// 如果不是以{开头,尝试包装为{} |
|||
val jsonToUse = if (!trimmedJson.startsWith("{")) { |
|||
if (trimmedJson.contains("=") || trimmedJson.contains(":")) { |
|||
// 简单的键值对,包装成JSON对象 |
|||
"{$trimmedJson}" |
|||
} else { |
|||
// 空参数,返回空对象 |
|||
"{}" |
|||
} |
|||
} else { |
|||
trimmedJson |
|||
} |
|||
|
|||
val jsonObject = JSONObject(jsonToUse) |
|||
convertJsonObjectToMap(jsonObject) |
|||
} catch (e: Exception) { |
|||
Log.w(TAG, "Failed to parse JSON arguments: '$json'", e) |
|||
emptyMap() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 是否已连接 |
|||
*/ |
|||
fun isConnected(): Boolean { |
|||
return isConnectedFlag || localFunctions.isNotEmpty() |
|||
} |
|||
|
|||
/** |
|||
* 断开所有连接 |
|||
*/ |
|||
suspend fun disconnectAll() { |
|||
closeAllConnections() |
|||
} |
|||
|
|||
/** |
|||
* 关闭连接 |
|||
*/ |
|||
override fun close() { |
|||
runBlocking { |
|||
closeAllConnections() |
|||
localFunctions.clear() |
|||
localFunctionDefs.clear() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 将JSONObject转换为Map |
|||
*/ |
|||
private fun convertJsonObjectToMap(jsonObject: JSONObject): Map<String, Any> { |
|||
val map = mutableMapOf<String, Any>() |
|||
|
|||
val keys = jsonObject.keys() |
|||
while (keys.hasNext()) { |
|||
val key = keys.next() |
|||
val value = jsonObject.get(key) |
|||
|
|||
map[key] = when (value) { |
|||
is JSONObject -> convertJsonObjectToMap(value) |
|||
is JSONArray -> convertJsonArrayToList(value) |
|||
else -> value |
|||
} |
|||
} |
|||
|
|||
return map |
|||
} |
|||
|
|||
/** |
|||
* 将JSONArray转换为List |
|||
*/ |
|||
private fun convertJsonArrayToList(jsonArray: JSONArray): List<Any> { |
|||
val list = mutableListOf<Any>() |
|||
|
|||
for (i in 0 until jsonArray.length()) { |
|||
val value = jsonArray.get(i) |
|||
list.add(when (value) { |
|||
is JSONObject -> convertJsonObjectToMap(value) |
|||
is JSONArray -> convertJsonArrayToList(value) |
|||
else -> value |
|||
}) |
|||
} |
|||
|
|||
return list |
|||
} |
|||
} |
|||
@ -0,0 +1,297 @@ |
|||
package com.yunqiinnovation.chat_api |
|||
|
|||
import android.util.Log |
|||
import io.ktor.client.* |
|||
import io.modelcontextprotocol.kotlin.sdk.* |
|||
import io.modelcontextprotocol.kotlin.sdk.client.* |
|||
import io.modelcontextprotocol.kotlin.sdk.shared.* |
|||
import io.modelcontextprotocol.kotlin.sdk.CallToolRequest |
|||
import io.modelcontextprotocol.kotlin.sdk.Tool |
|||
import kotlinx.coroutines.* |
|||
import kotlinx.coroutines.sync.Mutex |
|||
import kotlinx.coroutines.sync.withLock |
|||
import kotlinx.serialization.json.* |
|||
import kotlin.collections.mutableMapOf |
|||
import kotlin.collections.mutableListOf |
|||
|
|||
/** |
|||
* MCP子客户端,管理单个MCP服务器的连接 |
|||
* 使用MCP SDK 0.5.0的官方API实现真实连接 |
|||
*/ |
|||
class MCPSubClient( |
|||
private val serverId: String, |
|||
private val serverUrl: String, |
|||
private val httpClient: HttpClient? = null |
|||
) : AutoCloseable { |
|||
|
|||
companion object { |
|||
private const val TAG = "MCPSubClient" |
|||
} |
|||
|
|||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) |
|||
private val connectionMutex = Mutex() |
|||
private var mcpClient: Client? = null |
|||
private var isConnected = false |
|||
private var availableTools = mutableListOf<Tool>() |
|||
|
|||
/** |
|||
* 连接到MCP服务器 |
|||
*/ |
|||
suspend fun connect(): Boolean = connectionMutex.withLock { |
|||
if (isConnected) return true |
|||
|
|||
return try { |
|||
// 创建MCP客户端实例 |
|||
val client = Client( |
|||
clientInfo = Implementation( |
|||
name = "deep-voice-chat-api", |
|||
version = "1.0.0" |
|||
) |
|||
) |
|||
|
|||
// 根据URL类型选择传输方式 |
|||
val transport = when { |
|||
serverUrl.startsWith("http://") || serverUrl.startsWith("https://") -> { |
|||
// SSE传输 - 使用自定义的CustomSseClientTransport |
|||
val mcpHttpClient = httpClient ?: createMcpHttpClient() |
|||
CustomSseClientTransport( |
|||
client = mcpHttpClient, |
|||
urlString = serverUrl |
|||
) |
|||
} |
|||
else -> { |
|||
Log.e(TAG, "[$serverId] 不支持的服务器URL格式: $serverUrl") |
|||
return false |
|||
} |
|||
} |
|||
|
|||
// 连接到服务器 |
|||
client.connect(transport) |
|||
|
|||
// 获取可用工具列表 |
|||
try { |
|||
val toolsResult = client.listTools() |
|||
if (toolsResult != null) { |
|||
availableTools.clear() |
|||
availableTools.addAll(toolsResult.tools) |
|||
} |
|||
} catch (e: Exception) { |
|||
Log.w(TAG, "[$serverId] 获取工具列表失败: ${e.message}") |
|||
// 即使获取工具失败,连接也可能是成功的 |
|||
} |
|||
|
|||
mcpClient = client |
|||
isConnected = true |
|||
true |
|||
|
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "[$serverId] MCP连接失败: ${e.message}", e) |
|||
false |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 检查是否包含指定工具 |
|||
*/ |
|||
fun containsTool(name: String): Boolean { |
|||
return availableTools.any { it.name == name } |
|||
} |
|||
|
|||
/** |
|||
* 获取工具映射列表 |
|||
*/ |
|||
fun getToolMaps(): List<Map<String, Any>> { |
|||
val toolMaps = availableTools.map { tool -> |
|||
val parametersMap = tool.inputSchema?.let { inputSchema -> |
|||
convertInputSchemaToMap(inputSchema) |
|||
} ?: mapOf( |
|||
"type" to "object", |
|||
"properties" to emptyMap<String, Any>(), |
|||
"required" to emptyList<String>() |
|||
) |
|||
|
|||
val toolMap = mapOf( |
|||
"type" to "function", |
|||
"function" to mapOf( |
|||
"name" to tool.name, |
|||
"description" to (tool.description ?: ""), |
|||
"parameters" to parametersMap |
|||
) |
|||
) |
|||
|
|||
toolMap |
|||
} |
|||
|
|||
return toolMaps |
|||
} |
|||
|
|||
/** |
|||
* 调用MCP工具 |
|||
*/ |
|||
suspend fun callTool(name: String, arguments: Map<String, Any>): Map<String, Any>? { |
|||
val client = mcpClient ?: return null |
|||
|
|||
return try { |
|||
// 创建工具调用请求 - 将Map转换为JsonObject |
|||
val argumentsJson = kotlinx.serialization.json.buildJsonObject { |
|||
arguments.forEach { (key, value) -> |
|||
when (value) { |
|||
is String -> put(key, value) |
|||
is Number -> put(key, kotlinx.serialization.json.JsonPrimitive(value)) |
|||
is Boolean -> put(key, value) |
|||
else -> put(key, value.toString()) |
|||
} |
|||
} |
|||
} |
|||
|
|||
val request = CallToolRequest( |
|||
name = name, |
|||
arguments = argumentsJson |
|||
) |
|||
|
|||
// 调用工具 |
|||
val result = client.callTool(request) |
|||
|
|||
result?.let { callResult -> |
|||
// 将结果转换为统一格式 |
|||
val contentList = callResult.content.map { contentItem -> |
|||
// 根据不同的内容类型处理 |
|||
mapOf( |
|||
"type" to "text", |
|||
"text" to (contentItem.toString()) |
|||
) |
|||
} |
|||
|
|||
mapOf( |
|||
"content" to contentList, |
|||
"isError" to (callResult.isError ?: false) |
|||
) |
|||
} |
|||
|
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "[$serverId] 调用工具 '$name' 失败: ${e.message}", e) |
|||
mapOf( |
|||
"content" to listOf(mapOf( |
|||
"type" to "text", |
|||
"text" to "Tool call failed: ${e.message}" |
|||
)), |
|||
"isError" to true |
|||
) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 将Tool.Input转换为Map格式,供OpenAI使用 |
|||
*/ |
|||
private fun convertInputSchemaToMap(inputSchema: Tool.Input): Map<String, Any> { |
|||
val properties = mutableMapOf<String, Any>() |
|||
val required = mutableListOf<String>() |
|||
|
|||
// 处理properties |
|||
inputSchema.properties?.let { propsJsonObject -> |
|||
for ((key, value) in propsJsonObject) { |
|||
when (value) { |
|||
is JsonPrimitive -> { |
|||
if (value.isString) { |
|||
properties[key] = mapOf("type" to value.content) |
|||
} else { |
|||
properties[key] = value.content |
|||
} |
|||
} |
|||
is JsonObject -> { |
|||
properties[key] = convertJsonObjectToMap(value) |
|||
} |
|||
else -> { |
|||
Log.w(TAG, "未知的属性值类型: ${value::class.java.simpleName}") |
|||
properties[key] = value.toString() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 处理required |
|||
inputSchema.required?.let { requiredList -> |
|||
required.addAll(requiredList) |
|||
} |
|||
|
|||
val result = mapOf( |
|||
"type" to "object", |
|||
"properties" to properties, |
|||
"required" to required |
|||
) |
|||
|
|||
return result |
|||
} |
|||
|
|||
/** |
|||
* 将JsonObject转换为Map |
|||
*/ |
|||
private fun convertJsonObjectToMap(jsonObject: JsonObject): Map<String, Any> { |
|||
val map = mutableMapOf<String, Any>() |
|||
|
|||
for ((key, value) in jsonObject) { |
|||
map[key] = when (value) { |
|||
is JsonPrimitive -> { |
|||
when { |
|||
value.isString -> value.content |
|||
value.booleanOrNull != null -> value.boolean |
|||
value.longOrNull != null -> value.long |
|||
value.doubleOrNull != null -> value.double |
|||
else -> value.toString() |
|||
} |
|||
} |
|||
is JsonObject -> convertJsonObjectToMap(value) |
|||
is JsonArray -> value.map { element -> |
|||
when (element) { |
|||
is JsonPrimitive -> element.content |
|||
is JsonObject -> convertJsonObjectToMap(element) |
|||
else -> element.toString() |
|||
} |
|||
} |
|||
else -> value.toString() |
|||
} |
|||
} |
|||
|
|||
return map |
|||
} |
|||
|
|||
/** |
|||
* 刷新工具列表 |
|||
*/ |
|||
suspend fun refreshTools(): Boolean { |
|||
val client = mcpClient ?: return false |
|||
|
|||
return try { |
|||
val toolsResult = client.listTools() |
|||
if (toolsResult != null) { |
|||
availableTools.clear() |
|||
availableTools.addAll(toolsResult.tools) |
|||
true |
|||
} else { |
|||
false |
|||
} |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "[$serverId] 刷新工具列表失败: ${e.message}", e) |
|||
false |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 关闭连接 |
|||
*/ |
|||
override fun close() { |
|||
scope.launch { |
|||
connectionMutex.withLock { |
|||
try { |
|||
mcpClient?.close() |
|||
mcpClient = null |
|||
isConnected = false |
|||
availableTools.clear() |
|||
} catch (e: Exception) { |
|||
Log.e(TAG, "[$serverId] 关闭MCP连接时出错: ${e.message}", e) |
|||
} |
|||
} |
|||
} |
|||
scope.cancel() |
|||
} |
|||
} |
|||
@ -0,0 +1,471 @@ |
|||
package com.yunqiinnovation.chat_api |
|||
|
|||
import android.content.Context |
|||
import android.content.Intent |
|||
import android.content.pm.PackageManager |
|||
import android.location.LocationManager |
|||
import android.net.Uri |
|||
import android.os.Build |
|||
import android.provider.CalendarContract |
|||
import android.telephony.SmsManager |
|||
import androidx.core.content.ContextCompat |
|||
import java.text.SimpleDateFormat |
|||
import java.util.* |
|||
|
|||
/** |
|||
* 系统函数处理器 |
|||
* 提供内置的系统函数,与 iOS 版本保持一致 |
|||
*/ |
|||
class SystemFunctionHandler(private val context: Context? = null) { |
|||
|
|||
/** |
|||
* 注册所有系统函数 |
|||
*/ |
|||
fun registerAllFunctions(client: MCPClient) { |
|||
// 注册退出交互函数 |
|||
client.registerLocalFunction( |
|||
name = "exit_interaction", |
|||
description = "结束当前交互", |
|||
parameters = mapOf( |
|||
"type" to "object", |
|||
"properties" to emptyMap<String, Any>(), |
|||
"required" to emptyList<String>() |
|||
), |
|||
handler = ExitInteractionHandler(context) |
|||
) |
|||
|
|||
// 注册翻译模式函数 |
|||
client.registerLocalFunction( |
|||
name = "enter_translation_mode", |
|||
description = "用户请求进入实时翻译模式时,启动实时翻译功能", |
|||
parameters = mapOf( |
|||
"type" to "object", |
|||
"properties" to emptyMap<String, Any>(), |
|||
"required" to emptyList<String>() |
|||
), |
|||
handler = TranslationModeHandler(context) |
|||
) |
|||
|
|||
// 注册发送短信函数 |
|||
client.registerLocalFunction( |
|||
name = "send_text_message", |
|||
description = "发送短信", |
|||
parameters = mapOf( |
|||
"type" to "object", |
|||
"properties" to mapOf( |
|||
"contact" to mapOf( |
|||
"type" to "string", |
|||
"description" to "联系人姓名或电话号码" |
|||
), |
|||
"message" to mapOf( |
|||
"type" to "string", |
|||
"description" to "短信内容" |
|||
) |
|||
), |
|||
"required" to listOf("contact", "message") |
|||
), |
|||
handler = SendTextMessageHandler(context) |
|||
) |
|||
|
|||
// 注册拨打电话函数 |
|||
client.registerLocalFunction( |
|||
name = "make_phone_call", |
|||
description = "拨打电话", |
|||
parameters = mapOf( |
|||
"type" to "object", |
|||
"properties" to mapOf( |
|||
"contact" to mapOf( |
|||
"type" to "string", |
|||
"description" to "联系人姓名或电话号码" |
|||
) |
|||
), |
|||
"required" to listOf("contact") |
|||
), |
|||
handler = MakePhoneCallHandler(context) |
|||
) |
|||
|
|||
// 注册设置提醒函数 |
|||
client.registerLocalFunction( |
|||
name = "set_reminder", |
|||
description = "设置提醒事项", |
|||
parameters = mapOf( |
|||
"type" to "object", |
|||
"properties" to mapOf( |
|||
"title" to mapOf( |
|||
"type" to "string", |
|||
"description" to "提醒标题" |
|||
), |
|||
"content" to mapOf( |
|||
"type" to "string", |
|||
"description" to "提醒内容" |
|||
), |
|||
"time" to mapOf( |
|||
"type" to "string", |
|||
"description" to "提醒时间,格式为'yyyy-MM-dd HH:mm',如'2023-12-31 14:30'" |
|||
) |
|||
), |
|||
"required" to listOf("title", "time") |
|||
), |
|||
handler = SetReminderHandler(context) |
|||
) |
|||
|
|||
// 注册获取当前时间函数 |
|||
client.registerLocalFunction( |
|||
name = "get_current_time", |
|||
description = "获取当前日期和时间", |
|||
parameters = mapOf( |
|||
"type" to "object", |
|||
"properties" to mapOf( |
|||
"format" to mapOf( |
|||
"type" to "string", |
|||
"description" to "时间格式,可选,默认为标准格式" |
|||
) |
|||
), |
|||
"required" to emptyList<String>() |
|||
), |
|||
handler = GetCurrentTimeHandler() |
|||
) |
|||
|
|||
// 注册获取当前位置函数 |
|||
client.registerLocalFunction( |
|||
name = "get_current_location", |
|||
description = "获取当前地理位置", |
|||
parameters = mapOf( |
|||
"type" to "object", |
|||
"properties" to emptyMap<String, Any>(), |
|||
"required" to emptyList<String>() |
|||
), |
|||
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_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", |
|||
description = "打开系统录音机并开始录音", |
|||
parameters = mapOf( |
|||
"type" to "object", |
|||
"properties" to emptyMap<String, Any>(), |
|||
"required" to emptyList<String>() |
|||
), |
|||
handler = OpenRecorderHandler(context) |
|||
) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 退出交互处理器 |
|||
*/ |
|||
private class ExitInteractionHandler(private val context: Context?) : FunctionHandler { |
|||
override suspend fun handle(arguments: Map<String, Any>): String { |
|||
// 发送广播通知退出交互 |
|||
context?.sendBroadcast(Intent("com.yunqiinnovation.deepsound.EXIT_INTERACTION")) |
|||
return "{\"result\": \"已结束当前交互\"}" |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 翻译模式处理器 |
|||
*/ |
|||
private class TranslationModeHandler(private val context: Context?) : FunctionHandler { |
|||
override suspend fun handle(arguments: Map<String, Any>): String { |
|||
// 发送广播通知进入翻译模式 |
|||
context?.sendBroadcast(Intent("com.yunqiinnovation.deepsound.ENTER_TRANSLATION_MODE")) |
|||
return "{\"result\": \"已进入翻译模式\"}" |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 发送短信处理器 |
|||
*/ |
|||
private class SendTextMessageHandler(private val context: Context?) : FunctionHandler { |
|||
override suspend fun handle(arguments: Map<String, Any>): String { |
|||
val contact = arguments["contact"] as? String |
|||
val message = arguments["message"] as? String |
|||
|
|||
if (contact == null || message == null) { |
|||
return "{\"result\": \"缺少必要参数\"}" |
|||
} |
|||
|
|||
return try { |
|||
val smsIntent = Intent(Intent.ACTION_SENDTO).apply { |
|||
data = Uri.parse("smsto:$contact") |
|||
putExtra("sms_body", message) |
|||
flags = Intent.FLAG_ACTIVITY_NEW_TASK |
|||
} |
|||
|
|||
if (context?.packageManager?.queryIntentActivities(smsIntent, 0)?.isNotEmpty() == true) { |
|||
context.startActivity(smsIntent) |
|||
"{\"result\": \"已打开短信应用\"}" |
|||
} else { |
|||
"{\"result\": \"无法打开短信应用\"}" |
|||
} |
|||
} catch (e: Exception) { |
|||
"{\"result\": \"发送短信失败:${e.message}\"}" |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 拨打电话处理器 |
|||
*/ |
|||
private class MakePhoneCallHandler(private val context: Context?) : FunctionHandler { |
|||
override suspend fun handle(arguments: Map<String, Any>): String { |
|||
val contact = arguments["contact"] as? String |
|||
|
|||
if (contact == null) { |
|||
return "{\"result\": \"缺少联系人参数\"}" |
|||
} |
|||
|
|||
return try { |
|||
val callIntent = Intent(Intent.ACTION_DIAL).apply { |
|||
data = Uri.parse("tel:$contact") |
|||
flags = Intent.FLAG_ACTIVITY_NEW_TASK |
|||
} |
|||
|
|||
if (context?.packageManager?.queryIntentActivities(callIntent, 0)?.isNotEmpty() == true) { |
|||
context.startActivity(callIntent) |
|||
"{\"result\": \"已发起电话呼叫\"}" |
|||
} else { |
|||
"{\"result\": \"无法拨打电话\"}" |
|||
} |
|||
} catch (e: Exception) { |
|||
"{\"result\": \"拨打电话失败:${e.message}\"}" |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 设置提醒处理器 |
|||
*/ |
|||
private class SetReminderHandler(private val context: Context?) : FunctionHandler { |
|||
override suspend fun handle(arguments: Map<String, Any>): String { |
|||
val title = arguments["title"] as? String |
|||
val content = arguments["content"] as? String ?: "" |
|||
val timeString = arguments["time"] as? String |
|||
|
|||
if (title == null || timeString == null) { |
|||
return "{\"result\": \"缺少必要参数\"}" |
|||
} |
|||
|
|||
return try { |
|||
// 解析时间 |
|||
val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()) |
|||
val date = formatter.parse(timeString) |
|||
|
|||
if (date == null) { |
|||
return "{\"result\": \"时间格式错误\"}" |
|||
} |
|||
|
|||
// 创建日历事件 |
|||
val calendarIntent = Intent(Intent.ACTION_INSERT).apply { |
|||
data = CalendarContract.Events.CONTENT_URI |
|||
putExtra(CalendarContract.Events.TITLE, title) |
|||
putExtra(CalendarContract.Events.DESCRIPTION, content) |
|||
putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, date.time) |
|||
putExtra(CalendarContract.EXTRA_EVENT_END_TIME, date.time + 60 * 60 * 1000) // 默认1小时 |
|||
putExtra(CalendarContract.Events.HAS_ALARM, 1) |
|||
flags = Intent.FLAG_ACTIVITY_NEW_TASK |
|||
} |
|||
|
|||
if (context?.packageManager?.queryIntentActivities(calendarIntent, 0)?.isNotEmpty() == true) { |
|||
context.startActivity(calendarIntent) |
|||
"{\"result\": \"提醒设置成功\"}" |
|||
} else { |
|||
"{\"result\": \"无法打开日历应用\"}" |
|||
} |
|||
} catch (e: Exception) { |
|||
"{\"result\": \"设置提醒失败:${e.message}\"}" |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取当前时间处理器 |
|||
*/ |
|||
private class GetCurrentTimeHandler : FunctionHandler { |
|||
override suspend fun handle(arguments: Map<String, Any>): String { |
|||
val format = arguments["format"] as? String |
|||
|
|||
return try { |
|||
val formatter = if (!format.isNullOrEmpty()) { |
|||
SimpleDateFormat(format, Locale.getDefault()) |
|||
} else { |
|||
SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss", Locale.CHINA) |
|||
} |
|||
|
|||
val currentTime = formatter.format(Date()) |
|||
"{\"result\": \"$currentTime\", \"time\": \"$currentTime\"}" |
|||
} catch (e: Exception) { |
|||
"{\"result\": \"获取时间失败:${e.message}\"}" |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取当前位置处理器 |
|||
*/ |
|||
private class GetCurrentLocationHandler(private val context: Context?) : FunctionHandler { |
|||
override suspend fun handle(arguments: Map<String, Any>): String { |
|||
if (context == null) { |
|||
return "{\"result\": \"上下文未初始化\", \"success\": false}" |
|||
} |
|||
|
|||
// 检查位置权限 |
|||
val hasPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |
|||
ContextCompat.checkSelfPermission( |
|||
context, |
|||
android.Manifest.permission.ACCESS_FINE_LOCATION |
|||
) == PackageManager.PERMISSION_GRANTED |
|||
} else { |
|||
true |
|||
} |
|||
|
|||
if (!hasPermission) { |
|||
return "{\"result\": \"位置权限被拒绝。请前往设置中启用位置权限\", \"success\": false}" |
|||
} |
|||
|
|||
// 检查位置服务是否启用 |
|||
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as? LocationManager |
|||
val isLocationEnabled = locationManager?.isProviderEnabled(LocationManager.GPS_PROVIDER) == true || |
|||
locationManager?.isProviderEnabled(LocationManager.NETWORK_PROVIDER) == true |
|||
|
|||
if (!isLocationEnabled) { |
|||
return "{\"result\": \"位置服务未启用。请前往设置中启用位置服务\", \"success\": false}" |
|||
} |
|||
|
|||
// 注意:实际的位置获取需要异步处理,这里只返回提示信息 |
|||
// 在实际应用中应该使用 LocationCallback 或 Coroutines 来获取实时位置 |
|||
return "{\"result\": \"需要通过位置服务获取当前位置\", \"success\": true}" |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 媒体播放处理器 |
|||
*/ |
|||
private class MediaPlayHandler(private val context: Context?) : FunctionHandler { |
|||
override suspend fun handle(arguments: Map<String, Any>): String { |
|||
// 发送媒体播放广播 |
|||
context?.sendBroadcast(Intent("com.yunqiinnovation.deepsound.MEDIA_PLAY")) |
|||
return "{\"result\": \"已开始播放媒体\"}" |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 媒体暂停处理器 |
|||
*/ |
|||
private class MediaPauseHandler(private val context: Context?) : FunctionHandler { |
|||
override suspend fun handle(arguments: Map<String, Any>): String { |
|||
// 发送媒体暂停广播 |
|||
context?.sendBroadcast(Intent("com.yunqiinnovation.deepsound.MEDIA_PAUSE")) |
|||
return "{\"result\": \"已暂停媒体播放\"}" |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 媒体上一首处理器 |
|||
*/ |
|||
private class MediaPreviousHandler(private val context: Context?) : FunctionHandler { |
|||
override suspend fun handle(arguments: Map<String, Any>): String { |
|||
// 发送切换上一首广播 |
|||
context?.sendBroadcast(Intent("com.yunqiinnovation.deepsound.MEDIA_PREVIOUS")) |
|||
return "{\"result\": \"已切换到上一首\"}" |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 媒体下一首处理器 |
|||
*/ |
|||
private class MediaNextHandler(private val context: Context?) : FunctionHandler { |
|||
override suspend fun handle(arguments: Map<String, Any>): String { |
|||
// 发送切换下一首广播 |
|||
context?.sendBroadcast(Intent("com.yunqiinnovation.deepsound.MEDIA_NEXT")) |
|||
return "{\"result\": \"已切换到下一首\"}" |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 打开录音机处理器 |
|||
*/ |
|||
private class OpenRecorderHandler(private val context: Context?) : FunctionHandler { |
|||
override suspend fun handle(arguments: Map<String, Any>): String { |
|||
if (context == null) { |
|||
return "{\"result\": \"上下文未初始化\"}" |
|||
} |
|||
|
|||
return try { |
|||
// 尝试打开录音机应用 |
|||
val recorderIntent = Intent(Intent.ACTION_MAIN).apply { |
|||
addCategory(Intent.CATEGORY_APP_MUSIC) |
|||
flags = Intent.FLAG_ACTIVITY_NEW_TASK |
|||
} |
|||
|
|||
// 或者尝试使用录音Intent |
|||
val recordIntent = Intent("android.provider.MediaStore.RECORD_SOUND") |
|||
recordIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK |
|||
|
|||
when { |
|||
context.packageManager?.queryIntentActivities(recordIntent, 0)?.isNotEmpty() == true -> { |
|||
context.startActivity(recordIntent) |
|||
"{\"result\": \"已打开录音机\"}" |
|||
} |
|||
context.packageManager?.queryIntentActivities(recorderIntent, 0)?.isNotEmpty() == true -> { |
|||
context.startActivity(recorderIntent) |
|||
"{\"result\": \"已打开音频应用\"}" |
|||
} |
|||
else -> { |
|||
"{\"result\": \"无法打开录音机应用\"}" |
|||
} |
|||
} |
|||
} catch (e: Exception) { |
|||
"{\"result\": \"打开录音机失败:${e.message}\"}" |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
package com.yunqiinnovation.chat_api |
|||
|
|||
import io.ktor.client.* |
|||
import io.ktor.client.engine.okhttp.* |
|||
import io.ktor.client.plugins.sse.* |
|||
import okhttp3.OkHttpClient |
|||
import okhttp3.logging.HttpLoggingInterceptor |
|||
import java.security.SecureRandom |
|||
import java.security.cert.X509Certificate |
|||
import java.util.concurrent.TimeUnit |
|||
import javax.net.ssl.SSLContext |
|||
import javax.net.ssl.TrustManager |
|||
import javax.net.ssl.X509TrustManager |
|||
|
|||
/** |
|||
* 创建一个信任所有SSL证书的HttpClient |
|||
* 仅用于开发环境,生产环境应该使用正确的证书验证 |
|||
* MCP SDK 0.5.0应该自动处理SSE相关功能 |
|||
*/ |
|||
fun createSslTrustAllClient(): HttpClient { |
|||
// 创建一个信任所有证书的TrustManager |
|||
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager { |
|||
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {} |
|||
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {} |
|||
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf() |
|||
}) |
|||
|
|||
// 创建SSL上下文并初始化它 |
|||
val sslContext = SSLContext.getInstance("TLS") |
|||
sslContext.init(null, trustAllCerts, SecureRandom()) |
|||
|
|||
// 创建OkHttpClient并配置信任所有证书 |
|||
val okHttpClient = OkHttpClient.Builder() |
|||
.sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager) |
|||
.hostnameVerifier { _, _ -> true } |
|||
.connectTimeout(30, TimeUnit.SECONDS) |
|||
.readTimeout(30, TimeUnit.SECONDS) |
|||
.build() |
|||
|
|||
// 创建使用OkHttp引擎的HttpClient |
|||
return HttpClient(OkHttp) { |
|||
engine { |
|||
preconfigured = okHttpClient |
|||
} |
|||
|
|||
// 安装SSE插件 - 这是关键! |
|||
install(SSE) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 创建一个专门用于MCP连接的HttpClient,包含SSE支持 |
|||
* 注意:这个版本尝试不安装SSE插件,让MCP SDK自行处理 |
|||
*/ |
|||
fun createMcpHttpClient(): HttpClient { |
|||
return createSslTrustAllClient() |
|||
} |
|||
Loading…
Reference in new issue