diff --git a/apps/bym.js b/apps/bym.js index 2c988e2..55c4fdd 100644 --- a/apps/bym.js +++ b/apps/bym.js @@ -2,6 +2,7 @@ import ChatGPTConfig from '../config/config.js' import { Chaite } from 'chaite' import { intoUserMessage, toYunzai } from '../utils/message.js' import common from '../../../lib/common/common.js' +import { getGroupContextPrompt } from '../utils/group.js' export class bym extends plugin { constructor () { @@ -9,11 +10,12 @@ export class bym extends plugin { name: 'ChatGPT-Plugin伪人模式', dsc: 'ChatGPT-Plugin伪人模式', event: 'message', - priority: -150, + priority: 6000, rule: [ { - reg: '^#chatgpt伪人模式$', - fnc: 'bym' + reg: '^[^#][sS]*', + fnc: 'bym', + log: false } ] }) @@ -23,11 +25,19 @@ export class bym extends plugin { if (!ChatGPTConfig.bym.enable) { return false } + let prob = ChatGPTConfig.bym.probability + if (ChatGPTConfig.bym.hit.find(keyword => e.msg?.includes(keyword))) { + prob = 1 + } + if (Math.random() > prob) { + return false + } + logger.info('伪人模式触发') let recall = false let presetId = ChatGPTConfig.bym.defaultPreset if (ChatGPTConfig.bym.presetMap && ChatGPTConfig.bym.presetMap.length > 0) { const option = ChatGPTConfig.bym.presetMap.sort((a, b) => a.priority - b.priority) - .find(item => item.keywords.find(keyword => e.msg.includes(keyword))) + .find(item => item.keywords.find(keyword => e.msg?.includes(keyword))) if (option) { presetId = option.presetId } @@ -49,6 +59,12 @@ export class bym extends plugin { } preset.sendMessageOption.systemOverride = ChatGPTConfig.bym.presetPrefix + preset.sendMessageOption.systemOverride } + if (ChatGPTConfig.bym.temperature >= 0) { + preset.sendMessageOption.temperature = ChatGPTConfig.bym.temperature + } + if (ChatGPTConfig.bym.maxTokens > 0) { + preset.sendMessageOption.maxTokens = ChatGPTConfig.bym.maxTokens + } const userMessage = await intoUserMessage(e, { handleReplyText: true, handleReplyImage: true, @@ -71,6 +87,10 @@ export class bym extends plugin { this.reply(forwardElement) } } + if (ChatGPTConfig.llm.enableGroupContext && e.isGroup) { + const contextPrompt = await getGroupContextPrompt(e, ChatGPTConfig.llm.groupContextLength) + preset.sendMessageOption.systemOverride = preset.sendMessageOption.systemOverride ? preset.sendMessageOption.systemOverride + '\n' + contextPrompt : contextPrompt + } // 发送 const response = await Chaite.getInstance().sendMessage(userMessage, e, { ...preset.sendMessageOption, diff --git a/apps/chat.js b/apps/chat.js index 9bfdc1e..9ace857 100644 --- a/apps/chat.js +++ b/apps/chat.js @@ -1,6 +1,8 @@ import Config from '../config/config.js' import { Chaite, SendMessageOption } from 'chaite' import { getPreset, intoUserMessage, toYunzai } from '../utils/message.js' +import { YunzaiUserState } from '../models/chaite/user_state_storage.js' +import { getGroupContextPrompt, getGroupHistory } from '../utils/group.js' export class Chat extends plugin { constructor () { @@ -8,19 +10,34 @@ export class Chat extends plugin { name: 'ChatGPT-Plugin对话', dsc: 'ChatGPT-Plugin对话', event: 'message', - priority: 0, + priority: 500, rule: [ { reg: '^[^#][sS]*', fnc: 'chat', log: false + }, + { + reg: '#hi', + fnc: 'history' } ] }) } async chat (e) { - const state = await Chaite.getInstance().getUserStateStorage().getItem(e.sender.user_id + '') + let state = await Chaite.getInstance().getUserStateStorage().getItem(e.sender.user_id + '') + if (!state) { + state = new YunzaiUserState(e.sender.user_id, e.sender.nickname, e.sender.card) + await Chaite.getInstance().getUserStateStorage().setItem(e.sender.user_id + '', state) + } + const preset = await getPreset(e, state?.settings.preset || Config.llm.defaultChatPresetId, Config.basic.toggleMode, Config.basic.togglePrefix) + if (!preset) { + logger.debug('不满足对话触发条件或未找到预设,不进入对话') + return false + } else { + logger.info('进入对话, prompt: ' + e.msg) + } const sendMessageOptions = SendMessageOption.create(state?.settings) sendMessageOptions.onMessageWithToolCall = async content => { const { msgs, forward } = await toYunzai(e, [content]) @@ -31,11 +48,6 @@ export class Chat extends plugin { this.reply(forwardElement) } } - const preset = await getPreset(e, state?.settings.preset || Config.llm.defaultChatPresetId, Config.basic.toggleMode, Config.basic.togglePrefix) - if (!preset) { - logger.debug('不满足对话触发条件或未找到预设,不进入对话') - return false - } const userMessage = await intoUserMessage(e, { handleReplyText: false, handleReplyImage: true, @@ -45,10 +57,17 @@ export class Chat extends plugin { toggleMode: Config.basic.toggleMode, togglePrefix: Config.basic.togglePrefix }) + if (Config.llm.enableGroupContext && e.isGroup) { + const contextPrompt = await getGroupContextPrompt(e, Config.llm.groupContextLength) + sendMessageOptions.systemOverride = sendMessageOptions.systemOverride ? sendMessageOptions.systemOverride + '\n' + contextPrompt : (preset.sendMessageOption.systemOverride + contextPrompt) + } const response = await Chaite.getInstance().sendMessage(userMessage, e, { ...sendMessageOptions, chatPreset: preset }) + // 更新当前聊天进度 + state.current.messageId = response.id + await Chaite.getInstance().getUserStateStorage().setItem(e.sender.user_id + '', state) const { msgs, forward } = await toYunzai(e, response.contents) if (msgs.length > 0) { await e.reply(msgs, true) @@ -57,4 +76,9 @@ export class Chat extends plugin { this.reply(forwardElement) } } + + async history (e) { + const history = await getGroupHistory(e, 10) + e.reply(JSON.stringify(history)) + } } diff --git a/apps/management.js b/apps/management.js index 47c9a20..ce31c84 100644 --- a/apps/management.js +++ b/apps/management.js @@ -1,6 +1,7 @@ import ChatGPTConfig from '../config/config.js' import { createCRUDCommandRules, createSwitchCommandRules } from '../utils/command.js' import { Chaite } from 'chaite' +import * as crypto from 'node:crypto' export class ChatGPTManagement extends plugin { constructor () { @@ -17,7 +18,7 @@ export class ChatGPTManagement extends plugin { permission: 'master' }, { - reg: `^${cmdPrefix}结束(全部)?对话$`, + reg: `^(${cmdPrefix})?#?结束(全部)?对话$`, fnc: 'destroyConversation' }, { @@ -27,18 +28,22 @@ export class ChatGPTManagement extends plugin { } ] }) - this.initCommand(cmdPrefix) + if (!Chaite.getInstance()) { + const waitForChaite = async () => { + while (!Chaite.getInstance()) { + await new Promise(resolve => setTimeout(resolve, 1000)) + } + return Chaite.getInstance() + } + waitForChaite().then(() => { + this.initCommand(cmdPrefix) + }) + } else { + this.initCommand(cmdPrefix) + } } - async initCommand (cmdPrefix) { - const waitForChaite = async () => { - while (!Chaite.getInstance()) { - await new Promise(resolve => setTimeout(resolve, 1000)) - } - return Chaite.getInstance() - } - - await waitForChaite() + initCommand (cmdPrefix) { this.rule.push(...[ ...createCRUDCommandRules.bind(this)(cmdPrefix, '渠道', 'channels'), ...createCRUDCommandRules.bind(this)(cmdPrefix, '预设', 'presets'), @@ -94,7 +99,11 @@ export class ChatGPTManagement extends plugin { this.reply(`已结束${num}个用户的对话`) } else { const state = await Chaite.getInstance().getUserStateStorage().getItem(e.sender.user_id + '') - state.current.conversationId = '' + if (!state) { + this.reply('当前未开启对话') + return false + } + state.current.conversationId = crypto.randomUUID() state.current.messageId = '' await Chaite.getInstance().getUserStateStorage().setItem(e.sender.user_id + '', state) this.reply('已结束当前对话') diff --git a/config/config.js b/config/config.js index b5210e1..ba937cb 100644 --- a/config/config.js +++ b/config/config.js @@ -24,7 +24,7 @@ class ChatGPTConfig { // 是否开启调试模式 debug: false, // 一般命令的开头 - commandPrefix: '^#chatgpt' + commandPrefix: '#chatgpt' } /** @@ -80,7 +80,12 @@ class ChatGPTConfig { * promptBlockWords: string[], * responseBlockWords: string[], * blockStrategy: 'full' | 'mask', - * blockWordMask: string + * blockWordMask: string, + * enableGroupContext: boolean, + * groupContextLength: number, + * groupContextTemplatePrefix: string, + * groupContextTemplateMessage: string, + * groupContextTemplateSuffix: string * }} */ llm = { @@ -105,7 +110,21 @@ class ChatGPTConfig { // 触发屏蔽词的策略,完全屏蔽或仅屏蔽关键词 blockStrategy: 'full', // 如果blockStrategy为mask,屏蔽词的替换字符 - blockWordMask: '***' + blockWordMask: '***', + // 是否开启群组上下文 + enableGroupContext: false, + // 群组上下文长度 + groupContextLength: 20, + // 用于组装群聊上下文提示词的模板前缀 + groupContextTemplatePrefix: 'Latest several messages in the group chat:\n' + + '| sender.card | sender.nickname | sender.user_id | sender.role | sender.title | time | messageId | raw_message |\n' + + '|---|---|---|---|---|---|---|---|', + // 用于组装群聊上下文提示词的模板内容部分,每一条消息作为message,仿照示例填写 + // eslint-disable-next-line no-template-curly-in-string + groupContextTemplateMessage: '| ${message.sender.card} | ${message.sender.nickname} | ${message.sender.user_id} | ${message.sender.role} | ${message.sender.title} | ${message.time} | ${message.messageId} | ${message.raw_message} |', + // 用于组装群聊上下文提示词的模板后缀 + groupContextTemplateSuffix: '\n' + } /** diff --git a/models/chaite/history_manager.js b/models/chaite/history_manager.js index 1c42047..0f8c93e 100644 --- a/models/chaite/history_manager.js +++ b/models/chaite/history_manager.js @@ -37,13 +37,13 @@ export class LowDBHistoryManager extends AbstractHistoryManager { const message = await this.collection.findOne({ id: currentId }) if (!message) break messages.unshift(message) - currentId = message.parentMessageId + currentId = message.parentId } return messages } else if (conversationId) { return this.collection.find({ conversationId }) } - return this.collection.findAll() + return [] } async deleteConversation (conversationId) { diff --git a/models/chaite/user_state_storage.js b/models/chaite/user_state_storage.js index d2af4f4..53eb1e1 100644 --- a/models/chaite/user_state_storage.js +++ b/models/chaite/user_state_storage.js @@ -1,4 +1,22 @@ import { ChaiteStorage } from 'chaite' +import * as crypto from 'node:crypto' + +/** + * 继承UserState + */ +export class YunzaiUserState { + constructor (userId, nickname, card, conversationId = crypto.randomUUID()) { + this.userId = userId + this.nickname = nickname + this.card = card + this.conversations = [] + this.settings = {} + this.current = { + conversationId, + messageId: '' + } + } +} /** * @extends {ChaiteStorage} @@ -35,9 +53,12 @@ export class LowDBUserStateStorage extends ChaiteStorage { */ async setItem (id, state) { if (id) { - await this.collection.updateById(id, state) - return id + if (await this.getItem(id)) { + await this.collection.updateById(id, state) + return id + } } + state.id = id const result = await this.collection.insert(state) return result.id } diff --git a/package.json b/package.json index 732bc77..ea8c900 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "type": "module", "author": "ikechan8370", "dependencies": { - "chaite": "^1.2.1", + "chaite": "^1.2.3", "js-yaml": "^4.1.0", "keyv": "^5.3.1", "keyv-file": "^5.1.2", diff --git a/utils/bot.js b/utils/bot.js new file mode 100644 index 0000000..d14b253 --- /dev/null +++ b/utils/bot.js @@ -0,0 +1,10 @@ +/** + * 获取机器人框架 + * @returns {'trss'|'miao'} + */ +export function getBotFramework () { + if (Bot.bots) { + return 'trss' + } + return 'miao' +} diff --git a/utils/command.js b/utils/command.js index 50a61e1..3002f51 100644 --- a/utils/command.js +++ b/utils/command.js @@ -1,6 +1,7 @@ import { Chaite } from 'chaite' import common from '../../../lib/common/common.js' import ChatGPTConfig from '../config/config.js' +import { getBotFramework } from './bot.js' /** * 模板 * @param cmdPrefix @@ -33,7 +34,7 @@ export function createCRUDCommandRules (cmdPrefix, name, variable, detail = true const manager = getManagerByName(upperVariable) if (detail) { rules.push({ - reg: cmdPrefix + `${name}详情$`, + reg: cmdPrefix + `${name}详情`, fnc: `detail${upperVariable}` }) this[`detail${upperVariable}`] = async function (e) { @@ -130,6 +131,11 @@ export function createCRUDCommandRules (cmdPrefix, name, variable, detail = true } } } + if (getBotFramework() === 'trss') { + rules.forEach(rule => { + rule.reg = new RegExp(rule.reg) + }) + } return rules } @@ -161,8 +167,12 @@ const switchCommandPreset = { } export function createSwitchCommandRules (cmdPrefix, name, variable, preset = 0) { const upperVariable = variable.charAt(0).toUpperCase() + variable.slice(1) - return { + const rule = { reg: cmdPrefix + `(${switchCommandPreset[preset][0]}|${switchCommandPreset[preset][1]})${name}$`, fnc: `switch${upperVariable}` } + if (getBotFramework() === 'trss') { + rule.reg = new RegExp(rule.reg) + } + return rule } diff --git a/utils/group.js b/utils/group.js new file mode 100644 index 0000000..40aceb0 --- /dev/null +++ b/utils/group.js @@ -0,0 +1,146 @@ +import { getBotFramework } from './bot.js' +import ChatGPTConfig from '../config/config.js' + +export class GroupContextCollector { + /** + * 获取群组上下文 + * @param {*} bot bot实例 + * @param {string} groupId 群号 + * @param {number} start 起始seq + * @param {number} length 往前数几条 + * @returns {Promise>} + */ + async collect (bot = Bot, groupId, start = 0, length = 20) { + throw new Error('Method not implemented.') + } +} + +export class ICQQGroupContextCollector extends GroupContextCollector { + /** + * 获取群组上下文 + * @param {*} bot + * @param {string} groupId + * @param {number} start + * @param {number} length + * @returns {Promise>} + */ + async collect (bot = Bot, groupId, start = 0, length = 20) { + const group = bot.pickGroup(groupId) + let latestChats = await group.getChatHistory(start, 1) + if (latestChats.length > 0) { + let latestChat = latestChats[0] + if (latestChat) { + let seq = latestChat.seq || latestChat.message_id + let chats = [] + while (chats.length < length) { + let chatHistory = await group.getChatHistory(seq, 20) + if (!chatHistory || chatHistory.length === 0) { + break + } + chats.push(...chatHistory.reverse()) + if (seq === chatHistory[chatHistory.length - 1].seq || seq === chatHistory[chatHistory.length - 1].message_id) { + break + } + seq = chatHistory[chatHistory.length - 1].seq || chatHistory[chatHistory.length - 1].message_id + } + chats = chats.slice(0, length).reverse() + try { + let mm = bot.gml + for (const chat of chats) { + let sender = mm.get(chat.sender.user_id) + if (sender) { + chat.sender = sender + } + } + } catch (err) { + logger.warn(err) + } + // console.log(chats) + return chats + } + } + // } + return [] + } +} + +export class TRSSGroupContextCollector extends GroupContextCollector { + /** + * 获取群组上下文 + * @param {*} bot + * @param {string} groupId + * @param {number} start + * @param {number} length + * @returns {Promise>} + */ + async collect (bot = Bot, groupId, start = 0, length = 20) { + const group = bot.pickGroup(groupId) + let chats = await group.getChatHistory(start, length) + try { + let mm = bot.gml + for (const chat of chats) { + let sender = mm.get(chat.sender.user_id) + if (sender) { + chat.sender = sender + } + } + } catch (err) { + logger.warn(err) + } + return chats + } +} + +/** + * 获取群组上下文 + * @param e + * @param length + * @returns {Promise>} + */ +export async function getGroupHistory (e, length = 20) { + if (getBotFramework() === 'trss') { + const collector = new TRSSGroupContextCollector() + return await collector.collect(e.bot, e.group_id, 0, length) + } + return await new ICQQGroupContextCollector().collect(e.bot, e.group_id, 0, length) +} + +/** + * 获取构建群聊聊天记录的prompt + * @param e event + * @param {number} length 长度 + * @returns {Promise} + */ +export async function getGroupContextPrompt (e, length) { + const { + groupContextTemplatePrefix, + groupContextTemplateMessage, + groupContextTemplateSuffix + } = ChatGPTConfig.llm + const chats = await getGroupHistory(e, length) + const rows = chats.map(chat => { + const sender = chat.sender || {} + return groupContextTemplateMessage + // eslint-disable-next-line no-template-curly-in-string + .replace('${message.sender.card}', sender.card || '-') + // eslint-disable-next-line no-template-curly-in-string + .replace('${message.sender.nickname}', sender.nickname || '-') + // eslint-disable-next-line no-template-curly-in-string + .replace('${message.sender.user_id}', sender.user_id || '-') + // eslint-disable-next-line no-template-curly-in-string + .replace('${message.sender.role}', sender.role || '-') + // eslint-disable-next-line no-template-curly-in-string + .replace('${message.sender.title}', sender.title || '-') + // eslint-disable-next-line no-template-curly-in-string + .replace('${message.time}', chat.time || '-') + // eslint-disable-next-line no-template-curly-in-string + .replace('${message.messageId}', chat.messageId || '-') + // eslint-disable-next-line no-template-curly-in-string + .replace('${message.raw_message}', chat.raw_message || '-') + }).join('\n') + return [ + groupContextTemplatePrefix, + rows, + groupContextTemplateSuffix + ].join('\n') +} diff --git a/utils/message.js b/utils/message.js index 8e2a5e9..8aa7cf0 100644 --- a/utils/message.js +++ b/utils/message.js @@ -103,7 +103,7 @@ export async function getPreset (e, presetId, toggleMode, togglePrefix) { const isValidChat = checkChatMsg(e, toggleMode, togglePrefix) const manager = Chaite.getInstance().getChatPresetManager() const presets = await manager.getAllPresets() - const prefixHitPresets = presets.filter(p => e.msg.startsWith(p.prefix)) + const prefixHitPresets = presets.filter(p => e.msg?.startsWith(p.prefix)) if (!isValidChat && prefixHitPresets.length === 0) { return null }