diff --git a/FavorabilitySystem.md b/FavorabilitySystem.md new file mode 100644 index 0000000..556d941 --- /dev/null +++ b/FavorabilitySystem.md @@ -0,0 +1,123 @@ +# 好感度系统功能规范 + +## 功能概述 +为机器人添加一个可开关的好感度系统,通过AI工具自动调整用户的好感度值。好感度数据将保存在插件数据中,以用户QQ号为键,包含好感度值和调整原因。 + +## 核心功能 + +### 1. 好感度数据存储 +- 在`PluginData`中新增一个映射来存储好感度数据 +- 键:用户QQ号(Long) +- 值:包含好感度值和调整原因的数据结构 +- 默认值:0(中立) + +### 2. 好感度变化规则 +- **问正经问题**:+好感度(例如:询问学习/工作相关问题、寻求帮助等) +- **问无聊问题**:-好感度(例如:骚扰机器人要求评价他人、攻击性言论、让机器人做无意义的事情、引战问题等) +- **骂人**:直接降至-100 +- **时间偏移**:好感度会随时间向0偏移,偏移速度与当前好感度绝对值相关 + - 好感度越高或越低,偏移速度越慢 + - 设计算法确保极端值变化缓慢(具体公式见实现细节) + +### 3. 回复概率机制 +- 当好感度为负数时,有一定概率不回复用户消息 +- 概率计算:好感度绝对值的百分比 + - 例如:好感度为-50,则有50%概率不回复(即50%概率回复) + +### 4. 好感度调整工具 +- 新增一个AI工具,允许AI根据对话内容自主调整用户的好感度 +- 工具名称:`adjustUserFavorability` +- 工具参数: + - `userId`: 用户QQ号 + - `change`: 好感度变化值(可正可负) + - `reason`: 调整原因(用于溯源) + - `impression`: 对用户的印象/画像(可选) + +### 5. 系统开关 +- 在配置文件中添加开关选项,控制是否启用好感度系统 +- 默认启用 + +### 6. 管理员命令 +- 添加插件命令手动修改某个人的好感度 +- 添加命令重置所有好感度 + +## 实现细节 + +### 1. 数据结构 +在`PluginData`中添加: +```kotlin +/** + * 用户好感度数据 + * Key: 用户QQ号 + * Value: 好感度信息 + */ +val userFavorability by value(mutableMapOf()) + +/** + * 好感度信息数据类 + * @param value 好感度值 (-100 ~ 100) + * @param reason 调整原因列表,用于溯源 + * @param impression 对用户的印象/画像 + */ +data class FavorabilityInfo( + val value: Int = 0, + val reasons: List = emptyList(), + val impression: String = "" +) +``` + +### 2. 好感度工具 +创建新的工具类`AdjustUserFavorabilityAgent`,继承`BaseAgent`。 +工具描述:`根据用户行为调整其好感度值,范围-100~100` + +### 3. 消息处理逻辑 +在`JChatGPT.kt`的`onMessage`函数中: +- 添加好感度系统开关检查 +- 在决定是否回复前,计算回复概率 +- 如果随机数小于不回复概率,则直接返回,不进行后续处理 + +### 4. 时间偏移机制 +设计时间偏移算法,使好感度逐渐向0回归: +- 偏移公式:`偏移量 = sign(好感度) * (1 - (|好感度| / 100)^2) * 基础偏移速度` +- 基础偏移速度可设置为每天1-5点 +- 这样确保当好感度接近极端值时,变化速度会显著减慢 + +### 5. 配置选项 +在`PluginConfig.kt`中添加: +```kotlin +/** + * 是否启用好感度系统 + */ +val enableFavorabilitySystem by value(true) + +/** + * 好感度每日基础偏移速度(点/天) + */ +val favorabilityBaseShiftSpeed by value(2.0) +``` + +### 6. 插件命令 +添加以下命令: +- `/jgpt favorability `: 设置指定QQ号的好感度值 +- `/jgpt resetFavorability`: 重置所有用户的好感度为0 + +### 7. 提示词设计 +不再使用系统提示词中的占位符,而是将好感度信息直接添加到聊天历史的顶部。 + +### 8. 好感度信息展示 +- 不再使用系统提示词中的占位符 +- 在获取历史消息时,将好感度信息作为摘要添加到聊天历史的顶部 +- 格式示例: +``` +[好感度摘要] +用户840465812(筱杰) 好感度: 75 +印象: 热心的开发者,经常提供有用的建议 +调整原因: +- 2025-09-10 14:30: 提供了关于代码优化的建议 +10 +- 2025-09-09 10:15: 帮助测试新功能 +5 +``` + +## 待确认事项 + +1. 时间偏移的基础速度设定(每天多少点) +2. 好感度调整工具的具体参数和使用方式 \ No newline at end of file diff --git a/README.md b/README.md index bd9f113..1c4dd59 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ JChatGPT 是一个基于 Kotlin 的 Mirai Console 插件,它将大型语言模 - **丰富的工具系统**:包括网络搜索、代码执行、图像识别、群管理等 - **上下文记忆**:支持持久化记忆存储 - **好感度系统**:基于用户行为的好感度管理机制 +- **Token消耗统计**:记录每次对话的Token消耗,支持多维度统计查询 - **LaTeX 渲染**:自动将数学表达式渲染为图片 - **灵活的触发方式**:@机器人、关键字触发、回复消息等 - **权限控制**:细粒度的权限管理系统 @@ -37,12 +38,22 @@ AI 可以自动调用多种工具来完成复杂任务: ## 命令列表 -- `/jgpt setToken ` - 设置 OpenAI API Token +### 基础命令 - `/jgpt enable ` - 启用目标对话权限 - `/jgpt disable ` - 禁用目标对话权限 - `/jgpt reload` - 重载配置文件 -- `/jgpt favorability ` - 设置指定用户的好感度值(-100~100) -- `/jgpt resetFavorability` - 重置所有用户的好感度 +- `/jgpt clearMemory` - 清空所有对话记忆 +- `/jgpt clearContextCache` - 清空所有对话上下文缓存 + +### 好感度管理 +- `/jgpt setFavor ` - 设置指定用户的好感度值(-100~100) +- `/jgpt clearFavor` - 重置所有用户的好感度 + +### Token统计 +- `/jgpt tokensDaily [days]` - 查看指定天数的每日Token消耗统计(默认7天) +- `/jgpt tokensUsers [limit]` - 查看Token消耗最多的用户排名(默认Top 10) +- `/jgpt tokensGroups [limit]` - 查看Token消耗最多的群组排名(默认Top 10) +- `/jgpt tokensQuery [userId] [days]` - 查询详细的使用记录(可按用户和时间过滤) ## 配置文件 @@ -284,6 +295,111 @@ JChatGPT 插件包含一个可选的好感度系统,用于根据用户行为 - `enableFavorabilitySystem` - 是否启用好感度系统(默认:true) - `favorabilityBaseShiftSpeed` - 好感度每日基础偏移速度(点/天,默认:2.0) +## Token消耗统计 + +JChatGPT 插件内置了Token消耗统计功能,可以记录每次对话的Token使用情况,并提供多维度统计查询。 + +### 功能特性 +- **自动记录**:每次对话自动记录Token消耗 +- **详细数据**:记录时间戳、用户、群组、模型、输入/输出Token数 +- **多维统计**:支持按日期、用户、群组进行统计 +- **灵活查询**:支持详细记录查询和过滤 + +### 记录内容 +每次对话记录包含以下信息: +- 时间戳(Unix timestamp) +- 用户QQ号和昵称 +- 群组ID(群聊)或null(私聊) +- 使用的模型名称 +- 输入Token数(promptTokens) +- 输出Token数(completionTokens) +- 总Token数(totalTokens) + +### 统计命令 + +#### 每日统计 +``` +/jgpt tokensDaily [days] +``` +- 显示指定天数内的每日Token消耗统计 +- 默认显示最近7天 +- 输出示例: + ``` + 最近 7 天 Token 使用统计: + + 2026-03-18: 15342 tokens + 2026-03-17: 12890 tokens + 2026-03-16: 9567 tokens + ``` + +#### 用户排名 +``` +/jgpt tokensUsers [limit] +``` +- 显示Token消耗最多的用户排名 +- 默认显示Top 10 +- 输出示例: + ``` + Token 使用排名 Top 10: + + 张三(QQ:123456): 25430 tokens + 李四(QQ:234567): 18920 tokens + 王五(QQ:345678): 12450 tokens + ``` + +#### 群组排名 +``` +/jgpt tokensGroups [limit] +``` +- 显示Token消耗最多的群组排名 +- 默认显示Top 10 +- 仅统计群聊对话,不包括私聊 +- 输出示例: + ``` + 群组 Token 使用排名 Top 10: + + 群 987654321: 45670 tokens + 群 876543210: 32100 tokens + 群 765432109: 28930 tokens + ``` + +#### 详细查询 +``` +/jgpt tokensQuery [userId] [days] +``` +- 查询详细的使用记录 +- 可按用户ID过滤(可选) +- 可指定时间范围(默认7天) +- 最多显示20条记录 +- 输出示例: + ``` + 最近 7 天使用记录(最多显示20条): + + [03-18 14:35] 群987654321 - 张三 + 模型: qwen-max, Tokens: 2345 (提示: 1234, 完成: 1111) + + [03-18 14:30] 私聊 - 李四 + 模型: qwen-max, Tokens: 1876 (提示: 980, 完成: 896) + ``` + +### 数据存储 +- Token记录保存在插件数据目录的 `data/data.json` 文件中 +- 使用 `AutoSavePluginData` 自动持久化 +- 记录永久保存,不会自动删除 +- 数据格式为JSON,可手动查看和备份 + +### 使用场景 +- **成本监控**:了解API调用成本,控制预算 +- **使用分析**:分析哪些用户或群组使用最频繁 +- **性能优化**:识别高消耗对话,优化提示词 +- **趋势分析**:观察使用趋势,规划资源 + +### 注意事项 +- 仅统计聊天模型的Token消耗 +- 推理模型和视觉模型的消耗不在统计范围内 +- 每次对话轮次都会单独记录 +- 统计数据基于实际API返回的Token数 + ## 部署要求 - Java 11 或更高版本 diff --git a/build.gradle.kts b/build.gradle.kts index 7fe35e8..9d82809 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { } group = "top.jie65535.mirai" -version = "1.9.0" +version = "1.10.0" mirai { jvmTarget = JavaVersion.VERSION_11 diff --git a/src/main/kotlin/JChatGPT.kt b/src/main/kotlin/JChatGPT.kt index d1b4508..69cf623 100644 --- a/src/main/kotlin/JChatGPT.kt +++ b/src/main/kotlin/JChatGPT.kt @@ -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() val toolCallTasks = mutableListOf>() + 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 -> diff --git a/src/main/kotlin/PluginCommands.kt b/src/main/kotlin/PluginCommands.kt index 6c5eb30..0698819 100644 --- a/src/main/kotlin/PluginCommands.kt +++ b/src/main/kotlin/PluginCommands.kt @@ -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) + } } \ No newline at end of file diff --git a/src/main/kotlin/PluginData.kt b/src/main/kotlin/PluginData.kt index a24204d..7da3ffc 100644 --- a/src/main/kotlin/PluginData.kt +++ b/src/main/kotlin/PluginData.kt @@ -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()) + /** + * Token使用记录 + */ + val tokenUsageRecords by value(mutableListOf()) + /** * 添加对话记忆 */