Browse Source

上传支付逻辑

weicu
liwei1dao 11 months ago
parent
commit
f4969ed613
  1. 5
      lib/modules/agent/controllers/agent_controller.dart
  2. 89
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  3. 7
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt
  4. 5
      local_plugins/agent_service/lib/agent_service.dart
  5. 100
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt
  6. 48
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt

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

@ -1019,6 +1019,11 @@ class AgentController extends GetxController with WidgetsBindingObserver {
// }
}
break;
case AgentServiceEventType.assistantUsages:
final sessionid = event.data['sessionid'] ?? '';
final integral = event.data['integral'] ?? 0;
User.instance.aiIntegral = integral;
break;
default:
break;
}

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

@ -205,7 +205,8 @@ object AgentService : CoroutineScope {
serverurl = config["serverurl"]?.toString() ?: "";
usertoken = config["usertoken"]?.toString() ?: "";
callingTool = config["callingTool"]?.toString() ?: "";
aiintegral = (config["aiintegral"] as? Number)?.toInt() ?: 0
aiintegral = config["aiintegral"]?.toString()?.toIntOrNull() ?: 0;
// 初始化上下文和服务
this.context = appContext.applicationContext
@ -1007,7 +1008,6 @@ object AgentService : CoroutineScope {
// 兜底处理,直接添加原消息
addToHistoryMessages(userMessage)
}
// Log.e("ChatApiService", "liwei---------sendMessageStream messages:${messagesWithSystemPrompt}")
chatApiService.sendMessageStream(
sessionid,
messagesWithSystemPrompt,
@ -1017,16 +1017,19 @@ object AgentService : CoroutineScope {
sessionid: String,
prompt_tokens: Int?,
completion_tokens: Int?,
total_tokens: Int?
callfunctions: List<String>?
) {
try {
sendStatisticsEvent(
mapOf(
"CHAT_PROMPT_TOKENS" to (prompt_tokens ?: 0),
"CHAT_COMPLETION_TOKENS" to (completion_tokens ?: 0),
"CHAT_TOTAL_TOKENS" to (total_tokens ?: 0)
)
)
// 显式构造 Map<String, Int>,避免 mapOf 推断为 Map<String, Int>
val actions = mutableMapOf<String, Int>()
actions["AICHAT_PROMPT_TOKENS"] = (prompt_tokens ?: 0)
actions["AICHAT_COMPLETION_TOKENS"] = (completion_tokens ?: 0)
// 累计函数调用次数:按函数名分别累加 value += 1
callfunctions?.forEach { funcName ->
val key = "AICHAT_FUNCTION_" + funcName
actions[key] = (actions[key] ?: 0) + 1
}
sendStatisticsEvent(actions)
}catch (e: Exception) {
Log.e(TAG, "liwei--------------- AI Call onUsage 异常", e)
}
@ -1035,12 +1038,10 @@ object AgentService : CoroutineScope {
override fun onToken(sessionid: String, token: String) {
try {
if (sessionid != currsessionId) {
// Log.d(TAG, "liwei--------------- AI Call onToken 会话过期 $sessionid-------$currsessionId")
return
return
}
responseBuilder.append(token)
// Log.d(TAG, "liwei--------------- AI Call onToken:$token speakResponse:$speakResponse reply:$reply broadcast:$broadcast")
if (speakResponse && reply && broadcast && token.isNotEmpty()) {
if (speakResponse && reply && broadcast && token.isNotEmpty()) {
ttsService?.speakStream(token)
}
if (reply && token.isNotEmpty()) {
@ -1308,26 +1309,18 @@ object AgentService : CoroutineScope {
}
language = languages[0]
return try {
Log.i(TAG, "设置支持的语言: ${languages.joinToString(", ")}")
val asrSuccess = azureAsrHelper?.initialize(
Log.i(TAG, "设置支持的语言: ${languages.joinToString(", ")}")
val asrSuccess = azureAsrHelper?.initialize(
subscriptionKey = this.azureSpeechKey,
region = this.azureSpeechRegion,
supportedLanguages = languages.toTypedArray(),
audioSourceType = audioSourceType
) ?: false
if (asrSuccess) {
recognizeCallback()
recognizeCallback()
} else {
Log.w(TAG, "Azure ASR初始化失败")
}
// 同时更新TTS语言设置
val ttsSuccess = updateTtsLanguage(languages.first()) // 使用第一个语言作为TTS语言
@ -2549,16 +2542,18 @@ object AgentService : CoroutineScope {
for ((key, value) in actions) {
actionsObject.put(key, value)
}
jsonObject.put("actions", actionsObject)
jsonObject.put("usagetype", 3) // 3 表示AICHAT
jsonObject.put("usages", actionsObject)
val json = jsonObject.toString()
Log.d(TAG, "统计事件发送成功,请求: $json")
val mediaType = "application/json".toMediaType()
val body = json.toRequestBody(mediaType)
// 构建请求
val request = Request.Builder()
.url("$serverurl/api/home/user_pushaction")
.url("$serverurl/api/home/user_usages")
.addHeader("Authorization", usertoken)
.addHeader("Content-Type", "application/json")
.post(body)
@ -2573,8 +2568,42 @@ object AgentService : CoroutineScope {
override fun onResponse(call: Call, response: Response) {
response.use {
// 仅读取一次响应体,避免重复读取导致异常,同时统一后续处理使用
val bodyStr = response.body?.string() ?: ""
if (response.isSuccessful) {
Log.d(TAG, "统计事件发送成功")
Log.d(TAG, "统计事件发送成功,响应: $bodyStr")
try {
val json = JSONObject(bodyStr)
val code = json.optInt("code", -1)
val msg = json.optString("msg", "")
val dataObj = json.optJSONObject("data")
// 解析 data.usages 为 Map<String, Int>
val dataMap = mutableMapOf<String, Int>()
val usagesObj = dataObj?.optJSONObject("usages")
if (usagesObj != null) {
val keys = usagesObj.keys()
while (keys.hasNext()) {
val k = keys.next()
dataMap[k] = usagesObj.optInt(k, 0)
}
for ((k, v) in dataMap) {
Log.d(TAG, "统计事件发送成功,响应: $k -> $v")
if(k == "3"){ //AI积分更新
aiintegral = v
// 广播一个细化事件,便于前端单独处理 AI 积分更新
sendEvent(
"assistant_usages",
mapOf(
"integral" to aiintegral
)
)
}
}
}
} catch (e: Exception) {
Log.w(TAG, "解析统计响应失败: ${e.message}")
}
} else {
Log.w(TAG, "统计事件发送失败,HTTP状态码: ${response.code}")
}
@ -2619,4 +2648,4 @@ object AgentService : CoroutineScope {
Log.e(TAG, "保存 _isInterrupt 缓存状态时发生异常: ${e.message}", e)
}
}
}
}

7
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt

@ -70,6 +70,7 @@ class BleMediaService : Service(), CoroutineScope {
private const val APP_NAVIGATION_MODE = "navigationMode"
private const val APP_CALLING_TOOL = "callingTool"
private const val KEY_AI_INTAGRAL = "aiintegral"
// 用于跟踪服务是否正在运行
private val isRunning = AtomicBoolean(false)
@ -663,11 +664,13 @@ private var companionManager: BleCompanionManager? = null
private fun getConfigString(prefs: SharedPreferences, key: String, defaultValue: String): String {
return prefs.getString(key, defaultValue) ?: defaultValue
}
/**
* 从SharedPreferences获取字符串配置
*/
private fun getConfigInt(prefs: SharedPreferences, key: String, defaultValue: Int): Int {
return prefs.getInt(key, defaultValue)
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() {

5
local_plugins/agent_service/lib/agent_service.dart

@ -59,6 +59,9 @@ enum AgentServiceEventType {
/// 函数调用结果
functionCallResult,
/// AI助手使用统计
assistantUsages,
/// 图片处理中
imageProcessing,
@ -167,6 +170,8 @@ class AgentService {
return AgentServiceEventType.functionCall;
case 'function_call_result':
return AgentServiceEventType.functionCallResult;
case 'assistant_usages':
return AgentServiceEventType.assistantUsages;
case 'image_processing':
return AgentServiceEventType.imageProcessing;
case 'image_ready':

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

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

48
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt

@ -8,9 +8,6 @@ import android.net.Uri
import android.provider.Settings
import android.util.Log
import androidx.core.content.ContextCompat
import com.aallam.openai.api.chat.ChatMessage
import com.aallam.openai.api.chat.TextContent
import com.aallam.openai.api.chat.ListContent
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.sse.*
@ -166,47 +163,4 @@ fun roughTokenCount(text: String): Int {
val englishCharCount = trimmed.length - chineseCharCount
return chineseCharCount + (englishCharCount / 4) + 1
}
/**
* 估算消息列表的总 token 数量(近似)
* @param messages List of message map: role: user/assistant, content: string or array
* @return 估算 token 数
*/
fun estimatePromptTokens(messages: List<ChatMessage>): Int {
var totalText = ""
for (message in messages) {
when (val content = message.messageContent) {
is TextContent? -> {
totalText += content?.content + " "
}
is ListContent -> {
// 处理多模态内容数组
content.content.forEach { item ->
if (item is Map<*, *>) {
val type = item["type"] as? String
if (type == "text") {
val text = item["text"] as? String ?: ""
totalText += text + " "
}else if(type == "image_url"){
val text = item["image_url"] as? String ?: ""
totalText += text + " "
}
}
}
}
}
}
return roughTokenCount(totalText)
}
//fun estimatePromptTokens(messages: List<ChatMessage>): Int {
// var tokens = 0
// for (msg in messages) {
// tokens += 4 // 每条消息结构开销
// tokens += roughTokenCount(msg.role.role.lowercase()) // ChatRole 枚举转小写字符串
// tokens += roughTokenCount(msg.content ?: "")
// }
// tokens += 2 // 结尾 assistant 回复结构开销
// return tokens
//}
}
Loading…
Cancel
Save