Browse Source

上传本地使用量统计

newdev_shunjiawei
liwei1dao 1 year ago
parent
commit
70b665a733
  1. 8
      lib/data/services/ble_manager.dart
  2. 1
      local_plugins/agent_service/android/build.gradle.kts
  3. 63
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  4. 3
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  5. 10
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt
  6. 16
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt
  7. 33
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt

8
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<bool> startAgentService() async {
try {
// 从.env文件直接读取环境变量
// final azureSpeechKey = dotenv.env['AZURE_SPEECH_KEY'] ?? '';
final serverurl = dotenv.env['SERVER_URL'] ?? '';
var jsonString = _storage.read<String>("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
};
// 记录日志(不记录敏感信息)

1
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"))

63
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<String,Int>) {
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()
}
}

3
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) {

10
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, "")
)
}

16
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) {

33
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<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