Add token consumption tracking system Update version to v1.10.0

This commit is contained in:
2026-03-18 12:09:08 +08:00
parent c4afdb811b
commit b0c985e220
6 changed files with 426 additions and 5 deletions

View File

@@ -5,6 +5,7 @@ import com.aallam.openai.api.chat.ChatCompletionRequest
import com.aallam.openai.api.chat.ChatMessage
import com.aallam.openai.api.chat.ChatRole
import com.aallam.openai.api.chat.ToolCall
import com.aallam.openai.api.core.Usage
import com.aallam.openai.api.model.ModelId
import io.ktor.util.collections.*
import kotlinx.coroutines.Deferred
@@ -51,7 +52,7 @@ object JChatGPT : KotlinPlugin(
JvmPluginDescription(
id = "top.jie65535.mirai.JChatGPT",
name = "J ChatGPT",
version = "1.9.0",
version = "1.10.0",
) {
author("jie65535")
// dependsOn("xyz.cssxsh.mirai.plugin.mirai-hibernate-plugin", true)
@@ -548,6 +549,7 @@ object JChatGPT : KotlinPlugin(
var responseMessageBuilder: StringBuilder? = null
val responseToolCalls = mutableListOf<ToolCall.Function>()
val toolCallTasks = mutableListOf<Deferred<ChatMessage>>()
var lastTokenUsage: Usage? = null
// 处理聊天流式响应
responseFlow.collect { chunk ->
val delta = chunk.choices[0].delta ?: return@collect
@@ -609,6 +611,9 @@ object JChatGPT : KotlinPlugin(
}
}
}
// 捕获token使用量
chunk.usage?.let { lastTokenUsage = it }
}
// 移除思考内容
@@ -622,6 +627,23 @@ object JChatGPT : KotlinPlugin(
)
)
// 记录token使用量
lastTokenUsage?.let { usage ->
val now = OffsetDateTime.now().toEpochSecond()
val groupId = if (event is GroupMessageEvent) event.subject.id else null
val record = TokenUsageRecord(
timestamp = now,
userId = event.sender.id,
userNickname = event.senderName,
groupId = groupId,
model = PluginConfig.chatModel,
promptTokens = usage.promptTokens ?: 0,
completionTokens = usage.completionTokens ?: 0,
totalTokens = usage.totalTokens ?: 0
)
PluginData.tokenUsageRecords.add(record)
}
// 处理最后一个工具调用
if (responseToolCalls.size > toolCallTasks.size) {
val toolCallMessage = responseToolCalls.last().let { toolCall ->

View File

@@ -10,6 +10,10 @@ import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.contact.User
import top.jie65535.mirai.JChatGPT.reload
import java.time.Instant
import java.time.ZoneId
import java.time.LocalDate
import java.time.format.DateTimeFormatter
object PluginCommands : CompositeCommand(
JChatGPT, "jgpt", description = "J OpenAI ChatGPT"
@@ -72,4 +76,132 @@ object PluginCommands : CompositeCommand(
JChatGPT.clearContextCache()
sendMessage("已清空所有对话上下文缓存")
}
@SubCommand
suspend fun CommandSender.tokens() {
sendMessage("请使用子命令daily, users, groups, query")
}
@SubCommand
suspend fun CommandSender.tokensDaily(days: Int = 7) {
val now = Instant.now().epochSecond
val secondsPerDay = 86400
val cutoff = now - (days * secondsPerDay)
val dailyStats = PluginData.tokenUsageRecords
.filter { it.timestamp >= cutoff }
.groupBy {
LocalDate.ofInstant(
Instant.ofEpochSecond(it.timestamp),
ZoneId.systemDefault()
)
}
.mapValues { (_, records) ->
records.sumOf { it.totalTokens }
}
.toSortedMap()
if (dailyStats.isEmpty()) {
sendMessage("指定时间范围内无使用记录")
return
}
val response = buildString {
appendLine("最近 $days 天 Token 使用统计:")
appendLine()
dailyStats.forEach { (date, total) ->
appendLine("$date: $total tokens")
}
}
sendMessage(response)
}
@SubCommand
suspend fun CommandSender.tokensUsers(limit: Int = 10) {
val userStats = PluginData.tokenUsageRecords
.groupBy { it.userId }
.mapValues { (_, records) ->
Pair(
records.first().userNickname,
records.sumOf { it.totalTokens }
)
}
.toList()
.sortedByDescending { it.second.second }
.take(limit)
if (userStats.isEmpty()) {
sendMessage("暂无使用记录")
return
}
val response = buildString {
appendLine("Token 使用排名 Top $limit")
appendLine()
userStats.forEach {
appendLine("- ${it.second.first}(${it.first}): ${it.second.second} tokens")
}
}
sendMessage(response)
}
@SubCommand
suspend fun CommandSender.tokensGroups(limit: Int = 10) {
val groupStats = PluginData.tokenUsageRecords
.filter { it.groupId != null }
.groupBy { it.groupId!! }
.mapValues { (_, records) ->
records.sumOf { it.totalTokens }
}
.toList()
.sortedByDescending { it.second }
.take(limit)
if (groupStats.isEmpty()) {
sendMessage("暂无群组使用记录")
return
}
val response = buildString {
appendLine("群组 Token 使用排名 Top $limit")
appendLine()
groupStats.forEach { (groupId, total) ->
appendLine("- $groupId: $total tokens")
}
}
sendMessage(response)
}
@SubCommand
suspend fun CommandSender.tokensQuery(userId: Long?, days: Int = 7) {
val now = Instant.now().epochSecond
val cutoff = now - (days * 86400)
val filtered = PluginData.tokenUsageRecords
.filter { it.timestamp >= cutoff }
.filter { userId == null || it.userId == userId }
.sortedByDescending { it.timestamp }
.take(20)
if (filtered.isEmpty()) {
sendMessage("指定时间范围内无使用记录")
return
}
val response = buildString {
appendLine("最近 $days 天使用记录最多显示20条")
appendLine()
filtered.forEach { record ->
val time = Instant.ofEpochSecond(record.timestamp)
.atZone(ZoneId.systemDefault())
.format(DateTimeFormatter.ofPattern("MM-dd HH:mm"))
val location = if (record.groupId != null) "${record.groupId}" else "私聊"
appendLine("[$time] $location - ${record.userNickname}")
appendLine(" 模型: ${record.model}, Tokens: ${record.totalTokens} " +
"(输入: ${record.promptTokens}, 输出: ${record.completionTokens})")
appendLine()
}
}
sendMessage(response)
}
}

View File

@@ -34,6 +34,29 @@ data class FavorabilityInfo(
}
}
/**
* Token使用记录数据类
* @param timestamp Unix时间戳
* @param userId 用户QQ号
* @param userNickname 用户昵称
* @param groupId 群号私聊时为null
* @param model 模型名称
* @param promptTokens 输入token数
* @param completionTokens 输出token数
* @param totalTokens 总token数
*/
@Serializable
data class TokenUsageRecord(
val timestamp: Long,
val userId: Long,
val userNickname: String,
val groupId: Long?,
val model: String,
val promptTokens: Int,
val completionTokens: Int,
val totalTokens: Int
)
object PluginData : AutoSavePluginData("data") {
/**
* 联系人记忆
@@ -47,6 +70,11 @@ object PluginData : AutoSavePluginData("data") {
*/
val userFavorability by value(mutableMapOf<Long, FavorabilityInfo>())
/**
* Token使用记录
*/
val tokenUsageRecords by value(mutableListOf<TokenUsageRecord>())
/**
* 添加对话记忆
*/