diff --git a/README.md b/README.md index 655382c..bd9f113 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ JChatGPT 是一个基于 Kotlin 的 Mirai Console 插件,它将大型语言模 - **多模型支持**:支持聊天模型、推理模型和视觉模型 - **丰富的工具系统**:包括网络搜索、代码执行、图像识别、群管理等 - **上下文记忆**:支持持久化记忆存储 +- **好感度系统**:基于用户行为的好感度管理机制 - **LaTeX 渲染**:自动将数学表达式渲染为图片 - **灵活的触发方式**:@机器人、关键字触发、回复消息等 - **权限控制**:细粒度的权限管理系统 @@ -40,6 +41,8 @@ AI 可以自动调用多种工具来完成复杂任务: - `/jgpt enable ` - 启用目标对话权限 - `/jgpt disable ` - 禁用目标对话权限 - `/jgpt reload` - 重载配置文件 +- `/jgpt favorability ` - 设置指定用户的好感度值(-100~100) +- `/jgpt resetFavorability` - 重置所有用户的好感度 ## 配置文件 @@ -108,6 +111,10 @@ callKeyword: '[小筱][林淋月玥]' showToolCallingMessage: true # 是否启用记忆编辑功能,记忆存在data目录,提示词中需要加上{memory}来填充记忆,每个群都有独立记忆 memoryEnabled: true +# 是否启用好感度系统 +enableFavorabilitySystem: true +# 好感度每日基础偏移速度(点/天) +favorabilityBaseShiftSpeed: 2.0 ``` ## 系统提示词 @@ -250,6 +257,33 @@ JChatGPT 默认配置为使用阿里云百炼平台的通义千问系列模型 9. **ImageEdit** - 图像编辑 10. **WeatherService** - 天气查询 +## 好感度系统 + +JChatGPT 插件包含一个可选的好感度系统,用于根据用户行为调整机器人对用户的好感度。该系统有以下特性: + +### 核心机制 +- 好感度值范围:-100(完全不理会)到 100(非常好的朋友) +- 负好感度用户有一定概率不会收到回复,概率为好感度绝对值的百分比 +- 好感度会随时间向0偏移,偏移速度与当前好感度绝对值相关 + +### 好感度调整规则 +- 问正经问题:+好感度 +- 问无聊问题:-好感度 +- 骂人:直接降至-100 + +### 时间偏移机制 +好感度会随时间自然向0回归: +- 偏移公式:偏移量 = sign(好感度) * (1 - (|好感度| / 100)^2) * 基础偏移速度 +- 极端值变化缓慢,-100可能需要好几天才消气,100可能好多天都不会降低 + +### 管理命令 +- `/jgpt favorability ` - 设置指定用户的好感度值(-100~100) +- `/jgpt resetFavorability` - 重置所有用户的好感度 + +### 配置选项 +- `enableFavorabilitySystem` - 是否启用好感度系统(默认:true) +- `favorabilityBaseShiftSpeed` - 好感度每日基础偏移速度(点/天,默认:2.0) + ## 部署要求 - Java 11 或更高版本 diff --git a/build.gradle.kts b/build.gradle.kts index b2500f4..7fe35e8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { } group = "top.jie65535.mirai" -version = "1.8.0" +version = "1.9.0" mirai { jvmTarget = JavaVersion.VERSION_11 diff --git a/src/main/kotlin/JChatGPT.kt b/src/main/kotlin/JChatGPT.kt index d371360..38da2d3 100644 --- a/src/main/kotlin/JChatGPT.kt +++ b/src/main/kotlin/JChatGPT.kt @@ -29,7 +29,6 @@ import net.mamoe.mirai.event.events.GroupMessageEvent import net.mamoe.mirai.event.events.MessageEvent import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.Image.Key.queryUrl -import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource import net.mamoe.mirai.utils.info import top.jie65535.mirai.tools.* import xyz.cssxsh.mirai.hibernate.MiraiHibernateRecorder @@ -40,13 +39,16 @@ import java.time.ZoneOffset import java.time.format.DateTimeFormatter import kotlin.collections.* import kotlin.math.max +import kotlin.math.pow +import kotlin.math.sign import kotlin.time.Duration.Companion.seconds +import kotlin.time.Duration.Companion.hours object JChatGPT : KotlinPlugin( JvmPluginDescription( id = "top.jie65535.mirai.JChatGPT", name = "J ChatGPT", - version = "1.8.0", + version = "1.9.0", ) { author("jie65535") // dependsOn("xyz.cssxsh.mirai.plugin.mirai-hibernate-plugin", true) @@ -94,6 +96,16 @@ object JChatGPT : KotlinPlugin( GlobalEventChannel.parentScope(this) .subscribeAlways { event -> onMessage(event) } + // 启动定时任务处理好感度时间偏移 + if (PluginConfig.enableFavorabilitySystem) { + launch { + while (true) { + delay(24.hours) // 每24小时执行一次 + shiftFavorabilityOverTime() + } + } + } + logger.info { "Plugin loaded" } } @@ -132,6 +144,24 @@ object JChatGPT : KotlinPlugin( && event.message[QuoteReply]?.source?.fromId != event.bot.id) return + // 好感度系统检查 + if (PluginConfig.enableFavorabilitySystem) { + val userId = event.sender.id + PluginData.userFavorability[userId]?.let { favorabilityInfo -> + val favorability = favorabilityInfo.value + if (favorability < 0) { + // 负好感度有一定概率不回复 + val probability = kotlin.math.abs(favorability).toDouble() / 100.0 + if (kotlin.random.Random.nextDouble() < probability) { + // 不回复此消息 + logger.info("根据好感度系统,用户 ${event.senderName}($userId) (好感度: $favorability) 的消息被忽略,忽略概率: ${probability * 100}%") + event.subject.sendMessage("[实验功能] 因好感度低,此消息已被忽略(${probability * 100}%)") + return + } + } + } + } + startChat(event) } @@ -212,12 +242,38 @@ object JChatGPT : KotlinPlugin( val historyText = StringBuilder() var lastId = 0L if (event is GroupMessageEvent) { + if (PluginConfig.enableFavorabilitySystem) { + val favorabilityInfos = history.map { it.fromId } + .filter { it != event.bot.id } + .distinct() + .mapNotNull { PluginData.userFavorability[it] } + if (favorabilityInfos.isNotEmpty()) { + historyText.appendLine("## 相关成员的好感信息") + for (info in favorabilityInfos) { + historyText.append(getNameCard(event.group, info.userId)).append('\t') + .appendLine(info).appendLine() + } + historyText.appendLine("---").appendLine() + } + } + + historyText.appendLine("## 近期群消息(更早已隐藏)") for (record in history) { // 同一人发言不要反复出现这人的名字,减少上下文 appendGroupMessageRecord(historyText, record, event, lastId != record.fromId) lastId = record.fromId } } else { + if (PluginConfig.enableFavorabilitySystem) { + val favorabilityInfo = PluginData.userFavorability[event.sender.id] + if (favorabilityInfo != null) { + historyText.append("你对\"").append(event.senderName).append("\"的好感信息如下: ") + .appendLine(favorabilityInfo).appendLine() + historyText.appendLine("---").appendLine() + } + } + + historyText.appendLine("## 近期对话(更早已隐藏)") for (record in history) { // 同一人发言不要反复出现这人的名字,减少上下文 appendMessageRecord(historyText, record, event, lastId != record.fromId) @@ -241,6 +297,9 @@ object JChatGPT : KotlinPlugin( showSender: Boolean, ) { if (showSender) { + // 名字前空行 + historyText.appendLine() + // 名称显示 if (event.bot.id == record.fromId) { historyText.append("**你** " + getNameCard(event.subject.botAsMember)) } else { @@ -476,14 +535,15 @@ object JChatGPT : KotlinPlugin( if (!done) { history.add(ChatMessage.User( buildString { - appendLine("系统提示:本次运行最多还剩${retry-1}轮。") + appendLine("## 系统提示") + append("本次运行最多还剩").append(retry-1).appendLine("轮。") appendLine("如果要多次发言,可以一次性调用多次发言工具。") appendLine("如果没有什么要做的,可以提前结束。") appendLine("当前时间:" + dateTimeFormatter.format(OffsetDateTime.now())) val newMessages = getAfterHistory(startedAt, event) if (newMessages.isNotEmpty()) { - append("以下是上次运行至今的新消息\n\n$newMessages") + append("## 以下是上次运行至今的新消息\n\n$newMessages") } } )) @@ -494,7 +554,7 @@ object JChatGPT : KotlinPlugin( } else { done = false logger.warning("调用llm时发生异常,重试中", e) - event.subject.sendMessage("出错了...正在重试...") + // event.subject.sendMessage("出错了...正在重试...") } } } while (!done && 0 < --retry) @@ -611,6 +671,9 @@ object JChatGPT : KotlinPlugin( // 天气服务 WeatherService(), + // 好感度调整 + AdjustUserFavorabilityAgent(), + // Epic 免费游戏 // EpicFreeGame(), @@ -678,7 +741,7 @@ object JChatGPT : KotlinPlugin( logger.warning("获取群头衔失败", e) } // 群名片 - nameCard.append("】 ").append(member.nameCardOrNick).append("(").append(member.id).append(")") + nameCard.append("】\t\"").append(member.nameCardOrNick).append("\"\t(qq=").append(member.id).append(")") return nameCard.toString() } @@ -719,4 +782,41 @@ object JChatGPT : KotlinPlugin( return result } + /** + * 好感度时间偏移处理函数 + * 使好感度逐渐向0回归,偏移速度与当前好感度绝对值相关 + */ + private fun shiftFavorabilityOverTime() { + logger.info("开始执行好感度时间偏移处理") + + val iterator = PluginData.userFavorability.iterator() + while (iterator.hasNext()) { + val entry = iterator.next() + val userId = entry.key + val favorabilityInfo = entry.value + val currentFavorability = favorabilityInfo.value + + // 计算偏移量 + // 偏移公式:偏移量 = sign(好感度) * (1 - (|好感度| / 100)^2) * 基础偏移速度 + val sign = sign(currentFavorability.toFloat()).toInt() + val absFavorability = kotlin.math.abs(currentFavorability) + val shiftAmount = sign * (1 - (absFavorability / 100.0).pow(2)) * PluginConfig.favorabilityBaseShiftSpeed + + // 更新好感度 + val newFavorability = (currentFavorability - shiftAmount).toInt().coerceIn(-100, 100) + + // 如果新的好感度为0,则移除该条目以节省空间 + if (newFavorability == 0) { + iterator.remove() + logger.info("用户 $userId 的好感度已回归0,移除记录") + } else { + // 创建新的好感度信息,保持原因和印象不变 + val newInfo = favorabilityInfo.copy(value = newFavorability) + PluginData.userFavorability[userId] = newInfo + logger.info("用户 $userId 的好感度 ($currentFavorability -> $newFavorability)") + } + } + + logger.info("好感度时间偏移处理完成") + } } \ No newline at end of file diff --git a/src/main/kotlin/PluginCommands.kt b/src/main/kotlin/PluginCommands.kt index 154c2d9..f0d7fb1 100644 --- a/src/main/kotlin/PluginCommands.kt +++ b/src/main/kotlin/PluginCommands.kt @@ -2,6 +2,7 @@ package top.jie65535.mirai import net.mamoe.mirai.console.command.CommandSender import net.mamoe.mirai.console.command.CompositeCommand +import net.mamoe.mirai.console.permission.PermissionService.Companion.cancel import net.mamoe.mirai.console.permission.PermissionService.Companion.permit import net.mamoe.mirai.console.permission.PermitteeId.Companion.permitteeId import net.mamoe.mirai.contact.Contact @@ -9,15 +10,15 @@ 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 top.jie65535.mirai.JChatGPT.save object PluginCommands : CompositeCommand( JChatGPT, "jgpt", description = "J OpenAI ChatGPT" ) { + @SubCommand - suspend fun CommandSender.setToken(token: String) { - PluginConfig.openAiToken = token - PluginConfig.save() + suspend fun CommandSender.reload() { + PluginConfig.reload() + PluginData.reload() LargeLanguageModels.reload() sendMessage("OK") } @@ -35,24 +36,34 @@ object PluginCommands : CompositeCommand( @SubCommand suspend fun CommandSender.disable(contact: Contact) { when (contact) { - is Member -> contact.permitteeId.permit(JChatGPT.chatPermission) - is User -> contact.permitteeId.permit(JChatGPT.chatPermission) - is Group -> contact.permitteeId.permit(JChatGPT.chatPermission) + is Member -> contact.permitteeId.cancel(JChatGPT.chatPermission, false) + is User -> contact.permitteeId.cancel(JChatGPT.chatPermission, false) + is Group -> contact.permitteeId.cancel(JChatGPT.chatPermission, false) } sendMessage("OK") } - @SubCommand - suspend fun CommandSender.reload() { - PluginConfig.reload() - PluginData.reload() - LargeLanguageModels.reload() - sendMessage("OK") - } - @SubCommand suspend fun CommandSender.clearMemory() { PluginData.contactMemory.clear() sendMessage("OK") } + + @SubCommand + suspend fun CommandSender.setFavor(user: User, value: Int) { + // 限制好感度值在-100到100之间 + val clampedValue = value.coerceIn(-100, 100) + // 获取当前的好感度信息 + val currentInfo = PluginData.userFavorability[user.id] ?: FavorabilityInfo(user.id) + // 创建新的好感度信息,保持原因和印象不变 + val newInfo = currentInfo.copy(value = clampedValue) + PluginData.userFavorability[user.id] = newInfo + sendMessage("OK") + } + + @SubCommand + suspend fun CommandSender.clearFavor() { + PluginData.userFavorability.clear() + sendMessage("OK") + } } \ No newline at end of file diff --git a/src/main/kotlin/PluginConfig.kt b/src/main/kotlin/PluginConfig.kt index 06adb25..77dcc7e 100644 --- a/src/main/kotlin/PluginConfig.kt +++ b/src/main/kotlin/PluginConfig.kt @@ -98,4 +98,10 @@ object PluginConfig : AutoSavePluginConfig("Config") { @ValueDescription("是否启用记忆编辑功能,记忆存在data目录,提示词中需要加上{memory}来填充记忆,每个群都有独立记忆") val memoryEnabled by value(true) + + @ValueDescription("是否启用好感度系统") + val enableFavorabilitySystem by value(true) + + @ValueDescription("好感度每日基础偏移速度(点/天)") + val favorabilityBaseShiftSpeed by value(2.0) } \ No newline at end of file diff --git a/src/main/kotlin/PluginData.kt b/src/main/kotlin/PluginData.kt index 38ce9ea..a24204d 100644 --- a/src/main/kotlin/PluginData.kt +++ b/src/main/kotlin/PluginData.kt @@ -1,14 +1,52 @@ package top.jie65535.mirai +import kotlinx.serialization.Serializable import net.mamoe.mirai.console.data.AutoSavePluginData import net.mamoe.mirai.console.data.value +/** + * 好感度信息数据类 + * @param userId QQ + * @param value 好感度值 (-100 ~ 100) + * @param reasons 调整原因列表,用于溯源 + * @param impression 对用户的印象/画像 + */ +@Serializable +data class FavorabilityInfo( + val userId: Long, + val value: Int = 0, + val reasons: List = emptyList(), + val impression: String = "" +) { + override fun toString(): String { + return buildString { + append("好感度:$value") + if (impression.isNotEmpty()) { + append("\t印象:$impression") + } + if (reasons.isNotEmpty()) { + appendLine("\t调整原因:") + reasons.forEach { reason -> + appendLine("* $reason") + } + } + } + } +} + object PluginData : AutoSavePluginData("data") { /** * 联系人记忆 */ val contactMemory by value(mutableMapOf()) + /** + * 用户好感度数据 + * Key: 用户QQ号 + * Value: 好感度信息 + */ + val userFavorability by value(mutableMapOf()) + /** * 添加对话记忆 */ diff --git a/src/main/kotlin/tools/AdjustUserFavorabilityAgent.kt b/src/main/kotlin/tools/AdjustUserFavorabilityAgent.kt new file mode 100644 index 0000000..e52e90d --- /dev/null +++ b/src/main/kotlin/tools/AdjustUserFavorabilityAgent.kt @@ -0,0 +1,103 @@ +package top.jie65535.mirai.tools + +import com.aallam.openai.api.chat.Tool +import com.aallam.openai.api.core.Parameters +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.add +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.longOrNull +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonArray +import net.mamoe.mirai.event.events.MessageEvent +import top.jie65535.mirai.JChatGPT +import top.jie65535.mirai.PluginData +import top.jie65535.mirai.FavorabilityInfo +import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter + +class AdjustUserFavorabilityAgent : BaseAgent( + tool = Tool.function( + name = "adjustUserFavorability", + description = "可根据网友行为调整对其好感度,范围从-100到100。" + + "默认为0表示陌生人,100表示非常好的朋友,-100表示已拉黑。" + + "当好感度低于0时,有一定概率忽略该用户的消息,-100则100%忽略其消息。", + parameters = Parameters.buildJsonObject { + put("type", "object") + put("properties", buildJsonObject { + put("userId", buildJsonObject { + put("type", "integer") + put("description", "用户QQ号") + }) + put("change", buildJsonObject { + put("type", "integer") + put("description", "好感度变化值(可正可负)") + }) + put("reason", buildJsonObject { + put("type", "string") + put("description", "调整原因(供日志记录和溯源)") + }) + put("impression", buildJsonObject { + put("type", "string") + put("description", "对用户的印象或称呼(可选)") + }) + }) + putJsonArray("required") { + add("userId") + add("change") + add("reason") + } + } + ) +) { + override suspend fun execute(args: JsonObject?, event: MessageEvent): String { + requireNotNull(args) + + val userId = args["userId"]?.jsonPrimitive?.longOrNull + val change = args["change"]?.jsonPrimitive?.intOrNull + val reason = args["reason"]?.jsonPrimitive?.contentOrNull + val impression = args["impression"]?.jsonPrimitive?.contentOrNull + + if (userId == null || change == null || reason == null) { + return "错误:userId、change和reason参数不能为空" + } + + // 获取当前好感度信息 + val currentInfo = PluginData.userFavorability[userId] ?: FavorabilityInfo(userId) + val currentValue = currentInfo.value + + // 计算新的好感度值,限制在-100~100范围内 + val newValue = (currentValue + change).coerceIn(-100, 100) + + // 更新原因列表 + val timeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") + val newReason = "${timeFormatter.format(OffsetDateTime.now())}: $reason" + val newReasons = if (currentInfo.reasons.size >= 10) { + // 保留最近的10条原因记录 + (currentInfo.reasons.drop(1) + newReason) + } else { + (currentInfo.reasons + newReason) + } + + // 更新印象/画像 + val newImpression = impression ?: currentInfo.impression + + // 创建新的好感度信息 + val newInfo = FavorabilityInfo( + userId = userId, + value = newValue, + reasons = newReasons, + impression = newImpression + ) + + // 更新好感度 + PluginData.userFavorability[userId] = newInfo + + // 记录日志 + JChatGPT.logger.info("用户 $userId 的好感度 ($currentValue -> $newValue),原因:$reason") + + return "用户 $userId 的好感度已更新为 $newValue" + } +} \ No newline at end of file diff --git a/src/main/kotlin/tools/SendLaTeXExpression.kt b/src/main/kotlin/tools/SendLaTeXExpression.kt index 435ac98..3fe8650 100644 --- a/src/main/kotlin/tools/SendLaTeXExpression.kt +++ b/src/main/kotlin/tools/SendLaTeXExpression.kt @@ -10,7 +10,7 @@ import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource class SendLaTeXExpression : BaseAgent( tool = Tool.function( name = "sendLaTeXExpression", - description = "发送LaTeX数学表达式,将其渲染为图片并发送", + description = "发送LaTeX数学表达式,将其渲染为图片并发送。(暂不支持中文)", parameters = Parameters.buildJsonObject { put("type", "object") putJsonObject("properties") {