Browse Source

Merge branch 'dev' of https://git.onewaygame.cn/deepsound/deepvoice_client into lxm

newdev_shunjiawei
lxm 1 year ago
parent
commit
f9d2f0ec03
  1. 2
      lib/modules/agent/controllers/agent_controller.dart
  2. 20
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  3. 163
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt
  4. 65
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/mcp/CustomSseClientTransport.kt

2
lib/modules/agent/controllers/agent_controller.dart

@ -456,7 +456,7 @@ class AgentController extends GetxController {
final start = metaResult["card_navigation"]["start"] as String;
final end = metaResult["card_navigation"]["end"] as String;
// Logger.i(TAG, 'liwei-------- 打开导航页面: $start $end ');
Future.delayed(Duration(milliseconds: 1500), () {
Future.delayed(Duration(seconds: 2), () {
// Logger.i(TAG, 'liwei-------- 打开导航页面: $start $end ');
// 2秒后执行的代码
openNavigation(start, end, "");

20
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -662,7 +662,7 @@ object AgentService : CoroutineScope {
val responseBuilder = StringBuilder()
var aiMetadata:String = ""
var userMetadata = JSONObject()
var nobroadcast = false
// 如果有图片,添加图片信息到元数据
if (hasImage) {
userMetadata.put("hasImage", true)
@ -717,7 +717,7 @@ object AgentService : CoroutineScope {
callback = object : OpenAIService.StreamCallback {
override fun onToken(token: String) {
responseBuilder.append(token)
if (speakResponse) {
if (speakResponse && !nobroadcast) {
ttsService?.speakStream(token)
}
// 发送流式回复token
@ -748,8 +748,6 @@ object AgentService : CoroutineScope {
// 保存聊天记录
saveChatMessage(displayText, response,aiMetadata,userMetadata.toString())
}
// 标记AI流式输出已完成
@ -792,7 +790,9 @@ object AgentService : CoroutineScope {
"result" to functionCallResult.toString(),
))
Log.d(TAG, "mcp调用结果: $functionCallResult")
aiMetadata = autoHandleFcunCallResult(functionCallResult);
val (metestr, broadcast)= autoHandleFcunCallResult(functionCallResult);
aiMetadata = metestr;
nobroadcast = broadcast
}
}
)
@ -1108,8 +1108,9 @@ object AgentService : CoroutineScope {
/*
* 自动播放音乐
* */
fun autoHandleFcunCallResult(functionCallResult: JSONObject): String{
fun autoHandleFcunCallResult(functionCallResult: JSONObject): Pair<String, Boolean>{
val metaStr = functionCallResult.optString("meta")
var broadcast = false;
if (metaStr.isNotEmpty()) {
val meta = JSONObject(metaStr)
// Log.d(TAG, "mcp调用结果: $metaStr")
@ -1122,6 +1123,7 @@ object AgentService : CoroutineScope {
val sgener = cardMusic.optString("sgener", "")
val image = cardMusic.optString("image", "")
processMusicPlay(mapOf("id" to id,"url" to url, "title" to name, "artist" to sgener,"coverUrl" to image))
broadcast = true;
}else if (meta.has("card_musiclist")) { //音乐列表
val cardMusiclist = meta.getJSONObject("card_musiclist")
val musics = cardMusiclist.optJSONArray("musics")
@ -1138,6 +1140,7 @@ object AgentService : CoroutineScope {
}
if (playlist.isNotEmpty()) {
Log.w(TAG, "自动播放音乐列表 ${playlist}")
broadcast = true;
processMusicPlayList(playlist)
} else {
Log.w(TAG, "card_musiclist 中没有有效的音乐条目")
@ -1149,11 +1152,12 @@ object AgentService : CoroutineScope {
val cardNavigation = meta.getJSONObject("card_navigation")
val start = cardNavigation.optString("start", "")
val end = cardNavigation.optString("end", "")
broadcast = true;
processNavigation(start,end);
}
return metaStr;
return Pair(metaStr, broadcast);
}
return "";
return Pair("", false);
}
/**

163
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt

@ -33,6 +33,14 @@ import okhttp3.Interceptor
import io.ktor.client.request.*
import io.ktor.http.*
import com.yunqiinnovation.open_ai_service.mcp.CustomSseClientTransport
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okio.Buffer
/**
@ -68,11 +76,17 @@ class MCPSubClient(
// 保存服务器URL
private val serverUrl: String = serverUrl
init {
Log.d(TAG, "MCPSubClient初始化,URL: $serverUrl")
}
// 重连配置
private val maxRetryAttempts = 3
private val initialReconnectDelay = 1000L // 初始重连延迟1秒
private val maxReconnectDelay = 30000L // 最大重连延迟30秒
private var currentReconnectDelay = initialReconnectDelay
private var retryCount = 0
// 连接状态锁
private val connectionLock = Mutex()
// 初始化MCP客户端
private val mcp: Client = Client(
clientInfo = Implementation(
@ -95,7 +109,18 @@ class MCPSubClient(
// 工具名称到工具的映射,用于快速查找
private val toolMapping = mutableMapOf<String, ToolUnion>()
// 心跳检测
private var heartbeatJob: Job? = null
private val heartbeatInterval = 30000L // 30秒
// 网络状态监听
private var networkStateListener: NetworkStateListener? = null
init {
Log.d(TAG, "MCPSubClient初始化,URL: $serverUrl")
}
/**
* 将JsonObject转换为JsonValue
*/
@ -110,6 +135,10 @@ class MCPSubClient(
* @return 是否连接成功
*/
suspend fun connect(): Boolean {
if (isConnected) {
Log.d(TAG, "已经连接到服务器,跳过重复连接")
return true
}
try {
Log.d(TAG, "开始连接服务器: $serverUrl")
@ -130,6 +159,11 @@ class MCPSubClient(
processToolsList(toolsResult)
isConnected = true
retryCount = 0
currentReconnectDelay = initialReconnectDelay
// 启动心跳检测
startHeartbeat()
Log.d(TAG, "连接成功,可用工具: ${tools.size}个")
return true
} catch (e: Exception) {
@ -368,9 +402,11 @@ class MCPSubClient(
* 调用工具
*/
suspend fun callTool(name: String, arguments: Map<String, Any>): JSONObject? {
if (!isConnected) {
Log.e(TAG, "未连接到服务器")
return JSONObject().apply { put("context","未连接到MCP服务器") }
// 先检查连接状态
if (!checkConnection()) {
Log.e(TAG, "无法连接到服务器,工具调用失败")
return JSONObject().apply { put("context", "无法连接到MCP服务器") }
}
if (!containsTool(name)) {
@ -395,32 +431,123 @@ class MCPSubClient(
}
} catch (e: Exception) {
Log.e(TAG, "调用工具失败: ${e.message}")
isConnected = false
JSONObject().apply { put("context", "调用工具失败: ${e.message}") }
}
}
/**
* 设置网络状态监听器
*/
fun setNetworkStateListener(listener: NetworkStateListener) {
this.networkStateListener = listener
}
/**
* 检查连接状态并自动重连
*/
suspend fun checkConnection(): Boolean {
if (!isConnected) {
Log.d(TAG, "当前未连接,尝试重新连接...")
return connect()
}
try {
// 简单ping测试连接状态
mcp.ping()
return true
} catch (e: Exception) {
Log.e(TAG, "连接检查失败: ${e.message}")
isConnected = false
return false
}
}
/**
* 检查是否已连接
*/
fun isConnected(): Boolean {
return isConnected
}
/**
* 处理连接错误并尝试重连
*/
private suspend fun handleConnectionError(e: Exception) {
isConnected = false
stopHeartbeat()
if (retryCount < maxRetryAttempts) {
retryCount++
currentReconnectDelay = minOf(currentReconnectDelay * 2, maxReconnectDelay)
Log.w(TAG, "连接失败,将在 ${currentReconnectDelay}ms 后尝试重连 (尝试 $retryCount/$maxRetryAttempts)")
delay(currentReconnectDelay)
connect()
} else {
Log.e(TAG, "已达到最大重试次数($maxRetryAttempts),停止重连")
// 可以在这里添加通知或回调,告知上层连接彻底失败
}
}
/**
* 启动心跳检测
*/
private fun startHeartbeat() {
heartbeatJob?.cancel()
heartbeatJob = CoroutineScope(Dispatchers.IO).launch {
while (isActive && isConnected) {
try {
delay(heartbeatInterval)
// 发送心跳请求
val startTime = System.currentTimeMillis()
val response = mcp.ping()
val latency = System.currentTimeMillis() - startTime
Log.d(TAG, "心跳检测成功,延迟: ${latency}ms")
} catch (e: Exception) {
Log.e(TAG, "心跳检测失败: ${e.message}")
handleConnectionError(e)
break
}
}
}
}
/**
* 停止心跳检测
*/
private fun stopHeartbeat() {
heartbeatJob?.cancel()
heartbeatJob = null
}
/**
* 关闭连接
*/
override fun close() {
runBlocking {
try {
mcp.close()
sseTransport?.close()
isConnected = false
Log.d(TAG, "已关闭MCP服务器连接")
} catch (e: Exception) {
Log.e(TAG, "关闭MCP服务器连接失败: ${e.message}", e)
connectionLock.withLock {
try {
isConnected = false
stopHeartbeat()
mcp.close()
sseTransport?.close()
Log.d(TAG, "已关闭MCP服务器连接")
} catch (e: Exception) {
Log.e(TAG, "关闭MCP服务器连接失败: ${e.message}", e)
}
}
}
}
/**
* 网络状态监听接口
*/
interface NetworkStateListener {
fun onNetworkAvailable()
fun onNetworkLost()
}
}
/**

65
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/mcp/CustomSseClientTransport.kt

@ -19,41 +19,8 @@ import kotlin.properties.Delegates
import kotlin.time.Duration
import java.util.concurrent.atomic.AtomicBoolean
import org.json.JSONObject
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
//@Serializable
//data class JSONRPCRequest(
// val jsonrpc: String = "2.0",
// val method: String,
// val params: JsonElement? = null,
// val id: JsonPrimitive? = null
//) : JSONRPCMessage, Parcelable {
//
// constructor(parcel: Parcel) : this(
// parcel.readString() ?: "2.0",
// parcel.readString() ?: "",
// parcel.readString()?.let { Json.decodeFromString<JsonElement>(it) },
// parcel.readString()?.let { Json.decodeFromString<JsonPrimitive>(it) }
// )
//
// override fun writeToParcel(parcel: Parcel, flags: Int) {
// parcel.writeString(jsonrpc)
// parcel.writeString(method)
// parcel.writeString(params?.let { Json.encodeToString(it) })
// parcel.writeString(id?.let { Json.encodeToString(it) })
// }
//
// override fun describeContents(): Int = 0
//
// companion object CREATOR : Parcelable.Creator<JSONRPCRequest> {
// override fun createFromParcel(parcel: Parcel): JSONRPCRequest = JSONRPCRequest(parcel)
// override fun newArray(size: Int): Array<JSONRPCRequest?> = arrayOfNulls(size)
// }
//}
/**
* 自定义SSE客户端传输层,修复原始SseClientTransport中的URL拼接问题
* 解决URL查询参数与路径拼接错误的问题,确保消息端点URL格式正确
@ -77,11 +44,6 @@ class CustomSseClientTransport(
private var job: Job? = null
private val heartbeatIntervalMs = 30_000L // 30秒
private var heartbeatJob: Job? = null
// 创建JSON解析器,增强灵活性设置
private val json = Json {
ignoreUnknownKeys = true // 忽略未知字段
@ -341,9 +303,6 @@ class CustomSseClientTransport(
// 收集SSE事件
collectEvents()
// 启动心跳任务
startHeartbeat()
// 等待endpoint就绪
endpoint.await()
Log.d(TAG, "传输层启动完成,消息端点已就绪")
@ -391,7 +350,6 @@ class CustomSseClientTransport(
Log.e(TAG, "关闭失败: 传输层未初始化")
error("CustomSseClientTransport is not initialized!")
}
heartbeatJob?.cancel()
session.cancel()
_onClose()
job?.cancelAndJoin()
@ -404,29 +362,6 @@ class CustomSseClientTransport(
fun isInitialized(): Boolean {
return initialized.get()
}
private fun startHeartbeat() {
heartbeatJob = scope.launch {
while (isActive) {
delay(heartbeatIntervalMs)
try {
sendHeartbeat()
} catch (e: Exception) {
Log.e(TAG, "心跳发送失败: ${e.message}", e)
_onError(e)
}
}
}
}
private suspend fun sendHeartbeat() {
val pingMessage = JSONRPCRequest(
method = "ping",
jsonrpc = "2.0"
)
send(pingMessage)
}
}
Loading…
Cancel
Save