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 start = metaResult["card_navigation"]["start"] as String;
final end = metaResult["card_navigation"]["end"] as String; final end = metaResult["card_navigation"]["end"] as String;
// Logger.i(TAG, 'liwei-------- 打开导航页面: $start $end '); // Logger.i(TAG, 'liwei-------- 打开导航页面: $start $end ');
Future.delayed(Duration(milliseconds: 1500), () { Future.delayed(Duration(seconds: 2), () {
// Logger.i(TAG, 'liwei-------- 打开导航页面: $start $end '); // Logger.i(TAG, 'liwei-------- 打开导航页面: $start $end ');
// 2秒后执行的代码 // 2秒后执行的代码
openNavigation(start, end, ""); 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() val responseBuilder = StringBuilder()
var aiMetadata:String = "" var aiMetadata:String = ""
var userMetadata = JSONObject() var userMetadata = JSONObject()
var nobroadcast = false
// 如果有图片,添加图片信息到元数据 // 如果有图片,添加图片信息到元数据
if (hasImage) { if (hasImage) {
userMetadata.put("hasImage", true) userMetadata.put("hasImage", true)
@ -717,7 +717,7 @@ object AgentService : CoroutineScope {
callback = object : OpenAIService.StreamCallback { callback = object : OpenAIService.StreamCallback {
override fun onToken(token: String) { override fun onToken(token: String) {
responseBuilder.append(token) responseBuilder.append(token)
if (speakResponse) { if (speakResponse && !nobroadcast) {
ttsService?.speakStream(token) ttsService?.speakStream(token)
} }
// 发送流式回复token // 发送流式回复token
@ -748,8 +748,6 @@ object AgentService : CoroutineScope {
// 保存聊天记录 // 保存聊天记录
saveChatMessage(displayText, response,aiMetadata,userMetadata.toString()) saveChatMessage(displayText, response,aiMetadata,userMetadata.toString())
} }
// 标记AI流式输出已完成 // 标记AI流式输出已完成
@ -792,7 +790,9 @@ object AgentService : CoroutineScope {
"result" to functionCallResult.toString(), "result" to functionCallResult.toString(),
)) ))
Log.d(TAG, "mcp调用结果: $functionCallResult") 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") val metaStr = functionCallResult.optString("meta")
var broadcast = false;
if (metaStr.isNotEmpty()) { if (metaStr.isNotEmpty()) {
val meta = JSONObject(metaStr) val meta = JSONObject(metaStr)
// Log.d(TAG, "mcp调用结果: $metaStr") // Log.d(TAG, "mcp调用结果: $metaStr")
@ -1122,6 +1123,7 @@ object AgentService : CoroutineScope {
val sgener = cardMusic.optString("sgener", "") val sgener = cardMusic.optString("sgener", "")
val image = cardMusic.optString("image", "") val image = cardMusic.optString("image", "")
processMusicPlay(mapOf("id" to id,"url" to url, "title" to name, "artist" to sgener,"coverUrl" to 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")) { //音乐列表 }else if (meta.has("card_musiclist")) { //音乐列表
val cardMusiclist = meta.getJSONObject("card_musiclist") val cardMusiclist = meta.getJSONObject("card_musiclist")
val musics = cardMusiclist.optJSONArray("musics") val musics = cardMusiclist.optJSONArray("musics")
@ -1138,6 +1140,7 @@ object AgentService : CoroutineScope {
} }
if (playlist.isNotEmpty()) { if (playlist.isNotEmpty()) {
Log.w(TAG, "自动播放音乐列表 ${playlist}") Log.w(TAG, "自动播放音乐列表 ${playlist}")
broadcast = true;
processMusicPlayList(playlist) processMusicPlayList(playlist)
} else { } else {
Log.w(TAG, "card_musiclist 中没有有效的音乐条目") Log.w(TAG, "card_musiclist 中没有有效的音乐条目")
@ -1149,11 +1152,12 @@ object AgentService : CoroutineScope {
val cardNavigation = meta.getJSONObject("card_navigation") val cardNavigation = meta.getJSONObject("card_navigation")
val start = cardNavigation.optString("start", "") val start = cardNavigation.optString("start", "")
val end = cardNavigation.optString("end", "") val end = cardNavigation.optString("end", "")
broadcast = true;
processNavigation(start,end); 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.client.request.*
import io.ktor.http.* import io.ktor.http.*
import com.yunqiinnovation.open_ai_service.mcp.CustomSseClientTransport 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 import okio.Buffer
/** /**
@ -68,11 +76,17 @@ class MCPSubClient(
// 保存服务器URL // 保存服务器URL
private val serverUrl: String = serverUrl 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客户端 // 初始化MCP客户端
private val mcp: Client = Client( private val mcp: Client = Client(
clientInfo = Implementation( clientInfo = Implementation(
@ -95,7 +109,18 @@ class MCPSubClient(
// 工具名称到工具的映射,用于快速查找 // 工具名称到工具的映射,用于快速查找
private val toolMapping = mutableMapOf<String, ToolUnion>() 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 * 将JsonObject转换为JsonValue
*/ */
@ -110,6 +135,10 @@ class MCPSubClient(
* @return 是否连接成功 * @return 是否连接成功
*/ */
suspend fun connect(): Boolean { suspend fun connect(): Boolean {
if (isConnected) {
Log.d(TAG, "已经连接到服务器,跳过重复连接")
return true
}
try { try {
Log.d(TAG, "开始连接服务器: $serverUrl") Log.d(TAG, "开始连接服务器: $serverUrl")
@ -130,6 +159,11 @@ class MCPSubClient(
processToolsList(toolsResult) processToolsList(toolsResult)
isConnected = true isConnected = true
retryCount = 0
currentReconnectDelay = initialReconnectDelay
// 启动心跳检测
startHeartbeat()
Log.d(TAG, "连接成功,可用工具: ${tools.size}个") Log.d(TAG, "连接成功,可用工具: ${tools.size}个")
return true return true
} catch (e: Exception) { } catch (e: Exception) {
@ -368,9 +402,11 @@ class MCPSubClient(
* 调用工具 * 调用工具
*/ */
suspend fun callTool(name: String, arguments: Map<String, Any>): JSONObject? { 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)) { if (!containsTool(name)) {
@ -395,32 +431,123 @@ class MCPSubClient(
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "调用工具失败: ${e.message}") Log.e(TAG, "调用工具失败: ${e.message}")
isConnected = false
JSONObject().apply { put("context", "调用工具失败: ${e.message}") } 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 { fun isConnected(): Boolean {
return isConnected 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() { override fun close() {
runBlocking { runBlocking {
try { connectionLock.withLock {
mcp.close() try {
sseTransport?.close() isConnected = false
isConnected = false stopHeartbeat()
Log.d(TAG, "已关闭MCP服务器连接") mcp.close()
} catch (e: Exception) { sseTransport?.close()
Log.e(TAG, "关闭MCP服务器连接失败: ${e.message}", e) 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 kotlin.time.Duration
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import org.json.JSONObject 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拼接问题 * 自定义SSE客户端传输层,修复原始SseClientTransport中的URL拼接问题
* 解决URL查询参数与路径拼接错误的问题,确保消息端点URL格式正确 * 解决URL查询参数与路径拼接错误的问题,确保消息端点URL格式正确
@ -77,11 +44,6 @@ class CustomSseClientTransport(
private var job: Job? = null private var job: Job? = null
private val heartbeatIntervalMs = 30_000L // 30秒
private var heartbeatJob: Job? = null
// 创建JSON解析器,增强灵活性设置 // 创建JSON解析器,增强灵活性设置
private val json = Json { private val json = Json {
ignoreUnknownKeys = true // 忽略未知字段 ignoreUnknownKeys = true // 忽略未知字段
@ -341,9 +303,6 @@ class CustomSseClientTransport(
// 收集SSE事件 // 收集SSE事件
collectEvents() collectEvents()
// 启动心跳任务
startHeartbeat()
// 等待endpoint就绪 // 等待endpoint就绪
endpoint.await() endpoint.await()
Log.d(TAG, "传输层启动完成,消息端点已就绪") Log.d(TAG, "传输层启动完成,消息端点已就绪")
@ -391,7 +350,6 @@ class CustomSseClientTransport(
Log.e(TAG, "关闭失败: 传输层未初始化") Log.e(TAG, "关闭失败: 传输层未初始化")
error("CustomSseClientTransport is not initialized!") error("CustomSseClientTransport is not initialized!")
} }
heartbeatJob?.cancel()
session.cancel() session.cancel()
_onClose() _onClose()
job?.cancelAndJoin() job?.cancelAndJoin()
@ -404,29 +362,6 @@ class CustomSseClientTransport(
fun isInitialized(): Boolean { fun isInitialized(): Boolean {
return initialized.get() 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