From 9fcc25a7262eec542a021d41797d82ee3bd8ef2d Mon Sep 17 00:00:00 2001 From: ikechan8370 Date: Mon, 17 Mar 2025 16:19:12 +0800 Subject: [PATCH] feat: bym --- apps/bym.js | 93 ++++++++++++++++++++++++++++++++++++++++++++++ apps/chat.js | 25 +++++++++---- apps/management.js | 20 +++++++++- config/config.js | 80 +++++++++++++++++++++------------------ utils/message.js | 47 +++++++++++++++++++++++ 5 files changed, 219 insertions(+), 46 deletions(-) create mode 100644 apps/bym.js diff --git a/apps/bym.js b/apps/bym.js new file mode 100644 index 0000000..2c988e2 --- /dev/null +++ b/apps/bym.js @@ -0,0 +1,93 @@ +import ChatGPTConfig from '../config/config.js' +import { Chaite } from 'chaite' +import { intoUserMessage, toYunzai } from '../utils/message.js' +import common from '../../../lib/common/common.js' + +export class bym extends plugin { + constructor () { + super({ + name: 'ChatGPT-Plugin伪人模式', + dsc: 'ChatGPT-Plugin伪人模式', + event: 'message', + priority: -150, + rule: [ + { + reg: '^#chatgpt伪人模式$', + fnc: 'bym' + } + ] + }) + } + + async bym (e) { + if (!ChatGPTConfig.bym.enable) { + return false + } + 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))) + if (option) { + presetId = option.presetId + } + recall = !!option.recall + } + + const presetManager = Chaite.getInstance().getChatPresetManager() + let preset = await presetManager.getInstance(presetId) + if (!preset) { + preset = await presetManager.getInstance(ChatGPTConfig.bym.defaultPreset) + } + if (!preset) { + logger.debug('未找到预设,请检查配置文件') + return false + } + if (ChatGPTConfig.bym.presetPrefix) { + if (!preset.sendMessageOption.systemOverride) { + preset.sendMessageOption.systemOverride = '' + } + preset.sendMessageOption.systemOverride = ChatGPTConfig.bym.presetPrefix + preset.sendMessageOption.systemOverride + } + const userMessage = await intoUserMessage(e, { + handleReplyText: true, + handleReplyImage: true, + useRawMessage: true, + handleAtMsg: true, + excludeAtBot: false, + toggleMode: ChatGPTConfig.basic.toggleMode, + togglePrefix: ChatGPTConfig.basic.togglePrefix + }) + // 伪人不记录历史 + preset.sendMessageOption.disableHistoryRead = true + preset.sendMessageOption.disableHistorySave = true + // 设置多轮调用回掉 + preset.sendMessageOption.onMessageWithToolCall = async content => { + const { msgs, forward } = await toYunzai(e, [content]) + if (msgs.length > 0) { + await e.reply(msgs) + } + for (let forwardElement of forward) { + this.reply(forwardElement) + } + } + // 发送 + const response = await Chaite.getInstance().sendMessage(userMessage, e, { + ...preset.sendMessageOption, + chatPreset: preset + }) + const { msgs, forward } = await toYunzai(e, response.contents) + if (msgs.length > 0) { + // await e.reply(msgs, false, { recallMsg: recall }) + for (let msg of msgs) { + await e.reply(msg, false, { recallMsg: recall ? 10 : 0 }) + await common.sleep(Math.floor(Math.random() * 2000) + 1000) + } + } + if (ChatGPTConfig.bym.sendReasoning) { + for (let forwardElement of forward) { + await e.reply(forwardElement, false, { recallMsg: recall ? 10 : 0 }) + } + } + } +} diff --git a/apps/chat.js b/apps/chat.js index a4fc0bd..9bfdc1e 100644 --- a/apps/chat.js +++ b/apps/chat.js @@ -1,6 +1,6 @@ import Config from '../config/config.js' import { Chaite, SendMessageOption } from 'chaite' -import { getPreset, intoUserMessage } from '../utils/message.js' +import { getPreset, intoUserMessage, toYunzai } from '../utils/message.js' export class Chat extends plugin { constructor () { @@ -22,9 +22,18 @@ export class Chat extends plugin { async chat (e) { const state = await Chaite.getInstance().getUserStateStorage().getItem(e.sender.user_id + '') const sendMessageOptions = SendMessageOption.create(state?.settings) + sendMessageOptions.onMessageWithToolCall = async content => { + const { msgs, forward } = await toYunzai(e, [content]) + if (msgs.length > 0) { + await e.reply(msgs) + } + for (let forwardElement of forward) { + this.reply(forwardElement) + } + } const preset = await getPreset(e, state?.settings.preset || Config.llm.defaultChatPresetId, Config.basic.toggleMode, Config.basic.togglePrefix) if (!preset) { - logger.debug('未找到预设,不进入对话') + logger.debug('不满足对话触发条件或未找到预设,不进入对话') return false } const userMessage = await intoUserMessage(e, { @@ -40,10 +49,12 @@ export class Chat extends plugin { ...sendMessageOptions, chatPreset: preset }) - const responseText = response.contents - .filter(c => c.type === 'text') - .map(c => (/** @type {import('chaite').TextContent} **/ c).text) - .reduce((a, b) => a + b, '') - await this.reply(responseText) + const { msgs, forward } = await toYunzai(e, response.contents) + if (msgs.length > 0) { + await e.reply(msgs, true) + } + for (let forwardElement of forward) { + this.reply(forwardElement) + } } } diff --git a/apps/management.js b/apps/management.js index 9e17fe0..47c9a20 100644 --- a/apps/management.js +++ b/apps/management.js @@ -1,7 +1,6 @@ import ChatGPTConfig from '../config/config.js' import { createCRUDCommandRules, createSwitchCommandRules } from '../utils/command.js' import { Chaite } from 'chaite' -import { resolve } from 'eslint-plugin-promise/rules/lib/promise-statics.js' export class ChatGPTManagement extends plugin { constructor () { @@ -20,6 +19,11 @@ export class ChatGPTManagement extends plugin { { reg: `^${cmdPrefix}结束(全部)?对话$`, fnc: 'destroyConversation' + }, + { + reg: `^${cmdPrefix}(bym|伪人)设置默认预设`, + fnc: 'setDefaultBymPreset', + permission: 'master' } ] }) @@ -49,7 +53,8 @@ export class ChatGPTManagement extends plugin { ...createCRUDCommandRules.bind(this)(cmdPrefix, '黑名单群', 'blackGroups', false), ...createCRUDCommandRules.bind(this)(cmdPrefix, '白名单群', 'whiteGroups', false), ...createCRUDCommandRules.bind(this)(cmdPrefix, '黑名单用户', 'blackUsers', false), - ...createCRUDCommandRules.bind(this)(cmdPrefix, '白名单用户', 'whiteUsers', false) + ...createCRUDCommandRules.bind(this)(cmdPrefix, '白名单用户', 'whiteUsers', false), + createSwitchCommandRules(cmdPrefix, '(伪人|bym)', 'bym') ]) } @@ -60,6 +65,17 @@ export class ChatGPTManagement extends plugin { this.reply(`token: ${token}, 有效期300秒`, true) } + async setDefaultBymPreset (e) { + const presetId = e.msg.replace(`${ChatGPTConfig.basic.commandPrefix}伪人设置默认预设`, '') + const preset = await Chaite.getInstance().getChatPresetManager().getInstance(presetId) + if (preset) { + ChatGPTConfig.bym.defaultPreset = presetId + this.reply(`伪人模式默认预设已切换为${presetId}(${preset.name})`) + } else { + this.reply(`未找到预设${presetId}`) + } + } + async destroyConversation (e) { if (e.msg.includes('全部')) { if (!e.isMaster) { diff --git a/config/config.js b/config/config.js index ab81a48..b5210e1 100644 --- a/config/config.js +++ b/config/config.js @@ -27,6 +27,47 @@ class ChatGPTConfig { commandPrefix: '^#chatgpt' } + /** + * 伪人模式,基于框架实现,因此机器人开启前缀后依然需要带上前缀。 + * @type {{ + * enable: boolean, + * hit: string[], + * probability: number, + * defaultPreset: string, + * presetPrefix?: string, + * presetMap: Array<{ + * keywords: string[], + * presetId: string, + * priority: number, + * recall?: boolean + * }>, + * maxTokens: number, + * temperature: number, + * sendReasoning: boolean + * }} + * }} + */ + bym = { + // 开关 + enable: false, + // 伪人必定触发词 + hit: ['bym'], + // 不包含伪人必定触发词时的概率 + probability: 0.02, + // 伪人模式的默认预设 + defaultPreset: '', + // 伪人模式的预设前缀,会加在在所有其他预设前。例如此处可以用于配置通用的伪人发言风格(随意、模仿群友等),presetMap中专心配置角色设定即可 + presetPrefix: '', + // 包含关键词与预设的对应关系。包含特定触发词使用特定的预设,按照优先级排序 + presetMap: [], + // 如果大于0,会覆盖preset中的maxToken,用于控制伪人模式发言长度 + maxTokens: 0, + // 如果大于等于0,会覆盖preset中的temperature,用于控制伪人模式发言随机性 + temperature: -1, + // 是否发送思考内容 + sendReasoning: false + } + /** * 模型和对话相关配置 * @type {{ @@ -119,43 +160,6 @@ class ChatGPTConfig { constructor () { this.version = '3.0.0' - this.basic = { - toggleMode: 'at', - togglePrefix: '#chat', - debug: false, - commandPrefix: '^#chatgpt' - } - this.llm = { - defaultModel: '', - embeddingModel: 'gemini-embedding-exp-03-07', - dimensions: 0, - defaultChatPresetId: '', - enableCustomPreset: false, - customPresetUserWhiteList: [], - customPresetUserBlackList: [], - promptBlockWords: [], - responseBlockWords: [], - blockStrategy: 'full', - blockWordMask: '***' - } - this.management = { - blackGroups: [], - whiteGroups: [], - blackUsers: [], - whiteUsers: [], - defaultRateLimit: 0 - } - this.chaite = { - dataDir: 'data', - processorsDirPath: 'utils/processors', - toolsDirPath: 'utils/tools', - cloudBaseUrl: '', - cloudApiKey: '', - authKey: '', - host: '', - port: 48370 - } - this.watcher = null this.configPath = '' } @@ -216,6 +220,7 @@ class ChatGPTConfig { // 为所有嵌套对象创建Proxy this.basic = createDeepProxy(this.basic, handler) + this.bym = createDeepProxy(this.bym, handler) this.llm = createDeepProxy(this.llm, handler) this.management = createDeepProxy(this.management, handler) this.chaite = createDeepProxy(this.chaite, handler) @@ -248,6 +253,7 @@ class ChatGPTConfig { const config = { version: this.version, basic: this.basic, + bym: this.bym, llm: this.llm, management: this.management, chaite: this.chaite diff --git a/utils/message.js b/utils/message.js index 6187623..8e2a5e9 100644 --- a/utils/message.js +++ b/utils/message.js @@ -1,4 +1,5 @@ import { Chaite } from 'chaite' +import common from '../../../lib/common/common.js' /** * 将e中的消息转换为chaite的UserMessage @@ -143,3 +144,49 @@ export function checkChatMsg (e, toggleMode, togglePrefix) { } return false } + +/** + * 模型响应转为机器人格式 + * @param e + * @param {import('chaite').MessageContent[]} contents + * @returns {Promise<{ msgs: (import('icqq').TextElem | import('icqq').ImageElem | import('icqq').AtElem | import('icqq').PttElem | string)[], forward: *[]}>} + */ +export async function toYunzai (e, contents) { + /** + * 要发送的消息 + * @type {(import('icqq').TextElem | import('icqq').ImageElem | import('icqq').AtElem | import('icqq').PttElem | string)[]} + */ + const msgs = [] + /** + * 要转发的 + * @type {*[]} + */ + const forward = [] + for (let content of contents) { + switch (content.type) { + case 'text': { + msgs.push((/** @type {import('chaite').TextContent} **/ content).text) + break + } + case 'image': { + msgs.push(segment.image((/** @type {import('chaite').ImageContent} **/ content).image)) + break + } + case 'audio': { + msgs.push(segment.record((/** @type {import('chaite').AudioContent} **/ content).data)) + break + } + case 'reasoning': { + const reasoning = await common.makeForwardMsg(e, [(/** @type {import('chaite').ReasoningContent} **/ content).text], '思考过程') + forward.push(reasoning) + break + } + default: { + logger.warn(`不支持的类型 ${content.type}`) + } + } + } + return { + msgs, forward + } +}