Files
JChatGPT/src/main/kotlin/JChatGPT.kt

996 lines
38 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package top.jie65535.mirai
import com.aallam.openai.api.chat.ChatCompletionChunk
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
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register
import net.mamoe.mirai.console.command.CommandSender.Companion.toCommandSender
import net.mamoe.mirai.console.permission.PermissionId
import net.mamoe.mirai.console.permission.PermissionService
import net.mamoe.mirai.console.permission.PermissionService.Companion.hasPermission
import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription
import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.contact.MemberPermission.*
import net.mamoe.mirai.event.GlobalEventChannel
import net.mamoe.mirai.event.events.FriendMessageEvent
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.info
import top.jie65535.mirai.tools.*
import util.LunarDateUtil
import xyz.cssxsh.mirai.hibernate.MiraiHibernateRecorder
import xyz.cssxsh.mirai.hibernate.entry.MessageRecord
import java.io.File
import java.time.Instant
import java.time.OffsetDateTime
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
import kotlin.time.Duration.Companion.milliseconds
object JChatGPT : KotlinPlugin(
JvmPluginDescription(
id = "top.jie65535.mirai.JChatGPT",
name = "J ChatGPT",
version = "1.10.0",
) {
author("jie65535")
// dependsOn("xyz.cssxsh.mirai.plugin.mirai-hibernate-plugin", true)
}
) {
/**
* 是否包含历史对话
*/
private var includeHistory: Boolean = false
/**
* 聊天权限
*/
val chatPermission = PermissionId("JChatGPT", "Chat")
/**
* 唤醒关键字
*/
private var keyword: Regex? = null
override fun onEnable() {
// 注册聊天权限
PermissionService.INSTANCE.register(chatPermission, "JChatGPT Chat Permission")
PluginConfig.reload()
PluginData.reload()
// 设置Token
LargeLanguageModels.reload()
// 注册插件命令
PluginCommands.register()
// 检查消息记录插件是否存在
includeHistory = try {
MiraiHibernateRecorder
true
} catch (_: Throwable) {
false
}
if (PluginConfig.callKeyword.isNotEmpty()) {
keyword = Regex(PluginConfig.callKeyword)
}
GlobalEventChannel.parentScope(this)
.subscribeAlways<MessageEvent> { event -> onMessage(event) }
// 启动定时任务处理好感度时间偏移
if (PluginConfig.enableFavorabilitySystem) {
launch {
while (true) {
delay(24.hours) // 每24小时执行一次
shiftFavorabilityOverTime()
}
}
}
logger.info { "Plugin loaded" }
}
private val timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss")
.withZone(ZoneOffset.systemDefault())
private val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy年MM月dd E HH:mm:ss")
private val requestMap = ConcurrentSet<Long>()
/**
* 对话上下文缓存
*/
private val contextCache = ConcurrentMap<Long, ConversationCache>()
/**
* 清空所有对话上下文缓存(供管理员命令使用)
*/
fun clearContextCache() {
contextCache.clear()
}
/**
* 对话上下文缓存数据类
* @param history 完整的消息历史
* @param lastActivityAt 最后活动时间戳
*/
private data class ConversationCache(
val history: MutableList<ChatMessage>,
val lastActivityAt: Int
) {
fun isExpired(ttlSeconds: Int): Boolean {
return OffsetDateTime.now().toEpochSecond().toInt() - lastActivityAt > ttlSeconds
}
}
private suspend fun onMessage(event: MessageEvent) {
// 检查Token是否设置
if (LargeLanguageModels.chat == null) return
// 发送者是否有权限
if (!event.toCommandSender().hasPermission(chatPermission)) {
if (event is GroupMessageEvent) {
if (PluginConfig.groupOpHasChatPermission && event.sender.isOperator()) {
// 允许管理员使用
} else if (event.sender.active.temperature >= PluginConfig.temperaturePermission) {
// 允许活跃度达标成员使用
} else {
// 其它情况阻止使用
return
}
}
if (event is FriendMessageEvent) {
if (!PluginConfig.friendHasChatPermission) {
return
}
// TODO 检查好友上下文
}
}
// 如果没有 @bot 或者 触发关键字 或者 回复bot的消息 则直接结束
if (!event.message.contains(At(event.bot))
&& keyword?.let { event.message.content.contains(it) } != true
&& 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)
}
private var memePrompt: String? = null
private fun getSystemPrompt(event: MessageEvent): String {
val now = OffsetDateTime.now()
val prompt = StringBuilder(LargeLanguageModels.systemPrompt)
fun replace(target: String, replacement: () -> String) {
val i = prompt.indexOf(target)
if (i != -1) {
prompt.replace(i, i + target.length, replacement())
}
}
replace("{time}") {
val solarTime = dateTimeFormatter.format(now)
val lunarInfo = LunarDateUtil.getFormattedLunarAndHoliday(now)
"$solarTime\n农历$lunarInfo"
}
replace("{subject}") {
if (event is GroupMessageEvent) {
"\"${event.subject.name}\" 群聊中,你在本群的名片是:${getNameCard(event.subject.botAsMember)}"
} else {
"\"${event.senderName}\" 私聊中"
}
}
replace("{memory}") {
val memoryText = PluginData.contactMemory[event.subject.id]
if (memoryText.isNullOrEmpty()) {
"暂无相关记忆"
} else memoryText
}
replace("{meme}") {
memePrompt?.let { return@replace it }
if (PluginConfig.memeDir.isEmpty()) {
""
} else {
buildString {
val dir = File(PluginConfig.memeDir)
if (dir.exists() && dir.isDirectory) {
append("memes文件夹地址为")
append(PluginConfig.memeDir)
appendLine()
val memes = dir.list()
if (memes.isEmpty()) {
append("暂无表情包~")
} else {
for (name in memes) {
append("- ")
append(name)
appendLine()
}
appendLine()
append("表情包示例:![")
append(memes[0])
append("](")
append(File(dir, memes[0]).absoluteFile)
append(")")
appendLine()
}
} else {
append("配置的meme路径不存在")
}
}.also {
memePrompt = it
}
}
}
return prompt.toString()
}
// region - 历史消息相关 -
/**
* 获取历史消息
* @param event 消息事件
* @return 如果未获取到则返回空字符串
*/
private fun getHistory(event: MessageEvent): String {
if (!includeHistory) {
return event.message.content
}
val now = OffsetDateTime.now()
// 一段时间内的消息
val beforeTimestamp = now.minusMinutes(PluginConfig.historyWindowMin.toLong()).toEpochSecond().toInt()
return getAfterHistory(beforeTimestamp, event)
}
/**
* 获取指定时间后的历史消息
* @param time Epoch时间戳
* @param event 消息事件
* @return 如果未获取到则返回空字符串
*/
private fun getAfterHistory(time: Int, event: MessageEvent): String {
if (!includeHistory) {
return ""
}
// 现在时间
val nowTimestamp = OffsetDateTime.now().toEpochSecond().toInt()
// 最近这段时间的历史对话
val history = MiraiHibernateRecorder[event.subject, time, nowTimestamp]
.take(PluginConfig.historyMessageLimit) // 只取最近的部分消息,避免上下文过长
.sortedBy { it.time } // 按时间排序
.toMutableList()
// 有一定概率最后一条消息没加入,这里检查然后补充一下
val msgIds = event.message.ids.joinToString(",")
if (!history.any { it.ids == msgIds }) {
history.add(MessageRecord.fromSuccess(event.message.source, event.message))
}
// 构造历史消息
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)
lastId = record.fromId
}
}
return historyText.toString()
}
/**
* 添加群消息记录到历史上下文中
* @param historyText 历史消息构造器
* @param record 群消息记录
* @param event 群消息事件
*/
fun appendGroupMessageRecord(
historyText: StringBuilder,
record: MessageRecord,
event: GroupMessageEvent,
showSender: Boolean,
) {
if (showSender) {
// 名字前空行
historyText.appendLine()
// 名称显示
if (event.bot.id == record.fromId) {
historyText.append("**你** " + getNameCard(event.subject.botAsMember))
} else {
historyText.append(getNameCard(event.subject, record.fromId))
}
// 发言时间
historyText.append(' ')
.append(timeFormatter.format(Instant.ofEpochSecond(record.time.toLong())))
}
val recordMessage = record.toMessageChain()
recordMessage[QuoteReply.Key]?.let {
historyText.append(" 引用 ${getNameCard(event.subject, it.source.fromId)} 说的\n > ")
.appendLine(it.source.originalMessage.content.replace("\n", "\n > "))
}
if (showSender) {
// 消息内容
historyText.append(" 说:")
}
historyText.appendLine(record.toMessageChain().joinToString("") {
when (it) {
is At -> {
it.getDisplay(event.subject)
}
else -> singleMessageToText(it)
}
})
}
private fun getNameCard(group: Group, qq: Long): String {
val member = group[qq]
return if (member == null) {
"未知群员($qq)"
} else {
getNameCard(member)
}
}
/**
* 添加消息记录到历史上下文中
* @param historyText 历史消息构造器
* @param record 消息记录
* @param event 消息事件
*/
fun appendMessageRecord(
historyText: StringBuilder,
record: MessageRecord,
event: MessageEvent,
showSender: Boolean
) {
if (showSender) {
if (event.bot.id == record.fromId) {
historyText.append("**你** " + event.bot.nameCardOrNick)
} else {
historyText.append(event.senderName)
}
historyText
.append(" ")
// 发言时间
.append(timeFormatter.format(Instant.ofEpochSecond(record.time.toLong())))
}
val recordMessage = record.toMessageChain()
recordMessage[QuoteReply.Key]?.let {
historyText.append(" 引用\n > ")
.appendLine(
it.source.originalMessage
.joinToString("", transform = ::singleMessageToText)
.replace("\n", "\n > ")
)
}
if (showSender) {
historyText.append(" 说:")
}
// 消息内容
historyText.appendLine(
record.toMessageChain().joinToString("", transform = ::singleMessageToText)
)
}
private fun singleMessageToText(it: SingleMessage): String {
return when (it) {
is ForwardMessage -> {
it.title + "\n " + it.preview
}
// 图片格式化
is Image -> {
try {
val imageUrl = runBlocking {
it.queryUrl()
}
"![${if (it.isEmoji) "表情包" else "图片"}]($imageUrl)"
} catch (e: Throwable) {
logger.warning("图片地址获取失败", e)
it.content
}
}
else -> it.content
}
}
// endregion - 历史消息相关 -
private val thinkRegex = Regex("<think>[\\s\\S]*?</think>")
/**
* 截断过长的工具输出,并添加省略标记
*/
private fun truncateToolOutput(content: String, maxLength: Int = PluginConfig.maxToolOutputLength): String {
if (content.length <= maxLength) return content
val truncated = content.take(maxLength)
val marker = "\n\n[系统提示:因内容过长,部分内容已被省略]"
return truncated + marker
}
private suspend fun startChat(event: MessageEvent) {
if (!requestMap.add(event.subject.id)) {
logger.warning("The current Contact is busy!")
return
}
try {
// 尝试从缓存加载上下文
val subjectId = event.subject.id
val cache = contextCache[subjectId]
val history = if (PluginConfig.enableContextCache
&& cache != null
&& !cache.isExpired(PluginConfig.contextCacheTimeoutMinutes * 60)
) {
// 缓存有效,复用历史
logger.info("使用缓存的对话上下文,包含 ${cache.history.size} 条互动消息")
cache.history
} else {
// 缓存无效或不存在,创建新上下文
mutableListOf()
}
// 如果历史为空,添加系统提示词和聊天记录
if (history.isEmpty() || cache == null) {
val prompt = getSystemPrompt(event)
if (PluginConfig.logPrompt) {
logger.info("Prompt: $prompt")
}
history.add(ChatMessage(ChatRole.System, prompt))
val historyText = getHistory(event)
logger.info("注入聊天记录:\n$historyText")
history.add(ChatMessage.User(historyText))
} else {
val newMessages = getAfterHistory(cache.lastActivityAt, event)
logger.info("补充聊天记录:\n$newMessages")
history.add(
ChatMessage.User(
"## 以下是上次对话结束至今的新消息\n\n$newMessages"
)
)
}
var done: Boolean
// 至少循环3次
var retry = max(PluginConfig.retryMax, 3)
do {
try {
val startedAt = OffsetDateTime.now().toEpochSecond().toInt()
val responseFlow = chatCompletions(history)
var responseMessageBuilder: StringBuilder? = null
var reasoningContentBuilder: 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
// 处理推理内容更新
if (delta.reasoningContent != null) {
if (reasoningContentBuilder == null) {
reasoningContentBuilder = StringBuilder(delta.reasoningContent)
} else {
reasoningContentBuilder.append(delta.reasoningContent)
}
}
// 处理内容更新
if (delta.content != null) {
if (responseMessageBuilder == null) {
responseMessageBuilder = StringBuilder(delta.content)
} else {
responseMessageBuilder.append(delta.content)
}
}
// 处理工具调用更新
val toolCalls = delta.toolCalls
if (toolCalls != null) {
for (toolCallChunk in toolCalls) {
val index = toolCallChunk.index
val toolId = toolCallChunk.id
val function = toolCallChunk.function
// 新的请求
if (index >= responseToolCalls.size) {
// 处理已完成的工具调用
responseToolCalls.lastOrNull()?.let { toolCall ->
toolCallTasks.add(async {
val functionResponse = toolCall.execute(event)
ChatMessage(
role = ChatRole.Tool,
toolCallId = toolCall.id,
name = toolCall.function.name,
content = functionResponse
)
})
}
// 加入新的工具调用
if (toolId != null && function != null) {
responseToolCalls.add(ToolCall.Function(toolId, function))
}
} else if (function != null) {
// 拼接函数名字
if (function.nameOrNull != null) {
val currentTool = responseToolCalls[index]
responseToolCalls[index] = currentTool.copy(
function = currentTool.function.copy(
nameOrNull = currentTool.function.nameOrNull.orEmpty() + function.name
)
)
}
// 拼接函数参数
if (function.argumentsOrNull != null) {
val currentTool = responseToolCalls[index]
responseToolCalls[index] = currentTool.copy(
function = currentTool.function.copy(
argumentsOrNull = currentTool.function.argumentsOrNull.orEmpty() + function.arguments
)
)
}
}
}
}
// 捕获token使用量
chunk.usage?.let { lastTokenUsage = it }
}
// 移除思考内容
val responseContent = responseMessageBuilder?.replace(thinkRegex, "")?.trim()
logger.info("LLM Response: $responseContent")
// 记录AI回答
// reasoning_content仅在工具调用时需要回传DeepSeek规范否则丢弃
// toolCalls空列表转null避免序列化为"tool_calls":[]导致DeepSeek V4报400
// explicitNulls=false确保null字段不会序列化到JSON中兼容所有API
history.add(
ChatMessage(
role = ChatRole.Assistant,
content = responseContent,
toolCalls = responseToolCalls.ifEmpty { null },
reasoningContent = if (responseToolCalls.isNotEmpty()) reasoningContentBuilder?.toString() else null
)
)
// 记录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 ->
val functionResponse = toolCall.execute(event)
ChatMessage(
role = ChatRole.Tool,
toolCallId = toolCall.id,
name = toolCall.function.name,
content = functionResponse
)
}
if (toolCallTasks.isNotEmpty()) {
// 等待之前的所有工具完成
history.addAll(toolCallTasks.awaitAll())
}
// 将最后一个也加入对话历史中
history.add(toolCallMessage)
// 如果调用中包含结束对话工具则表示完成,反之则继续循环
done = responseToolCalls.any { it.function.name == "endConversation" }
} else {
done = true
}
if (!done) {
history.add(
ChatMessage.User(
buildString {
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")
}
}
))
} else {
// 保存对话上下文到缓存
if (PluginConfig.enableContextCache) {
contextCache[subjectId] = ConversationCache(
history = history,
lastActivityAt = startedAt
)
logger.debug("已保存对话上下文到缓存")
}
}
} catch (e: Exception) {
if (retry <= 1) {
throw e
} else {
done = false
logger.warning("调用llm时发生异常重试中", e)
// event.subject.sendMessage("出错了...正在重试...")
}
}
} while (!done && 0 < --retry)
} catch (ex: Throwable) {
logger.warning(ex)
event.subject.sendMessage("很抱歉,发生异常,请稍后重试")
} finally {
// 一段时间后才允许再次提问,防止高频对话
launch {
delay(500.milliseconds)
requestMap.remove(event.subject.id)
}
}
}
private val regexAtQq = Regex("""@(\d{5,12})""")
private val regexImage = Regex("""!\[(.*?)]\(([^\s"']+).*?\)""")
private data class MessageChunk(val range: IntRange, val content: Message)
/**
* 将聊天内容转为聊天消息
*
* @param contact 联系对象
* @param content 文本内容
* @return 构造的消息
*/
fun toMessage(contact: Contact, content: String): Message {
return if (content.isEmpty()) {
PlainText("...")
} else if (content.length < 3) {
PlainText(content)
} else {
val t = mutableListOf<MessageChunk>()
// @某人
regexAtQq.findAll(content).forEach {
val qq = it.groups[1]?.value?.toLongOrNull()
if (qq != null && contact is Group) {
contact[qq]?.let { member -> t.add(MessageChunk(it.range, At(member))) }
}
}
// 图片
regexImage.findAll(content).forEach {
// val placeholder = it.groupValues[1]
val url = it.groupValues[2]
t.add(
MessageChunk(
it.range,
Image(url)
)
)
}
// 构造消息链
buildMessageChain {
var index = 0
for ((range, msg) in t.sortedBy { it.range.first }) {
if (index < range.first) {
append(content, index, range.first)
}
append(msg)
index = range.last + 1
}
// 拼接后续消息
if (index < content.length) {
append(content, index, content.length)
}
}
}
}
/**
* 工具列表
*/
private val myTools = listOf(
// 发送单条消息
SendSingleMessageAgent(),
// 发送组合消息
SendCompositeMessage(),
// 发送语音消息
SendVoiceMessage(),
// 发送LaTeX表达式
SendLaTeXExpression(),
// 结束循环
StopLoopAgent(),
// 记忆代理
MemoryAppend(),
// 记忆修改
MemoryReplace(),
// 网页搜索
WebSearch(),
// 访问网页
VisitWeb(),
// 运行代码
RunCode(),
// 推理代理
ReasoningAgent(),
// 视觉代理
VisualAgent(),
// 图像编辑模型
ImageEdit(),
// 天气服务
WeatherService(),
// 好感度调整
AdjustUserFavorabilityAgent(),
// 请求主人帮助
RequestOwner(),
// Epic 免费游戏
// EpicFreeGame(),
// 群管代理
GroupManageAgent(),
)
// private suspend fun chatCompletion(
// chatMessages: List<ChatMessage>,
// hasTools: Boolean = true
// ): ChatMessage {
// val llm = LargeLanguageModels.chat ?: throw NullPointerException("OpenAI Token 未设置,无法开始")
// val availableTools = if (hasTools) {
// myTools.filter { it.isEnabled }.map { it.tool }
// } else null
// val request = ChatCompletionRequest(
// model = ModelId(PluginConfig.chatModel),
// temperature = PluginConfig.chatTemperature,
// messages = chatMessages,
// tools = availableTools,
// )
// logger.info("API Requesting... Model=${PluginConfig.chatModel}")
// val response = llm.chatCompletion(request)
// val message = response.choices.first().message
// logger.info("Response: $message ${response.usage}")
// return message
// }
private fun chatCompletions(
chatMessages: List<ChatMessage>,
hasTools: Boolean = true
): Flow<ChatCompletionChunk> {
val llm = LargeLanguageModels.chat ?: throw NullPointerException("OpenAI Token 未设置,无法开始")
val availableTools = if (hasTools) {
myTools.filter { it.isEnabled }.map { it.tool }
} else null
val request = ChatCompletionRequest(
model = ModelId(PluginConfig.chatModel),
temperature = PluginConfig.chatTemperature,
messages = chatMessages,
tools = availableTools,
)
logger.info("API Requesting... Model=${PluginConfig.chatModel}")
return llm.chatCompletions(request)
}
private fun getNameCard(member: Member): String {
val nameCard = StringBuilder()
// 群活跃等级
nameCard.append("【lv").append(member.active.temperature).append(" ")
try {
// 群头衔
if (member.specialTitle.isNotEmpty()) {
nameCard.append(member.specialTitle)
} else {
nameCard.append(
when (member.permission) {
OWNER -> "群主"
ADMINISTRATOR -> "管理员"
MEMBER -> member.temperatureTitle
}
)
}
} catch (e: Throwable) {
logger.warning("获取群头衔失败", e)
}
// 群名片
nameCard.append("\t\"").append(member.nameCardOrNick).append("\"\t(qq=").append(member.id).append(")")
return nameCard.toString()
}
private suspend fun ToolCall.Function.execute(event: MessageEvent): String {
val agent = myTools.find { it.tool.function.name == function.name }
?: return "Function ${function.name} not found"
// 提示正在执行函数
val receipt = if (PluginConfig.showToolCallingMessage && agent.loadingMessage.isNotEmpty()) {
event.subject.sendMessage(agent.loadingMessage)
} else null
// 执行函数
val result = try {
// 提取参数
val args = function.argumentsAsJsonOrNull()
logger.info("Calling ${function.name}(${args})")
agent.execute(args, event)
} catch (e: Throwable) {
logger.error("Failed to call ${function.name}", e)
"工具调用失败,请尝试自行回答用户,或如实告知。\n异常信息:${e.message}"
}
logger.info("Result=\"$result\"")
// 截断过长的工具输出
val truncatedResult = truncateToolOutput(result)
if (truncatedResult.length != result.length) {
logger.warning("工具 ${function.name} 返回内容过长,已从 ${result.length} 字符截断至 ${truncatedResult.length} 字符")
}
// 过会撤回加载消息
if (receipt != null) {
launch {
delay(3.seconds)
try {
receipt.recall()
} catch (e: Throwable) {
logger.error(
"消息撤回失败,调试信息:" +
"source.internalIds=${receipt.source.internalIds.joinToString()} " +
"source.ids= ${receipt.source.ids.joinToString()}", e
)
}
}
}
return truncatedResult
}
/**
* 好感度时间偏移处理函数
* 使好感度逐渐向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("好感度时间偏移处理完成")
}
}