mirror of
https://github.com/jie65535/JChatGPT.git
synced 2026-05-04 22:33:35 +08:00
Add token consumption tracking system Update version to v1.10.0
This commit is contained in:
@@ -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 ->
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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>())
|
||||
|
||||
/**
|
||||
* 添加对话记忆
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user