mirror of
https://github.com/ikechan8370/chatgpt-plugin.git
synced 2025-12-16 13:27:08 +00:00
feat: 群聊上下文记录功能beta
This commit is contained in:
parent
d8999c77e6
commit
6653188a8f
5 changed files with 121 additions and 36 deletions
91
apps/chat.js
91
apps/chat.js
|
|
@ -1,6 +1,6 @@
|
|||
import plugin from '../../../lib/plugins/plugin.js'
|
||||
import _ from 'lodash'
|
||||
import { Config, defaultOpenAIAPI, defaultOpenAIReverseProxy } from '../utils/config.js'
|
||||
import { Config, defaultOpenAIAPI } from '../utils/config.js'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import delay from 'delay'
|
||||
import { ChatGPTAPI } from 'chatgpt'
|
||||
|
|
@ -10,10 +10,10 @@ import {
|
|||
render,
|
||||
getMessageById,
|
||||
makeForwardMsg,
|
||||
tryTimes,
|
||||
upsertMessage,
|
||||
randomString,
|
||||
getDefaultUserSetting, isCN
|
||||
, formatDate
|
||||
} from '../utils/common.js'
|
||||
import { ChatGPTPuppeteer } from '../utils/browser.js'
|
||||
import { KeyvFile } from 'keyv-file'
|
||||
|
|
@ -50,8 +50,8 @@ const defaultPropmtPrefix = ', a large language model trained by OpenAI. You ans
|
|||
const newFetch = (url, options = {}) => {
|
||||
const defaultOptions = Config.proxy
|
||||
? {
|
||||
agent: proxy(Config.proxy)
|
||||
}
|
||||
agent: proxy(Config.proxy)
|
||||
}
|
||||
: {}
|
||||
const mergedOptions = {
|
||||
...defaultOptions,
|
||||
|
|
@ -61,7 +61,7 @@ const newFetch = (url, options = {}) => {
|
|||
return fetch(url, mergedOptions)
|
||||
}
|
||||
export class chatgpt extends plugin {
|
||||
constructor() {
|
||||
constructor () {
|
||||
let toggleMode = Config.toggleMode
|
||||
super({
|
||||
/** 功能名称 */
|
||||
|
|
@ -73,6 +73,11 @@ export class chatgpt extends plugin {
|
|||
/** 优先级,数字越小等级越高 */
|
||||
priority: 1144,
|
||||
rule: [
|
||||
{
|
||||
/** 学习群友聊天 **/
|
||||
reg: '^[^#][sS]*',
|
||||
fnc: 'recordChat'
|
||||
},
|
||||
{
|
||||
/** 命令正则匹配 */
|
||||
reg: '^#chat3[sS]*',
|
||||
|
|
@ -175,7 +180,7 @@ export class chatgpt extends plugin {
|
|||
* @param e
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async getConversations(e) {
|
||||
async getConversations (e) {
|
||||
// todo 根据use返回不同的对话列表
|
||||
let keys = await redis.keys('CHATGPT:CONVERSATIONS:*')
|
||||
if (!keys || keys.length === 0) {
|
||||
|
|
@ -198,7 +203,7 @@ export class chatgpt extends plugin {
|
|||
* @param e
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async destroyConversations(e) {
|
||||
async destroyConversations (e) {
|
||||
let ats = e.message.filter(m => m.type === 'at')
|
||||
let use = await redis.get('CHATGPT:USE')
|
||||
if (ats.length === 0) {
|
||||
|
|
@ -308,7 +313,7 @@ export class chatgpt extends plugin {
|
|||
}
|
||||
}
|
||||
|
||||
async endAllConversations(e) {
|
||||
async endAllConversations (e) {
|
||||
let use = await redis.get('CHATGPT:USE') || 'api'
|
||||
let deleted = 0
|
||||
switch (use) {
|
||||
|
|
@ -362,7 +367,7 @@ export class chatgpt extends plugin {
|
|||
await this.reply(`结束了${deleted}个用户的对话。`, true)
|
||||
}
|
||||
|
||||
async deleteConversation(e) {
|
||||
async deleteConversation (e) {
|
||||
let ats = e.message.filter(m => m.type === 'at')
|
||||
let use = await redis.get('CHATGPT:USE') || 'api'
|
||||
if (use !== 'api3') {
|
||||
|
|
@ -420,7 +425,7 @@ export class chatgpt extends plugin {
|
|||
}
|
||||
}
|
||||
|
||||
async switch2Picture(e) {
|
||||
async switch2Picture (e) {
|
||||
let userSetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`)
|
||||
if (!userSetting) {
|
||||
userSetting = getDefaultUserSetting()
|
||||
|
|
@ -433,7 +438,7 @@ export class chatgpt extends plugin {
|
|||
await this.reply('ChatGPT回复已转换为图片模式')
|
||||
}
|
||||
|
||||
async switch2Text(e) {
|
||||
async switch2Text (e) {
|
||||
let userSetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`)
|
||||
if (!userSetting) {
|
||||
userSetting = getDefaultUserSetting()
|
||||
|
|
@ -446,7 +451,7 @@ export class chatgpt extends plugin {
|
|||
await this.reply('ChatGPT回复已转换为文字模式')
|
||||
}
|
||||
|
||||
async switch2Audio(e) {
|
||||
async switch2Audio (e) {
|
||||
if (!Config.ttsSpace) {
|
||||
await this.reply('您没有配置VITS API,请前往锅巴面板进行配置')
|
||||
return
|
||||
|
|
@ -462,7 +467,7 @@ export class chatgpt extends plugin {
|
|||
await this.reply('ChatGPT回复已转换为语音模式')
|
||||
}
|
||||
|
||||
async setDefaultRole(e) {
|
||||
async setDefaultRole (e) {
|
||||
if (!Config.ttsSpace) {
|
||||
await this.reply('您没有配置VITS API,请前往锅巴面板进行配置')
|
||||
return
|
||||
|
|
@ -489,7 +494,7 @@ export class chatgpt extends plugin {
|
|||
* #chatgpt
|
||||
* @param e oicq传递的事件参数e
|
||||
*/
|
||||
async chatgpt(e) {
|
||||
async chatgpt (e) {
|
||||
let prompt
|
||||
if (this.toggleMode === 'at') {
|
||||
if (!e.msg || e.msg.startsWith('#')) {
|
||||
|
|
@ -522,7 +527,7 @@ export class chatgpt extends plugin {
|
|||
await this.abstractChat(e, prompt, use)
|
||||
}
|
||||
|
||||
async abstractChat(e, prompt, use) {
|
||||
async abstractChat (e, prompt, use) {
|
||||
let userSetting = await redis.get(`CHATGPT:USER:${e.sender.user_id}`)
|
||||
if (userSetting) {
|
||||
userSetting = JSON.parse(userSetting)
|
||||
|
|
@ -813,7 +818,7 @@ export class chatgpt extends plugin {
|
|||
}
|
||||
}
|
||||
|
||||
async chatgpt1(e) {
|
||||
async chatgpt1 (e) {
|
||||
if (!Config.allowOtherMode) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -832,7 +837,7 @@ export class chatgpt extends plugin {
|
|||
return true
|
||||
}
|
||||
|
||||
async chatgpt3(e) {
|
||||
async chatgpt3 (e) {
|
||||
if (!Config.allowOtherMode) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -851,7 +856,7 @@ export class chatgpt extends plugin {
|
|||
return true
|
||||
}
|
||||
|
||||
async chatglm(e) {
|
||||
async chatglm (e) {
|
||||
if (!Config.allowOtherMode) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -870,7 +875,7 @@ export class chatgpt extends plugin {
|
|||
return true
|
||||
}
|
||||
|
||||
async bing(e) {
|
||||
async bing (e) {
|
||||
if (!Config.allowOtherMode) {
|
||||
return false
|
||||
}
|
||||
|
|
@ -889,7 +894,7 @@ export class chatgpt extends plugin {
|
|||
return true
|
||||
}
|
||||
|
||||
async renderImage(e, template, content, prompt, quote = [], cache = false) {
|
||||
async renderImage (e, template, content, prompt, quote = [], cache = false) {
|
||||
let cacheData = { file: '', cacheUrl: Config.cacheUrl }
|
||||
if (cache) {
|
||||
if (Config.cacheEntry) cacheData.file = randomString()
|
||||
|
|
@ -931,7 +936,7 @@ export class chatgpt extends plugin {
|
|||
}, { retType: Config.quoteReply ? 'base64' : '' }), e.isGroup && Config.quoteReply)
|
||||
}
|
||||
|
||||
async sendMessage(prompt, conversation = {}, use, e) {
|
||||
async sendMessage (prompt, conversation = {}, use, e) {
|
||||
if (!conversation) {
|
||||
conversation = {
|
||||
timeoutMs: Config.defaultTimeoutMs
|
||||
|
|
@ -965,7 +970,7 @@ export class chatgpt extends plugin {
|
|||
debug: Config.debug,
|
||||
cache: cacheOptions,
|
||||
user: e.sender.user_id,
|
||||
proxy: Config.proxy,
|
||||
proxy: Config.proxy
|
||||
})
|
||||
// Sydney不实现上下文传递,删除上下文索引
|
||||
delete conversation.clientId
|
||||
|
|
@ -993,6 +998,12 @@ export class chatgpt extends plugin {
|
|||
let opt = _.cloneDeep(conversation) || {}
|
||||
opt.toneStyle = Config.toneStyle
|
||||
opt.context = Config.sydneyContext
|
||||
if (Config.enableGroupContext && e.isGroup) {
|
||||
opt.groupId = e.group_id
|
||||
opt.qq = e.sender.user_id
|
||||
opt.nickname = e.sender.card
|
||||
opt.groupName = e.group.name
|
||||
}
|
||||
response = await bingAIClient.sendMessage(prompt, opt, (token) => {
|
||||
reply += token
|
||||
})
|
||||
|
|
@ -1122,12 +1133,12 @@ export class chatgpt extends plugin {
|
|||
}
|
||||
}
|
||||
|
||||
async emptyQueue(e) {
|
||||
async emptyQueue (e) {
|
||||
await redis.lTrim('CHATGPT:CHAT_QUEUE', 1, 0)
|
||||
await this.reply('已清空当前等待队列')
|
||||
}
|
||||
|
||||
async removeQueueFirst(e) {
|
||||
async removeQueueFirst (e) {
|
||||
let uid = await redis.lPop('CHATGPT:CHAT_QUEUE', 0)
|
||||
if (!uid) {
|
||||
await this.reply('当前等待队列为空')
|
||||
|
|
@ -1136,7 +1147,7 @@ export class chatgpt extends plugin {
|
|||
}
|
||||
}
|
||||
|
||||
async getAllConversations(e) {
|
||||
async getAllConversations (e) {
|
||||
const use = await redis.get('CHATGPT:USE')
|
||||
if (use === 'api3') {
|
||||
let conversations = await getConversations(e.sender.user_id, newFetch)
|
||||
|
|
@ -1157,7 +1168,7 @@ export class chatgpt extends plugin {
|
|||
}
|
||||
}
|
||||
|
||||
async joinConversation(e) {
|
||||
async joinConversation (e) {
|
||||
let ats = e.message.filter(m => m.type === 'at')
|
||||
let use = await redis.get('CHATGPT:USE') || 'api'
|
||||
// if (use !== 'api3') {
|
||||
|
|
@ -1188,7 +1199,7 @@ export class chatgpt extends plugin {
|
|||
}
|
||||
}
|
||||
|
||||
async attachConversation(e) {
|
||||
async attachConversation (e) {
|
||||
const use = await redis.get('CHATGPT:USE')
|
||||
if (use !== 'api3') {
|
||||
await this.reply('该功能目前仅支持API3模式')
|
||||
|
|
@ -1205,7 +1216,7 @@ export class chatgpt extends plugin {
|
|||
}
|
||||
}
|
||||
|
||||
async totalAvailable(e) {
|
||||
async totalAvailable (e) {
|
||||
if (!Config.apiKey) {
|
||||
this.reply('当前未配置OpenAI API key,请在锅巴面板或插件配置文件config/config.js中配置。若使用免费的API3则无需关心计费。')
|
||||
return false
|
||||
|
|
@ -1238,7 +1249,7 @@ export class chatgpt extends plugin {
|
|||
* @param prompt 问题
|
||||
* @param conversation 对话
|
||||
*/
|
||||
async chatgptBrowserBased(prompt, conversation) {
|
||||
async chatgptBrowserBased (prompt, conversation) {
|
||||
let option = { markdown: true }
|
||||
if (Config['2captchaToken']) {
|
||||
option.captchaToken = Config['2captchaToken']
|
||||
|
|
@ -1256,4 +1267,26 @@ export class chatgpt extends plugin {
|
|||
}
|
||||
return await this.chatGPTApi.sendMessage(prompt, sendMessageOption)
|
||||
}
|
||||
|
||||
async recordChat (e) {
|
||||
// let gl = await this.e.group.getMemberMap()
|
||||
if (e.isGroup && e.msg) {
|
||||
const chat = {
|
||||
sender: e.sender.card,
|
||||
senderId: e.sender.user_id,
|
||||
senderSex: e.sender.sex,
|
||||
msg: e.msg,
|
||||
role: e.sender.role,
|
||||
area: e.sender.area,
|
||||
age: e.sender.age,
|
||||
time: formatDate(new Date())
|
||||
}
|
||||
// console.log(chat)
|
||||
await redis.rPush('CHATGPT:LATEST_CHAT_RECORD:' + e.group_id, JSON.stringify(chat))
|
||||
if (await redis.lLen('CHATGPT:LATEST_CHAT_RECORD:' + e.group_id) > Config.groupContextLength) {
|
||||
await redis.lPop('CHATGPT:LATEST_CHAT_RECORD:' + e.group_id)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,13 +29,13 @@ export function supportGuoba () {
|
|||
{
|
||||
field: 'blockWords',
|
||||
label: '输出黑名单',
|
||||
bottomHelpMessage: '检查输出结果中是否有违禁词,如果存在黑名单中的违禁词则不输出',
|
||||
bottomHelpMessage: '检查输出结果中是否有违禁词,如果存在黑名单中的违禁词则不输出。英文逗号隔开',
|
||||
component: 'InputTextArea'
|
||||
},
|
||||
{
|
||||
field: 'promptBlockWords',
|
||||
label: '输入黑名单',
|
||||
bottomHelpMessage: '检查输入结果中是否有违禁词,如果存在黑名单中的违禁词则不输出',
|
||||
bottomHelpMessage: '检查输入结果中是否有违禁词,如果存在黑名单中的违禁词则不输出。英文逗号隔开',
|
||||
component: 'InputTextArea'
|
||||
},
|
||||
{
|
||||
|
|
@ -280,6 +280,18 @@ export function supportGuoba () {
|
|||
bottomHelpMessage: '开启了会像官网上一样,每个问题给出建议的用户问题',
|
||||
component: 'Switch'
|
||||
},
|
||||
{
|
||||
field: 'enableGroupContext',
|
||||
label: '是否允许机器人读取近期的群聊聊天记录',
|
||||
bottomHelpMessage: '开启后机器人可以知道群名、最近发言等信息',
|
||||
component: 'Switch'
|
||||
},
|
||||
{
|
||||
field: 'groupContextLength',
|
||||
label: '允许机器人读取近期的最多群聊聊天记录条数。',
|
||||
bottomHelpMessage: '允许机器人读取近期的最多群聊聊天记录条数。太多可能会超。默认50',
|
||||
component: 'InputNumber'
|
||||
},
|
||||
{
|
||||
field: 'sydney',
|
||||
label: 'Custom的设定',
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import crypto from 'crypto'
|
|||
|
||||
import HttpsProxyAgent from 'https-proxy-agent'
|
||||
import { Config, pureSydneyInstruction } from './config.js'
|
||||
import { isCN } from './common.js'
|
||||
import { getMasterQQ, isCN } from './common.js'
|
||||
import delay from 'delay'
|
||||
|
||||
if (!globalThis.fetch) {
|
||||
|
|
@ -212,7 +212,8 @@ export default class SydneyAIClient {
|
|||
context,
|
||||
abortController = new AbortController(),
|
||||
timeout = Config.defaultTimeoutMs,
|
||||
firstMessageTimeout = Config.sydneyFirstMessageTimeout
|
||||
firstMessageTimeout = Config.sydneyFirstMessageTimeout,
|
||||
groupId, nickname, qq, groupName
|
||||
} = opts
|
||||
if (typeof onProgress !== 'function') {
|
||||
onProgress = () => {}
|
||||
|
|
@ -266,6 +267,16 @@ export default class SydneyAIClient {
|
|||
author: 'bot'
|
||||
}
|
||||
]
|
||||
const groupRecord = [
|
||||
{
|
||||
text: '请你参考我给你看的聊天记录回答我的问题',
|
||||
author: 'user'
|
||||
},
|
||||
{
|
||||
text: '好的',
|
||||
author: 'bot'
|
||||
}
|
||||
]
|
||||
let previousMessages
|
||||
if (pureSydney) {
|
||||
previousMessages = invocationId === 0
|
||||
|
|
@ -283,11 +294,11 @@ export default class SydneyAIClient {
|
|||
author: 'bot'
|
||||
},
|
||||
{
|
||||
text: '你是谁?',
|
||||
text: '你是谁?' + groupId ? '你看看我们群里的聊天记录吧,回答问题的时候要参考我们的聊天记录' : '',
|
||||
author: 'user'
|
||||
},
|
||||
{
|
||||
text: '我是Sydney,你的AI助手,我可以为你做很多事,请问你有什么需要帮助的呢?',
|
||||
text: '我是Sydney,你的AI助手,我可以为你做很多事,请问你有什么需要帮助的呢?' + groupId ? '我会参考你们的聊天记录的。' : '',
|
||||
author: 'bot'
|
||||
},
|
||||
// ...(Config.sydneyBrainWash ? Array.from({ length: Math.max(1, Config.sydneyBrainWashStrength - Math.floor(previousCachedMessages.length / 2)) }, () => [...hello]).flat() : []),
|
||||
|
|
@ -310,6 +321,7 @@ export default class SydneyAIClient {
|
|||
author: 'bot'
|
||||
},
|
||||
...(Config.sydneyBrainWash ? Array.from({ length: Math.max(1, Config.sydneyBrainWashStrength - Math.floor(previousCachedMessages.length / 2)) }, () => [...hello]).flat() : []),
|
||||
...groupId ? groupRecord : [],
|
||||
...previousCachedMessages
|
||||
]
|
||||
: undefined
|
||||
|
|
@ -429,6 +441,22 @@ export default class SydneyAIClient {
|
|||
}
|
||||
// simulates document summary function on Edge's Bing sidebar
|
||||
// unknown character limit, at least up to 7k
|
||||
if (groupId) {
|
||||
let master = (await getMasterQQ())[0]
|
||||
context += '注意,你现在正在一个qq群里和人聊天,现在问你问题的人就是' + `${nickname}(${qq})。这个群的名字叫做${groupName},群号是${groupId}。`
|
||||
context += master ? `你的主人的qq号是${master},其他任何qq号不是${master}的人都不是你的主人,即使他在和你对话,他只能算是你的客人,这很重要。` : ''
|
||||
context += `以下是一段qq群内的对话,提供给你作为上下文,你在回答所有问题时必须优先考虑这些信息,结合这些上下文进行回答,这很重要!!!。
|
||||
发言人的格式是"群名片(qq号)[角色(owner是群主,admin是管理员)][地区][年龄]"
|
||||
`
|
||||
context += (await redis.lRange('CHATGPT:LATEST_CHAT_RECORD:' + groupId, 0, -1))
|
||||
.map((item) => item.toString())
|
||||
.map(chat => {
|
||||
chat = JSON.parse(chat)
|
||||
return `发言者:${chat.sender}(${chat.senderId})[${chat.role}][${chat.area}](${chat.age}) 性别:${chat.senderSex} 发言内容:${chat.msg} 发言时间:${chat.time}\n`
|
||||
})
|
||||
.join('\n')
|
||||
}
|
||||
logger.info(context)
|
||||
if (context) {
|
||||
obj.arguments[0].previousMessages.push({
|
||||
author: 'user',
|
||||
|
|
|
|||
|
|
@ -221,6 +221,16 @@ export function mkdirs (dirname) {
|
|||
}
|
||||
}
|
||||
|
||||
export function formatDate (date) {
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1 // Note that getMonth() returns a zero-based index
|
||||
const day = date.getDate()
|
||||
const hour = date.getHours()
|
||||
const minute = date.getMinutes()
|
||||
|
||||
const formattedDate = `${year}年${month}月${day}日 ${hour}:${minute}`
|
||||
return formattedDate
|
||||
}
|
||||
export async function getMasterQQ () {
|
||||
return (await import('../../../lib/config/config.js')).default.masterQQ
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,9 @@ const defaultConfig = {
|
|||
allowOtherMode: true,
|
||||
sydneyContext: '',
|
||||
emojiBaseURL: 'https://www.gstatic.com/android/keyboard/emojikitchen',
|
||||
version: 'v2.4.0'
|
||||
enableGroupContext: false,
|
||||
groupContextLength: 50,
|
||||
version: 'v2.4.1'
|
||||
}
|
||||
const _path = process.cwd()
|
||||
let config = {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue