From 96ca24a82e142c137b62af7436a9ae840c74c88c Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Fri, 11 Jul 2025 15:57:49 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=9C=AC=E5=9C=B0=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E9=87=8F=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/services/ble_manager.dart | 8 ++- .../agent_service/android/build.gradle.kts | 1 + .../agent_service/AgentService.kt | 63 ++++++++++++++++++- .../agent_service/AgentServicePlugin.kt | 3 + .../agent_service/BleMediaService.kt | 10 ++- .../chat_api/ChatApiService.kt | 16 +++++ .../com/yunqiinnovation/chat_api/Utils.kt | 33 ++++++++++ 7 files changed, 130 insertions(+), 4 deletions(-) diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index 3cbc033c3..175ca4d9e 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -1,4 +1,6 @@ import 'dart:async'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + import '../../data/models/appconfig.dart'; import 'package:get/get.dart'; import 'package:ble_service/ble_service.dart'; @@ -517,7 +519,9 @@ class BleManager extends GetxService { Future startAgentService() async { try { // 从.env文件直接读取环境变量 - // final azureSpeechKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; + final serverurl = dotenv.env['SERVER_URL'] ?? ''; + var jsonString = _storage.read("logintoken"); + Logger.info('启动BLE服务,serverurl: ${serverurl}'); // final azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? 'eastasia'; // final openaiApiKey = dotenv.env['OPENAI_API_KEY'] ?? ''; // final openaiBaseUrl = @@ -557,6 +561,8 @@ class BleManager extends GetxService { 'mcpServer': jsonEncode(jsonData), 'volcanoAppId': '2316081933', 'volcanoToken': 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En', + 'serverurl': serverurl, + 'usertoken': jsonString }; // 记录日志(不记录敏感信息) diff --git a/local_plugins/agent_service/android/build.gradle.kts b/local_plugins/agent_service/android/build.gradle.kts index fc98a0bac..82eec6b49 100644 --- a/local_plugins/agent_service/android/build.gradle.kts +++ b/local_plugins/agent_service/android/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { implementation("androidx.media:media:1.6.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") implementation("androidx.security:security-crypto:1.1.0-alpha03") + implementation("com.squareup.okhttp3:okhttp:4.12.0") // 依赖本地其他插件 add("compileOnly", project(":azure_speech")) add("compileOnly", project(":chat_storage")) diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index db2e75132..37ebe489e 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -28,6 +28,9 @@ import com.deep_voice.speech.tts.TtsEventType import com.deep_voice.bytedance_speech.BytedanceTTS import java.util.UUID import com.yunqiinnovation.agent_service.Utils +import okhttp3.* +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody /** * 代理服务事件监听接口 @@ -72,6 +75,9 @@ object AgentService : CoroutineScope { // 会话ID,用于区分不同聊天上下文 private val sessionId = "default_agent" + // 服务端连接 + private var serverurl = "" + private var usertoken = "" // 历史聊天消息缓存 private val historyMessages = JSONArray() @@ -210,7 +216,9 @@ object AgentService : CoroutineScope { config["openaivisionModel"]?.toString() ?: "", config["mcpServer"]?.toString() ?: "" ) - + serverurl = config["serverurl"]?.toString() ?: ""; + usertoken = config["usertoken"]?.toString() ?: ""; + Log.e(TAG, "liwei -----------读取服务地址和用户token: $serverurl $usertoken") // 加载最近的聊天记录 loadChatHistory() @@ -783,6 +791,7 @@ object AgentService : CoroutineScope { var userMetadata = JSONObject() var reply = true; //是否回复 var broadcast = true //是否播发 + // 如果有图片,添加图片信息到元数据 if (hasImage) { userMetadata.put("hasImage", true) @@ -844,6 +853,26 @@ object AgentService : CoroutineScope { currsessionId, messagesWithSystemPrompt, object : StreamCallback { + + override fun onUsage( + sessionid: String, + prompt_tokens: Int?, + completion_tokens: Int?, + total_tokens: Int? + ) { + try { + sendStatisticsEvent( + mapOf( + "CHAT_PROMPT_TOKENS" to (prompt_tokens ?: 0), + "CHAT_COMPLETION_TOKENS" to (completion_tokens ?: 0), + "CHAT_TOTAL_TOKENS" to (total_tokens ?: 0) + ) + ) + }catch (e: Exception) { + Log.e(TAG, "liwei--------------- AI Call onUsage 异常", e) + } + } + override fun onToken(sessionid: String, token: String) { try { if (sessionid != currsessionId) { @@ -1634,4 +1663,36 @@ object AgentService : CoroutineScope { Log.i(TAG, "启动导航服务失败") } } + + + /* + * 发送统计事件 + * */ + fun sendStatisticsEvent(actions:Map) { + val client = OkHttpClient() + + // 外层 JSON 对象 + val jsonObject = JSONObject() + val actionsObject = JSONObject() + + for ((key, value) in actions) { + actionsObject.put(key, value) + } + + jsonObject.put("actions", actionsObject) + + val json = jsonObject.toString() + val mediaType = "application/json".toMediaType() + val body = json.toRequestBody(mediaType) + + val request = Request.Builder() + .url("$serverurl/api/home/user_pushaction") + .addHeader("Authorization", usertoken) // 添加 Token + .post(body) + .build() + Thread { + val response = client.newCall(request).execute() + // println("sendStatisticsEvent Response: ${response.body?.string()}") + }.start() + } } \ No newline at end of file diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt index 819bd6ad7..1a5d06561 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt @@ -71,6 +71,9 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream config["mcpServer"]?.toString()?.let { intent.putExtra("mcpServer", it) } config["volcanoAppId"]?.toString()?.let { intent.putExtra("volcanoAppId", it) } config["volcanoToken"]?.toString()?.let { intent.putExtra("volcanoToken", it) } + config["serverurl"]?.toString()?.let { intent.putExtra("serverurl", it) } + config["usertoken"]?.toString()?.let { intent.putExtra("usertoken", it) } + // 启动前台服务 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt index c31528263..ff4771fa6 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt @@ -48,7 +48,9 @@ class BleMediaService : Service(), CoroutineScope { private const val KEY_MCP_SERVER = "mcpServer" private const val KEY_VOLCANO_APP_ID = "volcanoAppId" private const val KEY_VOLCANO_TOKEN = "volcanoToken" - + + private const val KEY_SERVER_URL = "serverurl" + private const val KEY_USER_TOKEN = "usertoken" // 用于跟踪服务是否正在运行 private val isRunning = AtomicBoolean(false) @@ -171,6 +173,8 @@ class BleMediaService : Service(), CoroutineScope { it.getStringExtra(KEY_MCP_SERVER)?.let { value -> config[KEY_MCP_SERVER] = value } it.getStringExtra(KEY_VOLCANO_APP_ID)?.let { value -> config[KEY_VOLCANO_APP_ID] = value } it.getStringExtra(KEY_VOLCANO_TOKEN)?.let { value -> config[KEY_VOLCANO_TOKEN] = value } + it.getStringExtra(KEY_SERVER_URL)?.let { value -> config[KEY_SERVER_URL] = value } + it.getStringExtra(KEY_USER_TOKEN)?.let { value -> config[KEY_USER_TOKEN] = value } } Log.i(TAG, "从Intent中提取到配置参数: $config") return config @@ -204,7 +208,9 @@ class BleMediaService : Service(), CoroutineScope { KEY_SYSTEM_PROMPT to getConfigString(prefs, KEY_SYSTEM_PROMPT, "gpt-3.5-turbo"), KEY_MCP_SERVER to getConfigString(prefs, KEY_MCP_SERVER, ""), KEY_VOLCANO_APP_ID to getConfigString(prefs, KEY_VOLCANO_APP_ID, ""), - KEY_VOLCANO_TOKEN to getConfigString(prefs, KEY_VOLCANO_TOKEN, "") + KEY_VOLCANO_TOKEN to getConfigString(prefs, KEY_VOLCANO_TOKEN, ""), + KEY_SERVER_URL to getConfigString(prefs, KEY_SERVER_URL, ""), + KEY_USER_TOKEN to getConfigString(prefs, KEY_USER_TOKEN, "") ) } diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt index 45d5a0caf..7d7d50bbe 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt @@ -36,6 +36,10 @@ class ChatApiException(message: String) : Exception("ChatApiException: $message" * 与 iOS 版本 StreamCallback 协议保持完全一致 */ interface StreamCallback { + /** + * AI使用量 + * */ + fun onUsage(sessionid: String,prompt_tokens: Int?,completion_tokens: Int?,total_tokens: Int?) /** * 接收到新的token */ @@ -407,8 +411,14 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor Log.e("ChatApiService", "创建ChatCompletionRequest或调用chatCompletions失败: ${e.message}", e) throw e } + + var replyContext = ""; // Log.d(TAG, "liwei-------------------------开始AI 对话 7") chatsFlow.collect { result -> +// val usage = result.usage +// if (usage != null){ //发现流式不返回这歌数据 +// getSessionCallback(sessionid)?.onUsage(sessionid,usage.promptTokens,usage.completionTokens,usage.totalTokens) +// } // 每次处理前检查是否仍是当前会话 if (sessionid != currSessionId) { // throw CancellationException("Session cancelled") @@ -430,6 +440,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor // 处理普通文本内容 delta.content?.let { content -> try { + replyContext += content getSessionCallback(sessionid)?.onToken(sessionid,content) } catch (e: Exception) { Log.e(TAG, "onToken回调异常: ${e.message}", e) @@ -454,6 +465,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor val functionName = function.name if (!functionName.isNullOrEmpty()) { toolCallInfo.name = functionName + replyContext += functionName } } catch (e: Exception) { // 在流式响应中,name 可能在后续的 delta 中为 null,这是正常的 @@ -465,6 +477,7 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor val functionArgs = function.arguments if (!functionArgs.isNullOrEmpty()) { toolCallInfo.arguments += functionArgs + replyContext += functionArgs } } catch (e: Exception) { // 在流式响应中,arguments 可能在后续的 delta 中为 null,这是正常的 @@ -483,6 +496,9 @@ 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) // Log.d(TAG, "liwei-------------------------开始AI 对话 8") // 完成处理 if (sessionid == currSessionId) { diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt index 31b7bba05..cd1ab77f7 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt @@ -8,6 +8,7 @@ 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 io.ktor.client.* import io.ktor.client.engine.okhttp.* import io.ktor.client.plugins.sse.* @@ -148,4 +149,36 @@ fun checkPermissionAndGuide(context: Context, permission: String, permissionName return false } return true +} + + +/** + * 粗略计算一段文字的 token 数(每 4 字符 ≈ 1 token) + */ +fun roughTokenCount(text: String): Int { + val trimmed = text.trim() + if (trimmed.isEmpty()) return 0 + + // 中文字符(1字≈1token)+ 英文字符(4字符≈1token)混合估算 + val chineseCharCount = trimmed.count { it.code > 127 } + val englishCharCount = trimmed.length - chineseCharCount + + return chineseCharCount + (englishCharCount / 4) + 1 +} + + +/** + * 估算消息列表的总 token 数量(近似) + * @param messages List of message map: role: user/assistant, content: string + * @return 估算 token 数 + */ +fun estimatePromptTokens(messages: List): 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 } \ No newline at end of file