|
|
@ -38,7 +38,7 @@ interface StreamCallback { |
|
|
/** |
|
|
/** |
|
|
* AI使用量 |
|
|
* AI使用量 |
|
|
* */ |
|
|
* */ |
|
|
fun onUsage(sessionid: String,prompt_tokens: Int?,completion_tokens: Int?,total_tokens: Int?) |
|
|
fun onUsage(sessionid: String,prompt_tokens: Int?,completion_tokens: Int?,callfunctions: List<String>?) |
|
|
/** |
|
|
/** |
|
|
* 接收到新的token |
|
|
* 接收到新的token |
|
|
*/ |
|
|
*/ |
|
|
@ -106,6 +106,13 @@ private data class ToolCallInfo( |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 会话统计信息结构 |
|
|
|
|
|
private data class SessionStats( |
|
|
|
|
|
var promptTokens: Int = 0, |
|
|
|
|
|
var completionTokens: Int = 0, |
|
|
|
|
|
val calledFunctions: MutableList<String> = mutableListOf() |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* ChatAPI服务的Android实现 |
|
|
* ChatAPI服务的Android实现 |
|
|
* 基于 openai-kotlin 4.0.1,与 iOS 版本接口完全一致 |
|
|
* 基于 openai-kotlin 4.0.1,与 iOS 版本接口完全一致 |
|
|
@ -113,7 +120,7 @@ private data class ToolCallInfo( |
|
|
class ChatApiService(private val context: android.content.Context? = null) : CoroutineScope { |
|
|
class ChatApiService(private val context: android.content.Context? = null) : CoroutineScope { |
|
|
|
|
|
|
|
|
override val coroutineContext: CoroutineContext = Dispatchers.IO + SupervisorJob() |
|
|
override val coroutineContext: CoroutineContext = Dispatchers.IO + SupervisorJob() |
|
|
|
|
|
|
|
|
companion object { |
|
|
companion object { |
|
|
private const val TAG = "ChatApiService" |
|
|
private const val TAG = "ChatApiService" |
|
|
} |
|
|
} |
|
|
@ -133,7 +140,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor |
|
|
|
|
|
|
|
|
// MCP 客户端 |
|
|
// MCP 客户端 |
|
|
private var _mcpClient: MCPClient? = null |
|
|
private var _mcpClient: MCPClient? = null |
|
|
|
|
|
|
|
|
// 公开的MCP客户端访问器 |
|
|
// 公开的MCP客户端访问器 |
|
|
val mcpClient: MCPClient? |
|
|
val mcpClient: MCPClient? |
|
|
get() = _mcpClient |
|
|
get() = _mcpClient |
|
|
@ -147,6 +154,8 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor |
|
|
private var currSessionId = "" |
|
|
private var currSessionId = "" |
|
|
// 为每个会话维护独立的回调函数 |
|
|
// 为每个会话维护独立的回调函数 |
|
|
private val sessionCallbacks: MutableMap<String, StreamCallback> = Collections.synchronizedMap(mutableMapOf()) |
|
|
private val sessionCallbacks: MutableMap<String, StreamCallback> = Collections.synchronizedMap(mutableMapOf()) |
|
|
|
|
|
// 会话统计信息 |
|
|
|
|
|
private val sessionStats: MutableMap<String, SessionStats> = Collections.synchronizedMap(mutableMapOf()) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -165,8 +174,17 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor |
|
|
// 1. 取消当前的协程任务 |
|
|
// 1. 取消当前的协程任务 |
|
|
currentStreamJob?.cancel(CancellationException("New session started")) |
|
|
currentStreamJob?.cancel(CancellationException("New session started")) |
|
|
currentStreamJob = null |
|
|
currentStreamJob = null |
|
|
|
|
|
if (oldSessionId != ""){ |
|
|
// 2. 通知旧会话被中止 |
|
|
val stats = sessionStats[oldSessionId] |
|
|
|
|
|
getSessionCallback(oldSessionId)?.onUsage( |
|
|
|
|
|
oldSessionId, |
|
|
|
|
|
stats?.promptTokens, |
|
|
|
|
|
stats?.completionTokens, |
|
|
|
|
|
stats?.calledFunctions?.toList() |
|
|
|
|
|
) |
|
|
|
|
|
sessionStats.remove(oldSessionId) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
getSessionCallback(oldSessionId)?.onComplete(oldSessionId) // 直接完成当前会话 |
|
|
getSessionCallback(oldSessionId)?.onComplete(oldSessionId) // 直接完成当前会话 |
|
|
|
|
|
|
|
|
// 3. 清理状态 |
|
|
// 3. 清理状态 |
|
|
@ -174,6 +192,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor |
|
|
sessionCallbacks.remove(oldSessionId) |
|
|
sessionCallbacks.remove(oldSessionId) |
|
|
toolCalls.clear() |
|
|
toolCalls.clear() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Log.d(TAG, "会话 $oldSessionId 已被中止") |
|
|
Log.d(TAG, "会话 $oldSessionId 已被中止") |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
@ -187,6 +206,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor |
|
|
sessionCallbacks.remove(oldSessionId) |
|
|
sessionCallbacks.remove(oldSessionId) |
|
|
currentStreamJob = null |
|
|
currentStreamJob = null |
|
|
toolCalls.clear() |
|
|
toolCalls.clear() |
|
|
|
|
|
sessionStats.remove(oldSessionId) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
@ -335,6 +355,8 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor |
|
|
// val newSessionId = UUID.randomUUID().toString() |
|
|
// val newSessionId = UUID.randomUUID().toString() |
|
|
currSessionId = sessionid |
|
|
currSessionId = sessionid |
|
|
sessionCallbacks[sessionid] = callback |
|
|
sessionCallbacks[sessionid] = callback |
|
|
|
|
|
// 初始化会话统计 |
|
|
|
|
|
sessionStats[sessionid] = SessionStats() |
|
|
|
|
|
|
|
|
// 3. 转换消息并开始处理 |
|
|
// 3. 转换消息并开始处理 |
|
|
val messageList = convertJsonArrayToList(messages) |
|
|
val messageList = convertJsonArrayToList(messages) |
|
|
@ -414,7 +436,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor |
|
|
throw IllegalStateException("OpenAI客户端未初始化") |
|
|
throw IllegalStateException("OpenAI客户端未初始化") |
|
|
} |
|
|
} |
|
|
// Log.e("ChatApiService", "liwei---------,发送AI请求 model:${currentModel} messages:${chatMessages}") |
|
|
// Log.e("ChatApiService", "liwei---------,发送AI请求 model:${currentModel} messages:${chatMessages}") |
|
|
// 添加超时控制,10秒超时 |
|
|
// 添加超时控制,10秒超时 |
|
|
val flow = withTimeout(10.seconds) { |
|
|
val flow = withTimeout(10.seconds) { |
|
|
openAI!!.chatCompletions(chatCompletionRequest) |
|
|
openAI!!.chatCompletions(chatCompletionRequest) |
|
|
} |
|
|
} |
|
|
@ -496,7 +518,6 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
toolCall.function?.let { function -> |
|
|
toolCall.function?.let { function -> |
|
|
// 安全处理函数名称 - 使用 try-catch 避免 null 属性访问 |
|
|
|
|
|
try { |
|
|
try { |
|
|
val functionName = function.name |
|
|
val functionName = function.name |
|
|
if (!functionName.isNullOrEmpty()) { |
|
|
if (!functionName.isNullOrEmpty()) { |
|
|
@ -532,9 +553,22 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
val promptTokens = estimatePromptTokens(chatMessages); //计算上传token |
|
|
|
|
|
val completionTokens = roughTokenCount(replyContext); //计算上传token |
|
|
// 仅统计数据字节数,服务端负责计算token |
|
|
getSessionCallback(sessionid)?.onUsage(sessionid,promptTokens,completionTokens,promptTokens+completionTokens) |
|
|
val promptBytes = try { |
|
|
|
|
|
val jsonArray = org.json.JSONArray() |
|
|
|
|
|
messages.forEach { msg -> |
|
|
|
|
|
jsonArray.put(convertMapToJsonObject(msg)) |
|
|
|
|
|
} |
|
|
|
|
|
jsonArray.toString().toByteArray(Charsets.UTF_8).size |
|
|
|
|
|
} catch (_: Exception) { 0 } |
|
|
|
|
|
val completionBytes = replyContext.toByteArray(Charsets.UTF_8).size |
|
|
|
|
|
// 累计到会话统计 |
|
|
|
|
|
sessionStats.getOrPut(sessionid) { SessionStats() }.apply { |
|
|
|
|
|
this.promptTokens += promptBytes |
|
|
|
|
|
this.completionTokens += completionBytes |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// Log.d(TAG, "liwei-------------------------开始AI 对话 8") |
|
|
// Log.d(TAG, "liwei-------------------------开始AI 对话 8") |
|
|
// 完成处理 |
|
|
// 完成处理 |
|
|
if (sessionid == currSessionId) { |
|
|
if (sessionid == currSessionId) { |
|
|
@ -543,12 +577,33 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor |
|
|
val hasToolCalls = processToolCalls(sessionid) |
|
|
val hasToolCalls = processToolCalls(sessionid) |
|
|
if (!hasToolCalls) { |
|
|
if (!hasToolCalls) { |
|
|
try { |
|
|
try { |
|
|
|
|
|
val stats = sessionStats[sessionid] |
|
|
|
|
|
getSessionCallback(sessionid)?.onUsage( |
|
|
|
|
|
sessionid, |
|
|
|
|
|
stats?.promptTokens, |
|
|
|
|
|
stats?.completionTokens, |
|
|
|
|
|
stats?.calledFunctions?.toList() |
|
|
|
|
|
) |
|
|
getSessionCallback(sessionid)?.onComplete(sessionid) |
|
|
getSessionCallback(sessionid)?.onComplete(sessionid) |
|
|
} catch (e: Exception) { |
|
|
} catch (e: Exception) { |
|
|
Log.e(TAG, "onComplete回调异常: ${e.message}", e) |
|
|
Log.e(TAG, "onComplete回调异常: ${e.message}", e) |
|
|
} |
|
|
} |
|
|
clearCurrentSession() |
|
|
clearCurrentSession() |
|
|
} |
|
|
} |
|
|
|
|
|
}else{ |
|
|
|
|
|
// 会话结束,上报使用统计,再回调完成 |
|
|
|
|
|
try { |
|
|
|
|
|
val stats = sessionStats[sessionid] |
|
|
|
|
|
getSessionCallback(sessionid)?.onUsage( |
|
|
|
|
|
sessionid, |
|
|
|
|
|
stats?.promptTokens, |
|
|
|
|
|
stats?.completionTokens, |
|
|
|
|
|
stats?.calledFunctions?.toList() |
|
|
|
|
|
) |
|
|
|
|
|
getSessionCallback(sessionid)?.onComplete(sessionid) |
|
|
|
|
|
} catch (e: Exception) { |
|
|
|
|
|
Log.e(TAG, "onUsage回调异常: ${e.message}", e) |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
// Log.d(TAG, "liwei-------------------------开始AI 对话 10") |
|
|
// Log.d(TAG, "liwei-------------------------开始AI 对话 10") |
|
|
} catch (e: CancellationException) { |
|
|
} catch (e: CancellationException) { |
|
|
@ -557,6 +612,13 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor |
|
|
} catch (e: Exception) { |
|
|
} catch (e: Exception) { |
|
|
if (sessionid == currSessionId) { |
|
|
if (sessionid == currSessionId) { |
|
|
try { |
|
|
try { |
|
|
|
|
|
val stats = sessionStats[sessionid] |
|
|
|
|
|
getSessionCallback(sessionid)?.onUsage( |
|
|
|
|
|
sessionid, |
|
|
|
|
|
stats?.promptTokens, |
|
|
|
|
|
stats?.completionTokens, |
|
|
|
|
|
stats?.calledFunctions?.toList() |
|
|
|
|
|
) |
|
|
Log.d(TAG, "AI聊天异常 Session $sessionid 错误类型:${e::class.simpleName} 错误完整类型:${e::class.qualifiedName}") |
|
|
Log.d(TAG, "AI聊天异常 Session $sessionid 错误类型:${e::class.simpleName} 错误完整类型:${e::class.qualifiedName}") |
|
|
getSessionCallback(sessionid)?.onError(sessionid,1000,"流式请求失败: ${e.message}") |
|
|
getSessionCallback(sessionid)?.onError(sessionid,1000,"流式请求失败: ${e.message}") |
|
|
} catch (ex: Exception) { |
|
|
} catch (ex: Exception) { |
|
|
@ -586,7 +648,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor |
|
|
|
|
|
|
|
|
// 只处理有效的工具调用 |
|
|
// 只处理有效的工具调用 |
|
|
val validToolCalls = toolCalls.values.filter { it.isValid() } |
|
|
val validToolCalls = toolCalls.values.filter { it.isValid() } |
|
|
if (validToolCalls.isEmpty()) { |
|
|
if (validToolCalls.isEmpty()) { |
|
|
Log.w("ChatApiService", "[Session: $sessionid] 没有有效的工具调用(参数可能不完整)") |
|
|
Log.w("ChatApiService", "[Session: $sessionid] 没有有效的工具调用(参数可能不完整)") |
|
|
return false |
|
|
return false |
|
|
} |
|
|
} |
|
|
@ -601,6 +663,12 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor |
|
|
"id" to firstToolCall.id |
|
|
"id" to firstToolCall.id |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
// 记录方法名称以返回给调用方 |
|
|
|
|
|
val functionName: String = firstToolCall.name |
|
|
|
|
|
if (functionName.isNotEmpty()) { |
|
|
|
|
|
sessionStats.getOrPut(sessionid) { SessionStats() }.calledFunctions.add(functionName) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// 通知上层工具调用事件 |
|
|
// 通知上层工具调用事件 |
|
|
try { |
|
|
try { |
|
|
getSessionCallback(sessionid)?.onFunctionCall(sessionid,convertMapToJsonObject(functionCall)) |
|
|
getSessionCallback(sessionid)?.onFunctionCall(sessionid,convertMapToJsonObject(functionCall)) |
|
|
@ -609,7 +677,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 在当前协程作用域内处理工具调用,使用async确保生命周期管理 |
|
|
// 在当前协程作用域内处理工具调用,使用async确保生命周期管理 |
|
|
val toolCallDeferred = async { |
|
|
val toolCallDeferred = async { |
|
|
try { |
|
|
try { |
|
|
// 检查会话是否仍然有效 |
|
|
// 检查会话是否仍然有效 |
|
|
if (sessionid != currSessionId) { |
|
|
if (sessionid != currSessionId) { |
|
|
@ -750,7 +818,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor |
|
|
// 不重新抛出异常,让流程正常结束 |
|
|
// 不重新抛出异常,让流程正常结束 |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return true |
|
|
return true |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
@ -983,7 +1051,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor |
|
|
* 关闭MCP客户端 |
|
|
* 关闭MCP客户端 |
|
|
*/ |
|
|
*/ |
|
|
fun closeMcpClient() { |
|
|
fun closeMcpClient() { |
|
|
runBlocking { |
|
|
runBlocking { |
|
|
_mcpClient?.disconnectAll() |
|
|
_mcpClient?.disconnectAll() |
|
|
_mcpClient = null |
|
|
_mcpClient = null |
|
|
} |
|
|
} |
|
|
@ -1230,7 +1298,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor |
|
|
ChatMessage.System(displayText) |
|
|
ChatMessage.System(displayText) |
|
|
} |
|
|
} |
|
|
Role.User -> ChatMessage.User(contentParts) |
|
|
Role.User -> ChatMessage.User(contentParts) |
|
|
Role.Assistant -> { |
|
|
Role.Assistant -> { |
|
|
// 将多模态内容转换为ListContent |
|
|
// 将多模态内容转换为ListContent |
|
|
val listContent = ListContent(contentParts) |
|
|
val listContent = ListContent(contentParts) |
|
|
// 提取文本内容作为Assistant消息的content |
|
|
// 提取文本内容作为Assistant消息的content |
|
|
@ -1468,4 +1536,4 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor |
|
|
return "Function '$functionName' called with arguments: $arguments" |
|
|
return "Function '$functionName' called with arguments: $arguments" |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|