Add favorability system

Update version to v1.9.0
This commit is contained in:
2025-10-30 15:02:27 +08:00
parent 8ac0197fc4
commit 0f482407d4
8 changed files with 315 additions and 23 deletions

View File

@@ -7,6 +7,7 @@ JChatGPT 是一个基于 Kotlin 的 Mirai Console 插件,它将大型语言模
- **多模型支持**:支持聊天模型、推理模型和视觉模型 - **多模型支持**:支持聊天模型、推理模型和视觉模型
- **丰富的工具系统**:包括网络搜索、代码执行、图像识别、群管理等 - **丰富的工具系统**:包括网络搜索、代码执行、图像识别、群管理等
- **上下文记忆**:支持持久化记忆存储 - **上下文记忆**:支持持久化记忆存储
- **好感度系统**:基于用户行为的好感度管理机制
- **LaTeX 渲染**:自动将数学表达式渲染为图片 - **LaTeX 渲染**:自动将数学表达式渲染为图片
- **灵活的触发方式**@机器人、关键字触发、回复消息等 - **灵活的触发方式**@机器人、关键字触发、回复消息等
- **权限控制**:细粒度的权限管理系统 - **权限控制**:细粒度的权限管理系统
@@ -40,6 +41,8 @@ AI 可以自动调用多种工具来完成复杂任务:
- `/jgpt enable <contact>` - 启用目标对话权限 - `/jgpt enable <contact>` - 启用目标对话权限
- `/jgpt disable <contact>` - 禁用目标对话权限 - `/jgpt disable <contact>` - 禁用目标对话权限
- `/jgpt reload` - 重载配置文件 - `/jgpt reload` - 重载配置文件
- `/jgpt favorability <userId> <value>` - 设置指定用户的好感度值(-100~100
- `/jgpt resetFavorability` - 重置所有用户的好感度
## 配置文件 ## 配置文件
@@ -108,6 +111,10 @@ callKeyword: '[小筱][林淋月玥]'
showToolCallingMessage: true showToolCallingMessage: true
# 是否启用记忆编辑功能记忆存在data目录提示词中需要加上{memory}来填充记忆,每个群都有独立记忆 # 是否启用记忆编辑功能记忆存在data目录提示词中需要加上{memory}来填充记忆,每个群都有独立记忆
memoryEnabled: true memoryEnabled: true
# 是否启用好感度系统
enableFavorabilitySystem: true
# 好感度每日基础偏移速度(点/天)
favorabilityBaseShiftSpeed: 2.0
``` ```
## 系统提示词 ## 系统提示词
@@ -250,6 +257,33 @@ JChatGPT 默认配置为使用阿里云百炼平台的通义千问系列模型
9. **ImageEdit** - 图像编辑 9. **ImageEdit** - 图像编辑
10. **WeatherService** - 天气查询 10. **WeatherService** - 天气查询
## 好感度系统
JChatGPT 插件包含一个可选的好感度系统,用于根据用户行为调整机器人对用户的好感度。该系统有以下特性:
### 核心机制
- 好感度值范围:-100完全不理会到 100非常好的朋友
- 负好感度用户有一定概率不会收到回复,概率为好感度绝对值的百分比
- 好感度会随时间向0偏移偏移速度与当前好感度绝对值相关
### 好感度调整规则
- 问正经问题:+好感度
- 问无聊问题:-好感度
- 骂人:直接降至-100
### 时间偏移机制
好感度会随时间自然向0回归
- 偏移公式:偏移量 = sign(好感度) * (1 - (|好感度| / 100)^2) * 基础偏移速度
- 极端值变化缓慢,-100可能需要好几天才消气100可能好多天都不会降低
### 管理命令
- `/jgpt favorability <userId> <value>` - 设置指定用户的好感度值(-100~100
- `/jgpt resetFavorability` - 重置所有用户的好感度
### 配置选项
- `enableFavorabilitySystem` - 是否启用好感度系统默认true
- `favorabilityBaseShiftSpeed` - 好感度每日基础偏移速度(点/天默认2.0
## 部署要求 ## 部署要求
- Java 11 或更高版本 - Java 11 或更高版本

View File

@@ -7,7 +7,7 @@ plugins {
} }
group = "top.jie65535.mirai" group = "top.jie65535.mirai"
version = "1.8.0" version = "1.9.0"
mirai { mirai {
jvmTarget = JavaVersion.VERSION_11 jvmTarget = JavaVersion.VERSION_11

View File

@@ -29,7 +29,6 @@ import net.mamoe.mirai.event.events.GroupMessageEvent
import net.mamoe.mirai.event.events.MessageEvent import net.mamoe.mirai.event.events.MessageEvent
import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.message.data.Image.Key.queryUrl import net.mamoe.mirai.message.data.Image.Key.queryUrl
import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
import net.mamoe.mirai.utils.info import net.mamoe.mirai.utils.info
import top.jie65535.mirai.tools.* import top.jie65535.mirai.tools.*
import xyz.cssxsh.mirai.hibernate.MiraiHibernateRecorder import xyz.cssxsh.mirai.hibernate.MiraiHibernateRecorder
@@ -40,13 +39,16 @@ import java.time.ZoneOffset
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import kotlin.collections.* import kotlin.collections.*
import kotlin.math.max import kotlin.math.max
import kotlin.math.pow
import kotlin.math.sign
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
import kotlin.time.Duration.Companion.hours
object JChatGPT : KotlinPlugin( object JChatGPT : KotlinPlugin(
JvmPluginDescription( JvmPluginDescription(
id = "top.jie65535.mirai.JChatGPT", id = "top.jie65535.mirai.JChatGPT",
name = "J ChatGPT", name = "J ChatGPT",
version = "1.8.0", version = "1.9.0",
) { ) {
author("jie65535") author("jie65535")
// dependsOn("xyz.cssxsh.mirai.plugin.mirai-hibernate-plugin", true) // dependsOn("xyz.cssxsh.mirai.plugin.mirai-hibernate-plugin", true)
@@ -94,6 +96,16 @@ object JChatGPT : KotlinPlugin(
GlobalEventChannel.parentScope(this) GlobalEventChannel.parentScope(this)
.subscribeAlways<MessageEvent> { event -> onMessage(event) } .subscribeAlways<MessageEvent> { event -> onMessage(event) }
// 启动定时任务处理好感度时间偏移
if (PluginConfig.enableFavorabilitySystem) {
launch {
while (true) {
delay(24.hours) // 每24小时执行一次
shiftFavorabilityOverTime()
}
}
}
logger.info { "Plugin loaded" } logger.info { "Plugin loaded" }
} }
@@ -132,6 +144,24 @@ object JChatGPT : KotlinPlugin(
&& event.message[QuoteReply]?.source?.fromId != event.bot.id) && event.message[QuoteReply]?.source?.fromId != event.bot.id)
return 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) startChat(event)
} }
@@ -212,12 +242,38 @@ object JChatGPT : KotlinPlugin(
val historyText = StringBuilder() val historyText = StringBuilder()
var lastId = 0L var lastId = 0L
if (event is GroupMessageEvent) { 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) { for (record in history) {
// 同一人发言不要反复出现这人的名字,减少上下文 // 同一人发言不要反复出现这人的名字,减少上下文
appendGroupMessageRecord(historyText, record, event, lastId != record.fromId) appendGroupMessageRecord(historyText, record, event, lastId != record.fromId)
lastId = record.fromId lastId = record.fromId
} }
} else { } 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) { for (record in history) {
// 同一人发言不要反复出现这人的名字,减少上下文 // 同一人发言不要反复出现这人的名字,减少上下文
appendMessageRecord(historyText, record, event, lastId != record.fromId) appendMessageRecord(historyText, record, event, lastId != record.fromId)
@@ -241,6 +297,9 @@ object JChatGPT : KotlinPlugin(
showSender: Boolean, showSender: Boolean,
) { ) {
if (showSender) { if (showSender) {
// 名字前空行
historyText.appendLine()
// 名称显示
if (event.bot.id == record.fromId) { if (event.bot.id == record.fromId) {
historyText.append("**你** " + getNameCard(event.subject.botAsMember)) historyText.append("**你** " + getNameCard(event.subject.botAsMember))
} else { } else {
@@ -476,14 +535,15 @@ object JChatGPT : KotlinPlugin(
if (!done) { if (!done) {
history.add(ChatMessage.User( history.add(ChatMessage.User(
buildString { buildString {
appendLine("系统提示:本次运行最多还剩${retry-1}轮。") appendLine("## 系统提示")
append("本次运行最多还剩").append(retry-1).appendLine("轮。")
appendLine("如果要多次发言,可以一次性调用多次发言工具。") appendLine("如果要多次发言,可以一次性调用多次发言工具。")
appendLine("如果没有什么要做的,可以提前结束。") appendLine("如果没有什么要做的,可以提前结束。")
appendLine("当前时间:" + dateTimeFormatter.format(OffsetDateTime.now())) appendLine("当前时间:" + dateTimeFormatter.format(OffsetDateTime.now()))
val newMessages = getAfterHistory(startedAt, event) val newMessages = getAfterHistory(startedAt, event)
if (newMessages.isNotEmpty()) { if (newMessages.isNotEmpty()) {
append("以下是上次运行至今的新消息\n\n$newMessages") append("## 以下是上次运行至今的新消息\n\n$newMessages")
} }
} }
)) ))
@@ -494,7 +554,7 @@ object JChatGPT : KotlinPlugin(
} else { } else {
done = false done = false
logger.warning("调用llm时发生异常重试中", e) logger.warning("调用llm时发生异常重试中", e)
event.subject.sendMessage("出错了...正在重试...") // event.subject.sendMessage("出错了...正在重试...")
} }
} }
} while (!done && 0 < --retry) } while (!done && 0 < --retry)
@@ -611,6 +671,9 @@ object JChatGPT : KotlinPlugin(
// 天气服务 // 天气服务
WeatherService(), WeatherService(),
// 好感度调整
AdjustUserFavorabilityAgent(),
// Epic 免费游戏 // Epic 免费游戏
// EpicFreeGame(), // EpicFreeGame(),
@@ -678,7 +741,7 @@ object JChatGPT : KotlinPlugin(
logger.warning("获取群头衔失败", e) 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() return nameCard.toString()
} }
@@ -719,4 +782,41 @@ object JChatGPT : KotlinPlugin(
return result 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("好感度时间偏移处理完成")
}
} }

View File

@@ -2,6 +2,7 @@ package top.jie65535.mirai
import net.mamoe.mirai.console.command.CommandSender import net.mamoe.mirai.console.command.CommandSender
import net.mamoe.mirai.console.command.CompositeCommand 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.PermissionService.Companion.permit
import net.mamoe.mirai.console.permission.PermitteeId.Companion.permitteeId import net.mamoe.mirai.console.permission.PermitteeId.Companion.permitteeId
import net.mamoe.mirai.contact.Contact 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.Member
import net.mamoe.mirai.contact.User import net.mamoe.mirai.contact.User
import top.jie65535.mirai.JChatGPT.reload import top.jie65535.mirai.JChatGPT.reload
import top.jie65535.mirai.JChatGPT.save
object PluginCommands : CompositeCommand( object PluginCommands : CompositeCommand(
JChatGPT, "jgpt", description = "J OpenAI ChatGPT" JChatGPT, "jgpt", description = "J OpenAI ChatGPT"
) { ) {
@SubCommand @SubCommand
suspend fun CommandSender.setToken(token: String) { suspend fun CommandSender.reload() {
PluginConfig.openAiToken = token PluginConfig.reload()
PluginConfig.save() PluginData.reload()
LargeLanguageModels.reload() LargeLanguageModels.reload()
sendMessage("OK") sendMessage("OK")
} }
@@ -35,24 +36,34 @@ object PluginCommands : CompositeCommand(
@SubCommand @SubCommand
suspend fun CommandSender.disable(contact: Contact) { suspend fun CommandSender.disable(contact: Contact) {
when (contact) { when (contact) {
is Member -> contact.permitteeId.permit(JChatGPT.chatPermission) is Member -> contact.permitteeId.cancel(JChatGPT.chatPermission, false)
is User -> contact.permitteeId.permit(JChatGPT.chatPermission) is User -> contact.permitteeId.cancel(JChatGPT.chatPermission, false)
is Group -> contact.permitteeId.permit(JChatGPT.chatPermission) is Group -> contact.permitteeId.cancel(JChatGPT.chatPermission, false)
} }
sendMessage("OK") sendMessage("OK")
} }
@SubCommand
suspend fun CommandSender.reload() {
PluginConfig.reload()
PluginData.reload()
LargeLanguageModels.reload()
sendMessage("OK")
}
@SubCommand @SubCommand
suspend fun CommandSender.clearMemory() { suspend fun CommandSender.clearMemory() {
PluginData.contactMemory.clear() PluginData.contactMemory.clear()
sendMessage("OK") 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")
}
} }

View File

@@ -98,4 +98,10 @@ object PluginConfig : AutoSavePluginConfig("Config") {
@ValueDescription("是否启用记忆编辑功能记忆存在data目录提示词中需要加上{memory}来填充记忆,每个群都有独立记忆") @ValueDescription("是否启用记忆编辑功能记忆存在data目录提示词中需要加上{memory}来填充记忆,每个群都有独立记忆")
val memoryEnabled by value(true) val memoryEnabled by value(true)
@ValueDescription("是否启用好感度系统")
val enableFavorabilitySystem by value(true)
@ValueDescription("好感度每日基础偏移速度(点/天)")
val favorabilityBaseShiftSpeed by value(2.0)
} }

View File

@@ -1,14 +1,52 @@
package top.jie65535.mirai package top.jie65535.mirai
import kotlinx.serialization.Serializable
import net.mamoe.mirai.console.data.AutoSavePluginData import net.mamoe.mirai.console.data.AutoSavePluginData
import net.mamoe.mirai.console.data.value 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<String> = 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") { object PluginData : AutoSavePluginData("data") {
/** /**
* 联系人记忆 * 联系人记忆
*/ */
val contactMemory by value(mutableMapOf<Long, String>()) val contactMemory by value(mutableMapOf<Long, String>())
/**
* 用户好感度数据
* Key: 用户QQ号
* Value: 好感度信息
*/
val userFavorability by value(mutableMapOf<Long, FavorabilityInfo>())
/** /**
* 添加对话记忆 * 添加对话记忆
*/ */

View File

@@ -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"
}
}

View File

@@ -10,7 +10,7 @@ import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
class SendLaTeXExpression : BaseAgent( class SendLaTeXExpression : BaseAgent(
tool = Tool.function( tool = Tool.function(
name = "sendLaTeXExpression", name = "sendLaTeXExpression",
description = "发送LaTeX数学表达式将其渲染为图片并发送", description = "发送LaTeX数学表达式将其渲染为图片并发送。(暂不支持中文)",
parameters = Parameters.buildJsonObject { parameters = Parameters.buildJsonObject {
put("type", "object") put("type", "object")
putJsonObject("properties") { putJsonObject("properties") {