Browse Source

add

newdev_shunjiawei
wolfplus 1 year ago
parent
commit
e58570842c
  1. 26
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt
  2. 30
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  3. 187
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  4. 79
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt
  5. 404
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt
  6. 14
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/CustomSseClientTransport.kt
  7. 30
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt
  8. 36
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt

26
android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt

@ -19,7 +19,9 @@ class MainActivity: FlutterActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 设置日志级别,减少系统级日志
suppressSystemLogs()
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
@ -32,5 +34,27 @@ class MainActivity: FlutterActivity() {
override fun onDestroy() {
super.onDestroy()
}
/**
* 抑制系统级组件的日志输出
*/
private fun suppressSystemLogs() {
val systemTags = arrayOf(
"MediaCodec",
"CCodec",
"CCodecBufferChannel",
"OpusManager",
"MediaCodecList",
"MediaPlayer",
"AudioTrack",
"AudioManager",
"AudioFlinger",
"AudioService"
)
systemTags.forEach { tag ->
System.setProperty("log.tag.$tag", "WARN")
}
}
}

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

@ -152,15 +152,13 @@ object AgentService : CoroutineScope {
config["openaiApiKey"]?.toString() ?: "",
config["openaiBaseUrl"]?.toString() ?: "",
config["openaiModel"]?.toString() ?: "",
""
//config["mcpServer"]?.toString() ?: ""
config["mcpServer"]?.toString() ?: ""
)
// 加载最近的聊天记录
loadChatHistory()
isInitialized = true
Log.d(TAG, "代理服务初始化成功")
return true
} catch (e: Exception) {
Log.e(TAG, "初始化失败: ${e.message}")
@ -195,8 +193,6 @@ object AgentService : CoroutineScope {
job.cancel()
clearListeners()
isInitialized = false
Log.d(TAG, "代理服务资源已释放")
} catch (e: Exception) {
Log.e(TAG, "释放资源异常: ${e.message}")
}
@ -230,9 +226,7 @@ object AgentService : CoroutineScope {
language = ttsLanguage
)
if (success) {
Log.d(TAG, "Azure TTS服务初始化成功")
} else {
if (!success) {
Log.e(TAG, "Azure TTS服务初始化失败")
}
@ -268,13 +262,10 @@ object AgentService : CoroutineScope {
}
else -> {
// 处理其他类型的事件
Log.d(TAG, "TTS事件: ${event.type}")
}
}
}
})
Log.d(TAG, "TTS服务配置完成")
} catch (e: Exception) {
Log.e(TAG, "初始化TTS引擎失败: ${e.message}")
}
@ -372,7 +363,6 @@ object AgentService : CoroutineScope {
} else {
AzureAsrHelper.AudioSourceType.MICROPHONE
}
Log.d(TAG, "开始语音识别, 音频源类型: $audioSourceType")
azureAsrHelper?.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onRecognizing(recognizing: String, detectedLanguage: String) {
if (recognizing.isNotEmpty()) {
@ -477,7 +467,6 @@ object AgentService : CoroutineScope {
BleService.closeCodec()
isRecognitionActive = false
stopIdleCheck()
Log.d(TAG, "语音识别已停止")
} catch (e: Exception) {
Log.e(TAG, "停止语音识别异常: ${e.message}")
isRecognitionActive = false
@ -518,8 +507,6 @@ object AgentService : CoroutineScope {
// 通知ChatAPI服务终止当前流式请求
chatApiService.cancelCurrentStream()
// 记录日志
Log.d(TAG, "AI流输出已停止")
} catch (e: Exception) {
Log.e(TAG, "停止AI流输出异常", e)
// 确保状态被重置,即使发生异常
@ -586,8 +573,6 @@ object AgentService : CoroutineScope {
text: String = "",
speakResponse: Boolean = false
) {
Log.d(TAG, "处理图片输入: ${if (text.isEmpty()) "无附加文本" else "附带文本: $text"}")
// 创建带图片的用户消息并处理
val userMessage = createUserMessageWithImage(text, imageBase64)
// 图片描述用于存储
@ -687,7 +672,6 @@ object AgentService : CoroutineScope {
}
override fun onComplete() {
Log.d(TAG, "AI完整回复: $responseBuilder")
// 视情况决定是否朗读回复
if (speakResponse) {
ttsService?.flushStream()
@ -751,7 +735,6 @@ object AgentService : CoroutineScope {
"function_call" to functionCall.toString(),
"result" to functionCallResult.toString(),
))
Log.d(TAG, "mcp调用结果: $functionCallResult")
val (metestr, broadcast)= autoHandleFcunCallResult(functionCallResult);
aiMetadata = metestr;
nobroadcast = broadcast
@ -803,8 +786,6 @@ object AgentService : CoroutineScope {
addToHistoryMessages(createAssistantMessage(content))
}
}
Log.d(TAG, "已加载${recentMessages.size}条历史记录")
} catch (e: Exception) {
Log.e(TAG, "加载聊天历史失败: ${e.message}")
}
@ -906,7 +887,6 @@ object AgentService : CoroutineScope {
historyMessages.remove(0)
}
}
Log.d(TAG, "聊天历史已清除")
} else {
Log.e(TAG, "清除聊天历史失败")
}
@ -1099,7 +1079,6 @@ object AgentService : CoroutineScope {
}
}
}
Log.d(TAG, "开始播放音频资源")
} catch (e: Exception) {
Log.e(TAG, "播放音频资源异常: ${e.message}", e)
release()
@ -1134,7 +1113,6 @@ object AgentService : CoroutineScope {
// Log.d(TAG, "mcp调用结果: $metaStr")
if (meta.has("card_music")) { //音乐卡片
val cardMusic = meta.getJSONObject("card_music")
Log.d(TAG, "检查到音乐卡片: $cardMusic")
val id = cardMusic.optString("id", "")
val url = cardMusic.optString("url", "")
val name = cardMusic.optString("name", "")
@ -1157,7 +1135,6 @@ object AgentService : CoroutineScope {
playlist.add(mapOf("id" to id,"url" to url, "title" to name, "artist" to sgener,"coverUrl" to image))
}
if (playlist.isNotEmpty()) {
Log.w(TAG, "自动播放音乐列表 ${playlist}")
broadcast = true;
processMusicPlayList(playlist)
} else {
@ -1190,7 +1167,6 @@ object AgentService : CoroutineScope {
*/
fun processMusicPlay(song: Map<String,String>){
// 在其他 Service、BroadcastReceiver 或 Application 中调用
Log.i(TAG, "启动音乐服务 播放音乐 $song")
MusicServiceStarter.startServiceWithCommand(context, command = "play", song = song)
}
@ -1206,7 +1182,6 @@ object AgentService : CoroutineScope {
fun processMusicPlayList(songs:List<Map<String,String>>){
// 如果 playlist 中有有效的歌曲,开始播放
if (songs.isNotEmpty()) {
Log.i(TAG, "启动音乐服务 播放音乐列表 ${songs}")
MusicServiceStarter.startServiceWithPlaylist(context, command = "playlist", songs = songs)
} else {
Log.i(TAG, "音乐列表为空,未启动播放服务")
@ -1224,7 +1199,6 @@ object AgentService : CoroutineScope {
fun processNavigation(start:String,end:String){
// 如果 playlist 中有有效的歌曲,开始播放
if (!start.isNullOrEmpty() && !end.isNullOrEmpty()) {
Log.i(TAG, "启动导航服务 ${start} ${end}")
NavigationServiceHelper.startNavigation(context,"start", startpos = start, endpos = end)
} else {
Log.i(TAG, "启动导航服务失败")

187
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

@ -123,16 +123,26 @@ class AzureAsrHelper(private val context: Context) {
speechRecognitionLanguage = currentLanguage
}
// 设置静音超时时间(毫秒)
setProperty("SpeechServiceConnection_EndSilenceTimeoutMs", "800")
setProperty("Speech_SegmentationSilenceTimeoutMs", "800")
// 优化:大幅减少静音超时时间(从800ms到300ms)
setProperty("SpeechServiceConnection_EndSilenceTimeoutMs", "300")
setProperty("Speech_SegmentationSilenceTimeoutMs", "300")
// 优化:添加低延迟连接配置
setProperty("SpeechServiceConnection_InitialSilenceTimeoutMs", "200")
setProperty("SpeechServiceConnection_RecoMode", "INTERACTIVE")
setProperty("Speech_PushStreamFormat", "PCM")
// 设置分段策略为时间模式
setProperty("Speech_SegmentationStrategy", "Time")
}
// 创建识别器
return setupRecognizer()
val setupSuccess = setupRecognizer()
if (setupSuccess) {
// 优化:初始化完成后进行预热
warmupRecognizer()
}
return setupSuccess
} catch (e: Exception) {
Log.e(tag, "初始化失败: ${e.message}")
return false
@ -216,6 +226,36 @@ class AzureAsrHelper(private val context: Context) {
}
}
/**
* 预热识别器(减少首次识别延迟)
*/
private fun warmupRecognizer() {
try {
// 创建极短的音频数据进行预热
val warmupData = ByteArray(320) // 10ms 16kHz 单声道
// 模拟静音数据
warmupData.fill(0)
// 如果是外部音频流,进行预热
if (audioSourceType == AudioSourceType.EXTERNAL) {
externalAudioStream?.pushAudio(warmupData)
}
Log.d(tag, "ASR预热完成")
} catch (e: Exception) {
// 预热失败不影响正常使用
Log.d(tag, "ASR预热失败: ${e.message}")
}
}
/**
* 强制设置单一语言(优化语言检测逻辑)
*/
fun setForceLanguage(language: String) {
this.currentLanguage = language
this.isAutoDetectLanguage = false
speechConfig?.speechRecognitionLanguage = language
}
/**
* 设置外部音频流 - 使用拉流方式
*/
@ -337,21 +377,30 @@ class AzureAsrHelper(private val context: Context) {
* 设置事件监听器
*/
private fun setupEventListeners(callback: ContinuousRecognizeCallback) {
// 识别中事件
// 优化:识别中事件 - 添加文本长度检查
recognizer?.recognizing?.addEventListener(
EventHandler<SpeechRecognitionEventArgs> { _, event ->
val detectedLanguage = AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: ""
// 直接在当前线程调用回调
callback.onRecognizing(event.result.text, detectedLanguage)
// 优化:只处理非空结果
if (event.result.text.isNotEmpty()) {
val detectedLanguage = if (isAutoDetectLanguage) {
AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: ""
} else {
currentLanguage
}
callback.onRecognizing(event.result.text, detectedLanguage)
}
}
)
// 识别完成事件
// 优化:识别完成事件 - 优化语言检测
recognizer?.recognized?.addEventListener(
EventHandler<SpeechRecognitionEventArgs> { _, event ->
if (event.result.reason == ResultReason.RecognizedSpeech) {
val detectedLanguage = AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: ""
// 直接在当前线程调用回调
if (event.result.reason == ResultReason.RecognizedSpeech && event.result.text.isNotEmpty()) {
val detectedLanguage = if (isAutoDetectLanguage) {
AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: supportedLanguages[0]
} else {
currentLanguage
}
callback.onResult(event.result.text, detectedLanguage)
}
}
@ -538,19 +587,14 @@ class AzureAsrHelper(private val context: Context) {
private inner class MicrophoneStream : PullAudioInputStreamCallback() {
private var audioRecord: AudioRecord? = null
private var echoCanceler: AcousticEchoCanceler? = null
private var currentAudioFile: File? = null
private var fos: FileOutputStream? = null
// 用于存储音频数据的缓冲区
private val dataBuffer = mutableListOf<ByteArray>()
private var totalBytesWritten = 0
// 音频配置
private val sampleRate = 16000
private val channelConfig = AudioFormat.CHANNEL_IN_MONO
private val audioFormat = AudioFormat.ENCODING_PCM_16BIT
// 优化:减少缓冲区大小以降低延迟
private val bufferSize = AudioRecord.getMinBufferSize(
sampleRate, channelConfig, audioFormat
).let { if (it < 0) 4096 else it * 2 }
).let { if (it < 0) 2048 else it }
init {
initMicrophone()
@ -561,20 +605,6 @@ class AzureAsrHelper(private val context: Context) {
*/
private fun initMicrophone() {
try {
// 创建新的音频文件
val timestamp = System.currentTimeMillis()
val filePath = context.getExternalFilesDir(null)?.absolutePath + "/recorded_audio_$timestamp.wav"
currentAudioFile = File(filePath)
currentAudioFile?.createNewFile()
// 仅写入初始文件头
fos = FileOutputStream(currentAudioFile).apply {
write(generateWavHeader(0)) // 初始0长度
close()
}
// 追加音频数据到文件
fos = FileOutputStream(currentAudioFile, true)
// 创建录音对象
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
val format = AudioFormat.Builder()
@ -619,14 +649,20 @@ class AzureAsrHelper(private val context: Context) {
}
/**
* 应用音频效果
* 优化:条件性应用音频效果
*/
private fun applyAudioEffects() {
val sessionId = audioRecord?.audioSessionId ?: return
// 回音消除
echoCanceler = AcousticEchoCanceler.create(sessionId)
echoCanceler?.enabled = true;
// 优化:只在真正需要时启用回音消除
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
if (audioManager.isSpeakerphoneOn) {
echoCanceler = AcousticEchoCanceler.create(sessionId)
echoCanceler?.enabled = true
Log.d(tag, "启用回音消除")
} else {
Log.d(tag, "跳过回音消除(使用耳机)")
}
}
/**
@ -638,6 +674,9 @@ class AzureAsrHelper(private val context: Context) {
* Azure SDK 调用此方法获取音频数据
* 这是拉流模式的核心方法,由SDK调用以获取音频数据
*/
/**
* 优化:简化音频数据读取(移除文件保存)
*/
override fun read(buffer: ByteArray): Int {
try {
val result = audioRecord?.read(buffer, 0, buffer.size) ?: -1
@ -645,80 +684,18 @@ class AzureAsrHelper(private val context: Context) {
Log.e(tag, "读取音频数据失败: $result")
return 0
}
// 将音频数据保存成wav格式的音频文件
saveAudioDataToWav(buffer, result)
return result
} catch (e: Exception) {
Log.e(tag, "读取音频异常: ${e.message}")
return 0
}
}
/**
* 保存音频数据到 WAV 文件
*/
private fun saveAudioDataToWav(buffer: ByteArray, length: Int) {
// 复制数据到新数组
val copy = ByteArray(length)
System.arraycopy(buffer, 0, copy, 0, length)
// 添加到缓冲区
dataBuffer.add(copy)
totalBytesWritten += length
// 写入文件
try {
fos?.write(copy, 0, length)
} catch (e: Exception) {
Log.e(tag, "写入音频数据失败: ${e.message}")
}
}
// 更新文件头生成(修正RIFF长度计算)
private fun generateWavHeader(dataLength: Int): ByteArray {
val totalLength = 36 + dataLength // RIFF块总长度 = 头部36字节 + 音频数据
val byteRate = sampleRate * 2 * 1 // 采样率 * 字节/样本 * 通道数
return byteArrayOf(
'R'.code.toByte(), 'I'.code.toByte(), 'F'.code.toByte(), 'F'.code.toByte(),
(totalLength and 0xFF).toByte(), ((totalLength shr 8) and 0xFF).toByte(),
((totalLength shr 16) and 0xFF).toByte(), ((totalLength shr 24) and 0xFF).toByte(),
'W'.code.toByte(), 'A'.code.toByte(), 'V'.code.toByte(), 'E'.code.toByte(),
'f'.code.toByte(), 'm'.code.toByte(), 't'.code.toByte(), ' '.code.toByte(),
16, 0, 0, 0, // PCM头长度
1, 0, // PCM格式
1, 0, // 单声道
(sampleRate and 0xFF).toByte(), ((sampleRate shr 8) and 0xFF).toByte(), 0, 0, // 采样率
(byteRate and 0xFF).toByte(), ((byteRate shr 8) and 0xFF).toByte(), 0, 0, // 字节率
2, 0, // 块对齐 (通道数 * 样本位数/8)
16, 0, // 样本位数
'd'.code.toByte(), 'a'.code.toByte(), 't'.code.toByte(), 'a'.code.toByte(),
(dataLength and 0xFF).toByte(), ((dataLength shr 8) and 0xFF).toByte(),
((dataLength shr 16) and 0xFF).toByte(), ((dataLength shr 24) and 0xFF).toByte()
)
}
/**
* 关闭音频资源
* 优化:简化资源关闭
*/
override fun close() {
releaseAudioResources()
try {
// 关闭文件流
fos?.close()
// 更新WAV文件头
currentAudioFile?.let { file ->
RandomAccessFile(file, "rw").use { raf ->
raf.seek(0)
raf.write(generateWavHeader(totalBytesWritten))
}
Log.d(tag, "音频文件保存完成: ${file.absolutePath}")
}
} catch (e: Exception) {
Log.e(tag, "更新WAV文件头失败: ${e.message}")
} finally {
fos = null
currentAudioFile = null
}
}
/**
@ -763,14 +740,20 @@ private fun saveAudioDataToWav(buffer: ByteArray, length: Int) {
}
/**
* SDK调用:从队列中拉取数据
* 修复:外部音频流恢复阻塞读取
* @param buffer SDK提供的缓冲区
* @return 读取的字节数,0表示流结束
*/
override fun read(buffer: ByteArray): Int {
try {
// 阻塞等待下一块数据
// 修复:恢复阻塞等待,确保外部音频数据完整性
val chunk = queue.take()
// 检查是否是结束标志(空数组)
if (chunk.isEmpty()) {
return 0
}
val toCopy = minOf(chunk.size, buffer.size)
System.arraycopy(chunk, 0, buffer, 0, toCopy)
return toCopy

79
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt

@ -70,7 +70,14 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
// 创建语音配置
speechConfig = SpeechConfig.fromSubscription(ttsAppToken, ttsResource)
speechConfig?.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm)
// 优化:使用更低延迟的音频格式(从24KHz降到16KHz)
speechConfig?.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Riff16Khz16BitMonoPcm)
// 优化:设置低延迟连接属性
speechConfig?.setProperty("SpeechServiceConnection_InitialSilenceTimeoutMs", "300")
speechConfig?.setProperty("SpeechServiceConnection_EndSilenceTimeoutMs", "300")
speechConfig?.setSpeechSynthesisVoiceName(currentVoice)
// 创建音频配置
@ -91,6 +98,9 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
isInitialized = true
FileLogger.d(TAG, "TTS引擎初始化成功: 区域=$ttsResource")
// 优化:初始化完成后立即预热
warmupSynthesizer()
return true
} catch (e: Exception) {
FileLogger.e(TAG, "TTS引擎初始化失败: ${e.message}")
@ -243,6 +253,30 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
))
}
}
/**
* 预热TTS引擎(减少首次合成延迟)
*/
private fun warmupSynthesizer() {
try {
// 使用极短文本进行预热
val warmupSsml = """
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="zh-CN">
<voice name="$currentVoice">
<prosody rate="$currentRate" pitch="$currentPitch" volume="0%">
.
</prosody>
</voice>
</speak>
""".trimIndent()
// 静音预热(音量设为0)
synthesizer?.SpeakSsmlAsync(warmupSsml)
} catch (e: Exception) {
// 预热失败不影响正常使用
FileLogger.d(TAG, "TTS预热失败: ${e.message}")
}
}
//清洗播报语音内容
fun cleanTextForTTS(text: String): String {
return text
@ -320,6 +354,30 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
""".trimIndent()
}
/**
* 生成优化的SSML(减少复杂度提升速度)
*/
private fun generateOptimizedSsml(rawText: String): String {
// 优化:简化文本预处理,减少正则表达式使用
val processedText = rawText
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace(Regex("[😀-🟿]+"), "") // 简化表情符号移除
.trim()
// 优化:简化SSML结构,减少嵌套层级
return """
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="zh-CN">
<voice name="$currentVoice">
<prosody rate="$currentRate" pitch="$currentPitch" volume="$currentVolume">
$processedText
</prosody>
</voice>
</speak>
""".trimIndent()
}
/**
* 单次播放文本(非流式)
*/
@ -334,8 +392,8 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
}
try {
// 生成SSML并播放
val ssml = generateSsml(text)
// 优化:使用简化的SSML生成
val ssml = generateOptimizedSsml(text)
isSpeaking = true
@ -373,21 +431,22 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
// 添加新文本到缓冲区
streamBuffer.append(text)
// 增加500ms防抖逻辑
// 优化:大幅减少防抖时间从600ms到150ms
val currentTime = System.currentTimeMillis()
if (currentTime - lastSpeakTime < 600) {
if (currentTime - lastSpeakTime < 150) {
return true
}
lastSpeakTime = currentTime
val currentText = streamBuffer.toString() //cleanTextForTTS(streamBuffer.toString())
// 定义标点符号列表
val punctuationMarks = listOf('.', '。', '!', '!', '?', '?', ';', ';', ',', ',', ':', ':', '\n')
// 查找最后一个标点符号的位置
// 优化:使用字符集合替代列表,提升查找效率
val punctuationSet = setOf('.', '。', '!', '!', '?', '?', ';', ';', ',', ',', ':', ':', '\n')
// 优化:从后往前查找最后一个标点符号
var lastPunctuationIndex = -1
for (i in currentText.indices.reversed()) {
if (currentText[i] in punctuationMarks) {
for (i in currentText.length - 1 downTo 0) {
if (currentText[i] in punctuationSet) {
lastPunctuationIndex = i
break
}

404
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt

@ -68,7 +68,13 @@ private data class ToolCallInfo(
var name: String = "",
var arguments: String = ""
) {
fun isValid(): Boolean = id.isNotEmpty() && name.isNotEmpty()
fun isValid(): Boolean {
return try {
id.isNotEmpty() && name.isNotEmpty()
} catch (e: Exception) {
false
}
}
}
/**
@ -108,6 +114,8 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
private var toolCalls: MutableMap<Int, ToolCallInfo> = mutableMapOf()
private var isCanceled = false
// JSON处理
private val gson = Gson()
@ -124,13 +132,6 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
* 初始化ChatAPI服务
*/
fun initialize(apiKey: String, baseUrl: String, model: String, mcpServer: String): Boolean {
Log.d("ChatApiService", "=== ChatApiService 初始化开始 ===")
Log.d("ChatApiService", "参数检查:")
Log.d("ChatApiService", " - apiKey: ${if (apiKey.isNotEmpty()) "已提供(${apiKey.length}字符)" else "未提供"}")
Log.d("ChatApiService", " - baseUrl: '$baseUrl'")
Log.d("ChatApiService", " - model: '$model'")
Log.d("ChatApiService", " - mcpServer: '$mcpServer'")
this.apiKey = apiKey
if (baseUrl.isNotEmpty()) {
this.baseUrl = baseUrl
@ -138,7 +139,6 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
if (model.isNotEmpty()) {
this.model = model
}
Log.d("ChatApiService", "原始 baseUrl: $baseUrl")
// 处理 baseUrl:移除末尾的 /chat/completions(如果存在)
// 因为 openai-kotlin 会自动拼接 /chat/completions
@ -153,24 +153,16 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
processedBaseUrl += "/"
}
Log.d("ChatApiService", "处理后 baseUrl: $processedBaseUrl")
return try {
Log.d("ChatApiService", "开始创建OpenAI配置...")
// 创建OpenAI配置
val config = OpenAIConfig(
token = apiKey,
timeout = Timeout(socket = 60.seconds),
host = OpenAIHost(baseUrl = processedBaseUrl)
)
Log.d("ChatApiService", "OpenAI配置创建成功")
Log.d("ChatApiService", "正在创建OpenAI客户端...")
openAI = OpenAI(config)
Log.d("ChatApiService", "OpenAI客户端创建成功")
// 初始化MCP客户端
Log.d("ChatApiService", "准备初始化MCP客户端...")
// 保存MCP配置以便后续使用
mcpConfigJson = mcpServer
// 异步初始化MCP客户端
@ -179,7 +171,6 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
}
isInitialized = apiKey.isNotEmpty()
Log.d("ChatApiService", "ChatApiService初始化完成,isInitialized: $isInitialized")
true
} catch (e: Exception) {
Log.e("ChatApiService", "ChatApiService初始化失败: ${e.message}", e)
@ -231,72 +222,8 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
// 转换消息格式
val chatMessages = convertToChatMessages(messages)
// 获取MCP工具列表
val tools = mutableListOf<Tool>()
mcpClient?.getToolMaps()?.forEach { toolMap ->
try {
Log.d("ChatApiService", "处理工具映射: $toolMap")
val type = toolMap["type"] as? String
if (type == "function") {
@Suppress("UNCHECKED_CAST")
val functionMap = toolMap["function"] as? Map<String, Any>
if (functionMap == null) {
Log.e("ChatApiService", "工具function映射为null")
return@forEach
}
val name = functionMap["name"] as? String
if (name == null) {
Log.e("ChatApiService", "工具name为null")
return@forEach
}
val description = functionMap["description"] as? String ?: ""
val parametersMap = functionMap["parameters"] as? Map<String, Any>
Log.d("ChatApiService", "工具信息 - 名称: $name, 描述: $description")
Log.d("ChatApiService", "参数映射: $parametersMap")
if (parametersMap == null) {
Log.e("ChatApiService", "工具 $name 的parameters为null")
return@forEach
}
// 验证parametersMap的基本结构
if (!parametersMap.containsKey("type")) {
Log.e("ChatApiService", "工具 $name 的parameters缺少type字段")
return@forEach
}
val parametersJson = gson.toJson(parametersMap)
Log.d("ChatApiService", "参数JSON: $parametersJson")
try {
val parameters = com.aallam.openai.api.core.Parameters.fromJsonString(parametersJson)
Log.d("ChatApiService", "成功创建Parameters对象")
tools.add(Tool.function(
name = name,
description = description,
parameters = parameters
))
Log.d("ChatApiService", "成功添加工具: $name")
} catch (e: Exception) {
Log.e("ChatApiService", "创建Parameters对象失败 for 工具 $name: ${e.message}", e)
Log.e("ChatApiService", "失败的参数JSON: $parametersJson")
// 跳过这个工具,继续处理其他工具
}
} else {
Log.d("ChatApiService", "跳过非function类型的工具: $type")
}
} catch (e: Exception) {
Log.e("ChatApiService", "处理工具映射时出错: ${e.message}", e)
Log.e("ChatApiService", "出错的工具映射: $toolMap")
}
}
// 直接获取工具列表
val tools = getOpenAiTools()
// 构建请求
val chatCompletionRequest = ChatCompletionRequest(
@ -344,12 +271,6 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
* 发送消息(流式输出)
*/
fun sendMessageStream(messages: List<Map<String, Any>>) {
Log.d("ChatApiService", "=== sendMessageStream 开始 ===")
Log.d("ChatApiService", "isInitialized: $isInitialized")
Log.d("ChatApiService", "apiKey isEmpty: ${apiKey.isEmpty()}")
Log.d("ChatApiService", "openAI is null: ${openAI == null}")
Log.d("ChatApiService", "messages size: ${messages.size}")
if (!isInitialized || apiKey.isEmpty() || openAI == null) {
Log.e("ChatApiService", "ChatAPI服务未初始化,无法发送消息")
streamCallback?.onError(ChatApiException("ChatAPI服务未初始化"))
@ -363,140 +284,28 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
// 检查是否包含图片,决定使用哪个模型
val currentModel = if (containsImage(messages)) visionModel else model
Log.d("ChatApiService", "选择的模型: $currentModel (默认模型: $model, 视觉模型: $visionModel)")
currentStreamJob = launch {
try {
Log.e("ChatApiService", "=== 开始流式请求 ===")
Log.e("ChatApiService", "消息数量: ${messages.size}")
Log.e("ChatApiService", "当前模型: $currentModel")
Log.e("ChatApiService", "MCP客户端状态: ${mcpClient != null}")
// 转换消息格式
Log.e("ChatApiService", "开始转换消息格式...")
val chatMessages = try {
convertToChatMessages(messages)
} catch (e: Exception) {
Log.e("ChatApiService", "ERROR: 转换消息格式失败: ${e.message}", e)
Log.e("ChatApiService", "转换消息格式失败: ${e.message}", e)
throw e
}
Log.e("ChatApiService", "转换后的聊天消息数量: ${chatMessages.size}")
// 验证转换后的消息
chatMessages.forEachIndexed { index, message ->
Log.e("ChatApiService", "消息 $index: role=${message.role}, content类型=${message.content?.javaClass?.simpleName}")
}
// 获取MCP工具列表
Log.e("ChatApiService", "开始获取MCP工具列表...")
val tools = mutableListOf<Tool>()
val toolMaps = mcpClient?.getToolMaps()
Log.e("ChatApiService", "获取到的工具映射数量: ${toolMaps?.size ?: 0}")
toolMaps?.forEach { toolMap ->
try {
Log.e("ChatApiService", "处理工具映射: $toolMap")
val type = toolMap["type"] as? String
if (type == "function") {
@Suppress("UNCHECKED_CAST")
val functionMap = toolMap["function"] as? Map<String, Any>
if (functionMap == null) {
Log.e("ChatApiService", "ERROR: 工具function映射为null")
return@forEach
}
val name = functionMap["name"] as? String
if (name == null) {
Log.e("ChatApiService", "ERROR: 工具name为null")
return@forEach
}
val description = functionMap["description"] as? String ?: ""
val parametersMap = functionMap["parameters"] as? Map<String, Any>
Log.e("ChatApiService", "工具信息 - 名称: $name, 描述: $description")
Log.e("ChatApiService", "参数映射: $parametersMap")
if (parametersMap == null) {
Log.e("ChatApiService", "ERROR: 工具 $name 的parameters为null")
return@forEach
}
// 验证parametersMap的基本结构
if (!parametersMap.containsKey("type")) {
Log.e("ChatApiService", "ERROR: 工具 $name 的parameters缺少type字段")
return@forEach
}
val parametersJson = gson.toJson(parametersMap)
Log.e("ChatApiService", "参数JSON: $parametersJson")
try {
Log.e("ChatApiService", "正在调用 Parameters.fromJsonString...")
Log.e("ChatApiService", "参数JSON长度: ${parametersJson.length}")
Log.e("ChatApiService", "参数JSON内容预览: ${parametersJson.take(200)}...")
if (parametersJson.isBlank()) {
Log.e("ChatApiService", "ERROR: parametersJson为空或空白")
return@forEach
}
val parameters = com.aallam.openai.api.core.Parameters.fromJsonString(parametersJson)
Log.e("ChatApiService", "SUCCESS: 成功创建Parameters对象")
Log.e("ChatApiService", "正在创建Tool.function...")
Log.e("ChatApiService", "工具参数 - name: '$name' (isEmpty: ${name.isEmpty()})")
Log.e("ChatApiService", "工具参数 - description: '$description' (isEmpty: ${description.isEmpty()})")
Log.e("ChatApiService", "工具参数 - parameters: ${parameters != null}")
if (name.isEmpty()) {
Log.e("ChatApiService", "ERROR: 工具名称为空,跳过")
return@forEach
}
val tool = Tool.function(
name = name,
description = description,
parameters = parameters
)
tools.add(tool)
Log.e("ChatApiService", "SUCCESS: 成功添加工具: $name")
} catch (e: Exception) {
Log.e("ChatApiService", "ERROR: 创建Parameters对象失败 for 工具 $name: ${e.message}", e)
Log.e("ChatApiService", "失败的参数JSON: $parametersJson")
Log.e("ChatApiService", "异常堆栈: ${e.stackTraceToString()}")
// 跳过这个工具,继续处理其他工具
}
} else {
Log.e("ChatApiService", "跳过非function类型的工具: $type")
}
} catch (e: Exception) {
Log.e("ChatApiService", "ERROR: 处理工具映射时出错: ${e.message}", e)
Log.e("ChatApiService", "出错的工具映射: $toolMap")
}
}
Log.e("ChatApiService", "最终工具列表大小: ${tools.size}")
// 直接获取工具列表
val tools = getOpenAiTools()
// 构建请求
Log.e("ChatApiService", "正在构建ChatCompletionRequest...")
Log.e("ChatApiService", "构建参数检查:")
Log.e("ChatApiService", " - currentModel: '$currentModel' (isEmpty: ${currentModel.isEmpty()})")
Log.e("ChatApiService", " - chatMessages size: ${chatMessages.size}")
Log.e("ChatApiService", " - tools size: ${tools.size}")
Log.e("ChatApiService", " - openAI对象: ${openAI != null}")
if (currentModel.isEmpty()) {
Log.e("ChatApiService", "ERROR: 模型名称为空")
Log.e("ChatApiService", "模型名称为空")
throw IllegalArgumentException("模型名称不能为空")
}
if (chatMessages.isEmpty()) {
Log.e("ChatApiService", "ERROR: 消息列表为空")
Log.e("ChatApiService", "消息列表为空")
throw IllegalArgumentException("消息列表不能为空")
}
@ -508,53 +317,41 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
temperature = 0.7,
tools = if (tools.isNotEmpty()) tools else null
)
Log.e("ChatApiService", "SUCCESS: ChatCompletionRequest创建成功")
if (openAI == null) {
Log.e("ChatApiService", "ERROR: openAI对象为null")
Log.e("ChatApiService", "openAI对象为null")
throw IllegalStateException("OpenAI客户端未初始化")
}
Log.e("ChatApiService", "正在调用 openAI.chatCompletions...")
val flow = openAI!!.chatCompletions(chatCompletionRequest)
Log.e("ChatApiService", "SUCCESS: 获得chatsFlow,开始收集...")
flow
} catch (e: Exception) {
Log.e("ChatApiService", "ERROR: 创建ChatCompletionRequest或调用chatCompletions失败: ${e.message}", e)
Log.e("ChatApiService", "创建ChatCompletionRequest或调用chatCompletions失败: ${e.message}", e)
throw e
}
chatsFlow.collect { result ->
if (isCanceled) return@collect
Log.e("ChatApiService", "收到流式响应,结果类型: ${result.javaClass.simpleName}")
Log.e("ChatApiService", "result.choices大小: ${result.choices?.size ?: 0}")
val choice = result.choices?.firstOrNull()
if (choice == null) {
Log.e("ChatApiService", "ERROR: choice为null")
Log.e("ChatApiService", "choice为null")
return@collect
}
Log.e("ChatApiService", "choice存在,delta类型: ${choice.delta?.javaClass?.simpleName ?: "null"}")
val delta = choice.delta
if (delta == null) {
Log.e("ChatApiService", "ERROR: delta为null")
Log.e("ChatApiService", "delta为null")
return@collect
}
Log.e("ChatApiService", "delta存在,content: ${delta.content}, toolCalls: ${delta.toolCalls?.size ?: 0}")
// 处理普通文本内容
delta.content?.let { content ->
Log.e("ChatApiService", "收到文本内容: $content")
streamCallback?.onToken(content)
}
// 收集工具调用信息
delta.toolCalls?.forEach { toolCall ->
Log.e("ChatApiService", "处理工具调用: index=${toolCall.index}, id=${toolCall.id}")
val index = toolCall.index
// 创建或获取现有的工具调用信息
@ -564,7 +361,6 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
try {
toolCall.id?.let { id ->
toolCallInfo.id = id.toString()
Log.e("ChatApiService", "更新工具调用ID: $id")
}
} catch (e: Exception) {
Log.e("ChatApiService", "处理工具调用ID异常: ${e.message}")
@ -573,13 +369,20 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
// 安全处理函数信息
try {
toolCall.function?.let { function ->
function.name?.let { name ->
toolCallInfo.name = name
Log.e("ChatApiService", "更新工具名称: $name")
try {
function.name?.let { name ->
toolCallInfo.name = name
}
} catch (e: Exception) {
Log.e("ChatApiService", "处理工具调用函数名称异常: ${e.message}")
}
function.arguments?.let { args ->
toolCallInfo.arguments += args
Log.e("ChatApiService", "添加参数片段: $args")
try {
function.arguments?.let { args ->
toolCallInfo.arguments += args
}
} catch (e: Exception) {
Log.e("ChatApiService", "处理工具调用参数异常: ${e.message}")
}
}
} catch (e: Exception) {
@ -589,7 +392,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
}
if (!isCanceled) {
// 处理工具调用或完成
// 检查是否有工具调用需要处理
val hasToolCalls = processToolCalls()
if (!hasToolCalls) {
streamCallback?.onComplete()
@ -608,25 +411,28 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
* 处理工具调用
*/
private suspend fun processToolCalls(): Boolean {
Log.d("ChatApiService", "开始处理工具调用,工具调用数量: ${toolCalls.size}")
val firstToolCall = try {
toolCalls.values.firstOrNull { it.isValid() }
} catch (e: Exception) {
Log.e("ChatApiService", "查找有效工具调用异常: ${e.message}")
null
}
val firstToolCall = toolCalls.values.firstOrNull { it.isValid() }
if (firstToolCall == null) {
Log.d("ChatApiService", "没有有效的工具调用")
return false
}
Log.d("ChatApiService", "第一个有效的工具调用: name=${firstToolCall.name}, id=${firstToolCall.id}")
Log.d("ChatApiService", "工具调用参数: ${firstToolCall.arguments}")
// 创建函数调用字典
val functionCall = mapOf(
"name" to firstToolCall.name,
"arguments" to firstToolCall.arguments,
"id" to firstToolCall.id
)
Log.d("ChatApiService", "创建的函数调用字典: $functionCall")
val functionCall = try {
mapOf(
"name" to (firstToolCall.name.takeIf { it.isNotEmpty() } ?: ""),
"arguments" to (firstToolCall.arguments.takeIf { it.isNotEmpty() } ?: "{}"),
"id" to (firstToolCall.id.takeIf { it.isNotEmpty() } ?: "")
)
} catch (e: Exception) {
Log.e("ChatApiService", "创建函数调用字典异常: ${e.message}")
return false
}
// 通知上层工具调用事件
streamCallback?.onFunctionCall(convertMapToJsonObject(functionCall))
@ -639,36 +445,25 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
val functionName = firstToolCall.name
val argumentsJson = firstToolCall.arguments
Log.d("ChatApiService", "准备调用MCP工具: $functionName")
Log.d("ChatApiService", "参数JSON: $argumentsJson")
val result = if (_mcpClient?.hasToolWithName(functionName) == true) {
Log.d("ChatApiService", "MCP客户端中找到工具: $functionName")
// 解析参数
val arguments = _mcpClient?.parseJsonArguments(argumentsJson) ?: emptyMap()
Log.d("ChatApiService", "解析后的参数: $arguments")
// 调用MCP工具
Log.d("ChatApiService", "开始调用MCP工具...")
val toolResult = _mcpClient?.callTool(functionName, arguments)
Log.d("ChatApiService", "MCP工具调用结果: $toolResult")
// 处理结果
if (toolResult != null) {
if (toolResult["isError"] == true) {
Log.w("ChatApiService", "MCP工具调用返回错误")
// 处理错误情况
val content = toolResult["content"] as? List<*>
val firstContent = content?.firstOrNull() as? Map<*, *>
val errorText = firstContent?.get("text") as? String ?: "Tool execution failed"
mapOf("context" to errorText)
} else if (toolResult.containsKey("context")) {
Log.d("ChatApiService", "本地函数结果")
// 本地函数结果
toolResult
} else {
Log.d("ChatApiService", "MCP工具结果")
// MCP工具结果
val content = toolResult["content"] as? List<*>
val firstContent = content?.firstOrNull() as? Map<*, *>
@ -680,13 +475,10 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
mapOf("context" to "Tool call failed")
}
} else {
Log.w("ChatApiService", "MCP客户端中未找到工具: $functionName")
// 工具不存在
mapOf("context" to "Tool not found: $functionName")
}
Log.d("ChatApiService", "最终处理结果: $result")
if (!isCanceled) {
// 处理结果
streamCallback?.onFunctionCallResult(
@ -808,6 +600,8 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
handler = handler
)
true
} catch (e: Exception) {
Log.e("ChatApiService", "Failed to register function: $name", e)
@ -826,8 +620,8 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
// 直接使用类的CoroutineScope启动协程
launch {
try {
val result = _mcpClient?.connectToSSE(serverUrl) ?: false
Log.d("ChatApiService", "MCP客户端初始化${if (result) "成功" else "失败"}")
_mcpClient?.connectToSSE(serverUrl)
} catch (e: Exception) {
Log.e("ChatApiService", "MCP客户端初始化失败: ${e.message}", e)
}
@ -843,6 +637,88 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
return _mcpClient?.isConnected() ?: false
}
/**
* 直接从MCPClient获取OpenAI工具格式
* 将MCP工具映射转换为OpenAI工具格式
*/
private fun getOpenAiTools(): List<Tool> {
try {
val tools = mutableListOf<Tool>()
val toolMaps = mcpClient?.getToolMaps() ?: return emptyList()
toolMaps.forEach { toolMap ->
try {
val type = toolMap["type"] as? String
if (type == "function") {
@Suppress("UNCHECKED_CAST")
val functionMap = toolMap["function"] as? Map<String, Any>
if (functionMap == null) {
Log.e("ChatApiService", "工具function映射为null")
return@forEach
}
val name = functionMap["name"] as? String
if (name == null) {
Log.e("ChatApiService", "工具name为null")
return@forEach
}
val description = functionMap["description"] as? String ?: ""
val parametersMap = functionMap["parameters"] as? Map<String, Any>
if (parametersMap == null) {
Log.e("ChatApiService", "工具 $name 的parameters为null")
return@forEach
}
// 验证parametersMap的基本结构
if (!parametersMap.containsKey("type")) {
Log.e("ChatApiService", "工具 $name 的parameters缺少type字段")
return@forEach
}
val parametersJson = gson.toJson(parametersMap)
try {
if (parametersJson.isBlank()) {
Log.e("ChatApiService", "parametersJson为空或空白")
return@forEach
}
val parameters = com.aallam.openai.api.core.Parameters.fromJsonString(parametersJson)
if (name.isEmpty()) {
Log.e("ChatApiService", "工具名称为空,跳过")
return@forEach
}
val tool = Tool.function(
name = name,
description = description,
parameters = parameters
)
tools.add(tool)
} catch (e: Exception) {
Log.e("ChatApiService", "创建Parameters对象失败 for 工具 $name: ${e.message}", e)
// 跳过这个工具,继续处理其他工具
}
}
} catch (e: Exception) {
Log.e("ChatApiService", "处理工具映射时出错: ${e.message}", e)
}
}
return tools
} catch (e: Exception) {
Log.e("ChatApiService", "获取OpenAI工具格式失败: ${e.message}", e)
return emptyList()
}
}
/**
* 关闭MCP客户端
*/
@ -864,7 +740,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
return "MCP客户端未初始化"
}
try {
return try {
// 获取函数名称
val name = functionCall.getString("name")
@ -876,10 +752,10 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
val result = _mcpClient?.callTool(name, arguments)
// 返回工具调用结果
return result?.get("context") as? String ?: "工具调用失败"
result?.get("context") as? String ?: "工具调用失败"
} catch (e: Exception) {
Log.e("ChatApiService", "处理MCP工具调用失败: ${e.message}", e)
return "处理MCP工具调用失败: ${e.message}"
"处理MCP工具调用失败: ${e.message}"
}
}
@ -890,6 +766,8 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
return _mcpClient?.getToolMaps() ?: emptyList()
}
/**
* 处理图片
*/

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

@ -105,13 +105,12 @@ class CustomSseClientTransport(
}
"open" -> {
Log.d(TAG, "SSE连接已打开")
// SSE连接已打开
}
"endpoint" -> {
try {
val eventData = event.data ?: ""
Log.d(TAG, "收到endpoint事件: $eventData")
// 构建完整的端点URL
val fullEndpoint = if (eventData.contains(hostPart)) {
@ -135,7 +134,6 @@ class CustomSseClientTransport(
fullEndpoint
}
Log.d(TAG, "最终消息端点: $endpointWithParams")
endpoint.complete(endpointWithParams)
} catch (e: Exception) {
Log.e(TAG, "处理endpoint事件失败: ${e.message}", e)
@ -182,11 +180,6 @@ class CustomSseClientTransport(
hostPart = urlInfo.first
pathPart = urlInfo.second
queryParams = urlInfo.third
Log.d(TAG, "原始URL: $urlString")
Log.d(TAG, "主机部分: $hostPart")
Log.d(TAG, "路径部分: $pathPart")
Log.d(TAG, "查询参数: $queryParams")
}
// 创建SSE会话 - 直接使用原始URL
@ -202,8 +195,6 @@ class CustomSseClientTransport(
"$hostPart$pathPart"
}
Log.d(TAG, "SSE连接URL: $sseConnectUrl")
client.sseSession(
urlString = sseConnectUrl,
reconnectionTime = reconnectionTime,
@ -219,7 +210,6 @@ class CustomSseClientTransport(
// 等待endpoint就绪
endpoint.await()
Log.d(TAG, "传输层启动完成,消息端点已就绪")
}
/**
@ -234,7 +224,6 @@ class CustomSseClientTransport(
try {
val messageEndpoint = endpoint.getCompleted()
Log.d(TAG, "发送消息到: $messageEndpoint")
val jsonString = json.encodeToString(message)
@ -266,6 +255,5 @@ class CustomSseClientTransport(
session.cancel()
_onClose()
job?.cancelAndJoin()
Log.d(TAG, "传输层已关闭")
}
}

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

@ -98,14 +98,12 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
if (subClient.connect()) {
subClients[serverId] = subClient
connectedCount++
Log.d(TAG, "Connected to MCP server: $serverId")
} else {
Log.w(TAG, "Failed to connect to MCP server: $serverId")
}
}
isConnectedFlag = connectedCount > 0
Log.d(TAG, "Connected to $connectedCount MCP servers")
connectedCount > 0
} catch (e: Exception) {
@ -121,7 +119,6 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
subClients.forEach { (serverId, client) ->
try {
client.close()
Log.d(TAG, "已关闭子客户端 [$serverId]")
} catch (e: Exception) {
Log.e(TAG, "关闭子客户端 [$serverId] 失败: ${e.message}")
}
@ -177,7 +174,6 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
)
localFunctionDefs[name] = functionDef
Log.d(TAG, "Registered local function: $name")
return true
} catch (e: Exception) {
Log.e(TAG, "注册本地函数失败: ${e.message}", e)
@ -192,7 +188,6 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
val removed = localFunctions.remove(name) != null
if (removed) {
localFunctionDefs.remove(name)
Log.d(TAG, "Unregistered local function: $name")
}
return removed
}
@ -284,10 +279,29 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
*/
fun parseJsonArguments(json: String): Map<String, Any> {
return try {
val jsonObject = JSONObject(json)
// 处理空字符串或空白字符串
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", e)
Log.w(TAG, "Failed to parse JSON arguments: '$json'", e)
emptyMap()
}
}
@ -304,7 +318,6 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
*/
suspend fun disconnectAll() {
closeAllConnections()
Log.d(TAG, "Disconnected all MCP clients")
}
/**
@ -315,7 +328,6 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
closeAllConnections()
localFunctions.clear()
localFunctionDefs.clear()
Log.d(TAG, "已关闭MCP客户端")
}
}

36
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPSubClient.kt

@ -41,8 +41,6 @@ class MCPSubClient(
if (isConnected) return true
return try {
Log.d(TAG, "[$serverId] 开始连接到MCP服务器: $serverUrl")
// 创建MCP客户端实例
val client = Client(
clientInfo = Implementation(
@ -55,7 +53,6 @@ class MCPSubClient(
val transport = when {
serverUrl.startsWith("http://") || serverUrl.startsWith("https://") -> {
// SSE传输 - 使用自定义的CustomSseClientTransport
Log.d(TAG, "[$serverId] 创建自定义SSE传输,URL: $serverUrl")
val mcpHttpClient = httpClient ?: createMcpHttpClient()
CustomSseClientTransport(
client = mcpHttpClient,
@ -77,12 +74,6 @@ class MCPSubClient(
if (toolsResult != null) {
availableTools.clear()
availableTools.addAll(toolsResult.tools)
Log.d(TAG, "[$serverId] 获取到 ${availableTools.size} 个工具")
// 打印工具信息
availableTools.forEach { tool ->
Log.d(TAG, "[$serverId] 工具: ${tool.name} - ${tool.description}")
}
}
} catch (e: Exception) {
Log.w(TAG, "[$serverId] 获取工具列表失败: ${e.message}")
@ -91,7 +82,6 @@ class MCPSubClient(
mcpClient = client
isConnected = true
Log.d(TAG, "[$serverId] MCP连接成功")
true
} catch (e: Exception) {
@ -111,13 +101,7 @@ class MCPSubClient(
* 获取工具映射列表
*/
fun getToolMaps(): List<Map<String, Any>> {
Log.d(TAG, "[$serverId] 开始获取工具映射,工具数量: ${availableTools.size}")
val toolMaps = availableTools.map { tool ->
Log.d(TAG, "[$serverId] 处理工具: ${tool.name}")
Log.d(TAG, "[$serverId] 工具描述: ${tool.description}")
Log.d(TAG, "[$serverId] 输入Schema: ${tool.inputSchema}")
val parametersMap = tool.inputSchema?.let { inputSchema ->
convertInputSchemaToMap(inputSchema)
} ?: mapOf(
@ -126,8 +110,6 @@ class MCPSubClient(
"required" to emptyList<String>()
)
Log.d(TAG, "[$serverId] 转换后的参数映射: $parametersMap")
val toolMap = mapOf(
"type" to "function",
"function" to mapOf(
@ -137,11 +119,9 @@ class MCPSubClient(
)
)
Log.d(TAG, "[$serverId] 最终工具映射: $toolMap")
toolMap
}
Log.d(TAG, "[$serverId] 完成工具映射生成,返回 ${toolMaps.size} 个工具")
return toolMaps
}
@ -152,8 +132,6 @@ class MCPSubClient(
val client = mcpClient ?: return null
return try {
Log.d(TAG, "[$serverId] 调用工具: $name, 参数: $arguments")
// 创建工具调用请求 - 将Map转换为JsonObject
val argumentsJson = kotlinx.serialization.json.buildJsonObject {
arguments.forEach { (key, value) ->
@ -175,8 +153,6 @@ class MCPSubClient(
val result = client.callTool(request)
result?.let { callResult ->
Log.d(TAG, "[$serverId] 工具调用结果: ${callResult.content.size} 个内容项")
// 将结果转换为统一格式
val contentList = callResult.content.map { contentItem ->
// 根据不同的内容类型处理
@ -208,16 +184,12 @@ class MCPSubClient(
* 将Tool.Input转换为Map格式,供OpenAI使用
*/
private fun convertInputSchemaToMap(inputSchema: Tool.Input): Map<String, Any> {
Log.d(TAG, "开始转换Input Schema: $inputSchema")
val properties = mutableMapOf<String, Any>()
val required = mutableListOf<String>()
// 处理properties
inputSchema.properties?.let { propsJsonObject ->
Log.d(TAG, "处理properties: $propsJsonObject")
for ((key, value) in propsJsonObject) {
Log.d(TAG, "处理属性: $key = $value (${value::class.java.simpleName})")
when (value) {
is JsonPrimitive -> {
if (value.isString) {
@ -235,13 +207,12 @@ class MCPSubClient(
}
}
}
} ?: Log.d(TAG, "properties为null")
}
// 处理required
inputSchema.required?.let { requiredList ->
Log.d(TAG, "处理required: $requiredList")
required.addAll(requiredList)
} ?: Log.d(TAG, "required为null")
}
val result = mapOf(
"type" to "object",
@ -249,7 +220,6 @@ class MCPSubClient(
"required" to required
)
Log.d(TAG, "转换后的Schema Map: $result")
return result
}
@ -296,7 +266,6 @@ class MCPSubClient(
if (toolsResult != null) {
availableTools.clear()
availableTools.addAll(toolsResult.tools)
Log.d(TAG, "[$serverId] 刷新工具列表成功,共 ${availableTools.size} 个工具")
true
} else {
false
@ -318,7 +287,6 @@ class MCPSubClient(
mcpClient = null
isConnected = false
availableTools.clear()
Log.d(TAG, "[$serverId] MCP连接已关闭")
} catch (e: Exception) {
Log.e(TAG, "[$serverId] 关闭MCP连接时出错: ${e.message}", e)
}

Loading…
Cancel
Save